您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
过滤并隐藏小红书网页版中作者ID包含特定字词的帖子
// ==UserScript== // @name 小红书作者ID过滤器 // @namespace http://tampermonkey.net/ // @version 4.0 // @description 过滤并隐藏小红书网页版中作者ID包含特定字词的帖子 // @author ObenK // @license MIT // @match https://www.xiaohongshu.com/* // @match https://www.xiaohongshu.com/explore* // @match https://www.xiaohongshu.com/search_result* // @match https://www.xiaohongshu.com/user/profile/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== (function() { 'use strict'; // 添加CSS样式 const style = document.createElement('style'); style.textContent = ` .xhs-filter-hidden { display: none !important; visibility: hidden !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } .xhs-title-hidden .title, .xhs-title-hidden .footer .title, .xhs-title-hidden [class*="title"] { display: none !important; visibility: hidden !important; height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; } `; document.head.appendChild(style); // 配置类 class Config { static get DEFAULT_KEYWORDS() { return ['丰田', '搬运工', '代购', '广告']; } static get HIDE_TITLES() { return GM_getValue('hideTitles', false); } static set HIDE_TITLES(value) { GM_setValue('hideTitles', value); } static get BLOCKED_KEYWORDS() { const saved = GM_getValue('blockedKeywords', null); if (saved === null) { // 首次安装时设置默认关键词 this.BLOCKED_KEYWORDS = this.DEFAULT_KEYWORDS; return this.DEFAULT_KEYWORDS; } return saved; } static set BLOCKED_KEYWORDS(keywords) { GM_setValue('blockedKeywords', keywords); } static addKeyword(keyword) { const keywords = this.BLOCKED_KEYWORDS; const normalizedKeyword = keyword.trim(); if (normalizedKeyword && !keywords.some(k => k.toLowerCase() === normalizedKeyword.toLowerCase())) { keywords.push(normalizedKeyword); this.BLOCKED_KEYWORDS = keywords; return true; } return false; } static removeKeyword(keyword) { const keywords = this.BLOCKED_KEYWORDS; const index = keywords.indexOf(keyword); if (index > -1) { keywords.splice(index, 1); this.BLOCKED_KEYWORDS = keywords; } } static resetToDefault() { this.BLOCKED_KEYWORDS = this.DEFAULT_KEYWORDS; } // 统计相关方法 static getKeywordStats() { return GM_getValue('keywordStats', {}); } static incrementKeywordHit(keyword) { const stats = this.getKeywordStats(); stats[keyword] = (stats[keyword] || 0) + 1; GM_setValue('keywordStats', stats); } static resetKeywordStats() { GM_setValue('keywordStats', {}); } } // 高性能过滤器类 class PostFilter { constructor() { this.processedPosts = new WeakSet(); this.observer = null; this.retryTimer = null; this.batchTimer = null; this.pendingPosts = []; this.keywordRegex = null; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.startFiltering()); } else { this.startFiltering(); } this.updateKeywordRegex(); this.setupApiInterceptor(); // 添加API拦截 } updateKeywordRegex() { const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { this.keywordRegex = null; } else { // 使用更精确的正则表达式,确保大小写不敏感匹配 const escapedKeywords = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); this.keywordRegex = new RegExp(escapedKeywords.join('|'), 'i'); } } startFiltering() { this.filterExistingPosts(); this.setupMutationObserver(); setTimeout(() => this.filterExistingPosts(), 100); setTimeout(() => this.filterExistingPosts(), 500); } filterExistingPosts() { if (!this.keywordRegex) return; const posts = this.getAllPosts(); const newPosts = posts.filter(post => !this.processedPosts.has(post)); if (newPosts.length === 0) return; this.processPosts(newPosts); } processPosts(posts) { if (!this.keywordRegex) return; let filteredCount = 0; for (const post of posts) { if (this.processedPosts.has(post)) continue; const authorId = this.getAuthorId(post); if (!authorId) continue; // 使用正则表达式一次性匹配所有关键词 const match = authorId.match(this.keywordRegex); if (match) { this.hidePost(post, authorId); filteredCount++; // 更新统计 try { Config.incrementKeywordHit(match[0]); } catch (e) { console.error('[小红书过滤器] 统计更新失败:', e); } } this.processedPosts.add(post); } if (filteredCount > 0) { console.log(`[小红书过滤器] 处理了 ${posts.length} 个帖子,过滤了 ${filteredCount} 个`); } } setupMutationObserver() { // 增强的MutationObserver,处理无限滚动 this.observer = new MutationObserver((mutations) => { let hasNewPosts = false; const allNewPosts = []; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // 处理节点本身和子节点 const posts = this.getPostsFromNode(node); if (posts.length > 0) { allNewPosts.push(...posts); hasNewPosts = true; } // 处理延迟加载的内容 setTimeout(() => { const delayedPosts = this.getPostsFromNode(node); const newDelayedPosts = delayedPosts.filter(post => !this.processedPosts.has(post)); if (newDelayedPosts.length > 0) { this.processPosts(newDelayedPosts); } }, 200); } }); }); if (allNewPosts.length > 0) { const newPosts = allNewPosts.filter(post => !this.processedPosts.has(post)); if (newPosts.length > 0) { this.processPosts(newPosts); } } }); this.observer.observe(document.body, { childList: true, subtree: true }); // 额外监听滚动事件,处理无限滚动 let scrollTimer = null; window.addEventListener('scroll', () => { if (scrollTimer) clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { this.filterExistingPosts(); }, 300); }); } getAllPosts() { // 增强选择器,覆盖更多小红书页面结构 const selectors = [ '.note-item', '[data-testid="note-item"]', '.feeds-container .note-item', '.search-container .note-item', '[class*="note-item"]' ]; const posts = []; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => posts.push(el)); }); return [...new Set(posts)]; } getPostsFromNode(node) { if (!node || !node.querySelectorAll) return []; const selectors = [ '.note-item', '[data-testid="note-item"]', '[class*="note-item"]' ]; const posts = []; selectors.forEach(selector => { const elements = node.querySelectorAll(selector); elements.forEach(el => posts.push(el)); }); return [...new Set(posts)]; } getAuthorId(post) { // 基于提供的HTML结构,优先查找.author-wrapper下的.name元素 const authorWrapper = post.querySelector('.author-wrapper'); if (authorWrapper) { const nameElement = authorWrapper.querySelector('.name'); if (nameElement) { return nameElement.textContent?.trim(); } } // 备用选择器 const selectors = [ '.author .name', '.author-name', '[data-testid="author-name"]', '.user-name', '.nickname', 'a[href*="/user/profile/"] span', '[class*="author"] [class*="name"]' ]; for (const selector of selectors) { const element = post.querySelector(selector); if (element) { const text = element.textContent?.trim(); if (text) return text; } } // 从用户链接中提取 const userLink = post.querySelector('a[href*="/user/profile/"]'); if (userLink) { const nameSpan = userLink.querySelector('span'); if (nameSpan) { return nameSpan.textContent?.trim(); } const linkText = userLink.textContent?.trim(); if (linkText && linkText !== '') { return linkText; } } return null; } hidePost(post, authorId) { // 使用CSS类替代直接样式操作 post.classList.add('xhs-filter-hidden'); post.setAttribute('data-filtered', 'true'); post.setAttribute('data-filter-reason', `作者ID包含屏蔽关键词: ${authorId}`); console.log(`[小红书过滤器] 已隐藏作者 "${authorId}" 的帖子`); } // 强制重新过滤所有帖子 forceReFilter() { // 重新显示所有被隐藏的帖子 this.restoreHiddenPosts(); // 清空已处理集合 this.processedPosts = new WeakSet(); // 重新更新正则表达式 this.updateKeywordRegex(); // 重新过滤所有帖子 this.filterExistingPosts(); console.log('[小红书过滤器] 已强制重新过滤所有帖子'); } // 恢复所有被隐藏的帖子 restoreHiddenPosts() { const hiddenPosts = document.querySelectorAll('[data-filtered="true"]'); hiddenPosts.forEach(post => { post.classList.remove('xhs-filter-hidden'); post.removeAttribute('data-filtered'); post.removeAttribute('data-filter-reason'); }); console.log(`[小红书过滤器] 已恢复 ${hiddenPosts.length} 个被隐藏的帖子`); } // 验证API拦截有效性 setupApiInterceptor() { const self = this; // 测试API拦截是否生效 console.log('[API验证] 开始设置API拦截器...'); // 增强的Fetch拦截 - 验证有效性 const originalFetch = window.fetch; window.fetch = async function(...args) { const url = args[0]; const urlStr = typeof url === 'string' ? url : (url?.url || url?.href || ''); // 更精确的API检测 const isXHSApi = urlStr.includes('/api/sns/web/v1/feed') || urlStr.includes('/api/sns/web/v1/search') || urlStr.includes('/api/sns/web/v1/homefeed') || urlStr.includes('/api/sns/web/v1/search/notes') || urlStr.includes('/api/sns/web/v1/search/notes/v1'); if (isXHSApi) { console.log(`[API检测] 拦截到小红书API: ${urlStr}`); try { const response = await originalFetch.apply(this, args); const clone = response.clone(); const data = await clone.json(); console.log('[API数据] 成功获取API响应'); // 验证数据结构 if (data && typeof data === 'object') { let filteredCount = 0; let totalItems = 0; // 检测所有可能的数据结构 const items = data.data?.items || data.data?.notes || data.data || []; if (Array.isArray(items)) { totalItems = items.length; // 过滤逻辑 const filteredItems = items.filter(item => { const user = item?.note_card?.user || item?.user; if (user) { const authorId = user.nickname || user.name || user.display_name || ''; const shouldFilter = self.shouldFilterAuthor(authorId); if (shouldFilter) { // 获取匹配的关键词并更新统计 const match = authorId.match(self.keywordRegex); if (match) { Config.incrementKeywordHit(match[0]); console.log(`[API过滤] 过滤作者: ${authorId} (关键词: ${match[0]})`); } } return !shouldFilter; } return true; }); filteredCount = totalItems - filteredItems.length; // 更新数据结构 if (data.data?.items) { data.data.items = filteredItems; } else if (data.data?.notes) { data.data.notes = filteredItems; } else if (Array.isArray(data.data)) { data.data = filteredItems; } if (filteredCount > 0) { console.log(`[API过滤] 从 ${totalItems} 个帖子中过滤了 ${filteredCount} 个`); } } } return new Response(JSON.stringify(data), { status: response.status, statusText: response.statusText, headers: response.headers }); } catch (e) { console.error('[API拦截异常]', e); return originalFetch.apply(this, args); } } return originalFetch.apply(this, args); }; // 增强XHR拦截 - 验证有效性 const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._xhsFilterUrl = typeof url === 'string' ? url : ''; return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { const originalOnReadyStateChange = this.onreadystatechange; this.onreadystatechange = function() { if (this.readyState === 4 && this._xhsFilterUrl) { const isXHSApi = this._xhsFilterUrl.includes('/api/sns/web/v1/feed') || this._xhsFilterUrl.includes('/api/sns/web/v1/search') || this._xhsFilterUrl.includes('/api/sns/web/v1/homefeed') || this._xhsFilterUrl.includes('/api/sns/web/v1/search/notes'); if (isXHSApi) { console.log(`[XHR检测] 拦截到小红书XHR: ${this._xhsFilterUrl}`); try { const data = JSON.parse(this.responseText); let filteredCount = 0; let totalItems = 0; // 检测所有可能的数据结构 const items = data.data?.items || data.data?.notes || data.data || []; if (Array.isArray(items)) { totalItems = items.length; const filteredItems = items.filter(item => { const user = item?.note_card?.user || item?.user; if (user) { const authorId = user.nickname || user.name || user.display_name || ''; const shouldFilter = self.shouldFilterAuthor(authorId); if (shouldFilter) { // 获取匹配的关键词并更新统计 const match = authorId.match(self.keywordRegex); if (match) { Config.incrementKeywordHit(match[0]); console.log(`[XHR过滤] 过滤作者: ${authorId} (关键词: ${match[0]})`); } } return !shouldFilter; } return true; }); filteredCount = totalItems - filteredItems.length; // 更新响应数据 if (data.data?.items) { data.data.items = filteredItems; } else if (data.data?.notes) { data.data.notes = filteredItems; } else if (Array.isArray(data.data)) { data.data = filteredItems; } if (filteredCount > 0) { console.log(`[XHR过滤] 从 ${totalItems} 个帖子中过滤了 ${filteredCount} 个`); } } try { Object.defineProperty(this, 'responseText', { value: JSON.stringify(data), writable: true, configurable: true }); } catch (e) { console.warn('[XHR修改异常]', e); } } catch (e) { console.error('[XHR拦截异常]', e); } } } if (originalOnReadyStateChange) { originalOnReadyStateChange.call(this); } }; return originalXHRSend.apply(this, arguments); }; } // 判断是否应该过滤作者 shouldFilterAuthor(authorId) { if (!this.keywordRegex || !authorId || typeof authorId !== 'string') return false; return this.keywordRegex.test(authorId.trim()); } } // 可视化管理类 class VisualizationManager { static createVisualizationPanel() { const panel = document.createElement('div'); panel.id = 'xhs-visualization-panel'; panel.innerHTML = ` <div style=" position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 1px solid #ddd; border-radius: 12px; padding: 25px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); z-index: 10001; max-width: 600px; width: 95%; max-height: 80vh; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; "> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> <h3 style="margin: 0; color: #333; font-size: 20px;">📊 数据可视化中心</h3> <button onclick="this.parentElement.parentElement.parentElement.remove()" style=" background: none; border: none; font-size: 24px; cursor: pointer; color: #666; ">×</button> </div> <!-- 热力图 --> <div style="margin-bottom: 25px;"> <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">🌡️ 时间屏蔽热力图</h4> <div id="heatmap-container" style="background: #f8f9fa; border-radius: 8px; padding: 15px;"> ${this.renderHeatmap()} </div> </div> <!-- 词云动画 --> <div style="margin-bottom: 25px;"> <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">☁️ 动态词云</h4> <div id="wordcloud-container" style="background: #f8f9fa; border-radius: 8px; padding: 20px; text-align: center;"> ${this.renderWordCloud()} </div> </div> <!-- 统计图表 --> <div style="margin-bottom: 20px;"> <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">📊 统计图表</h4> <div id="stats-container" style="background: #f8f9fa; border-radius: 8px; padding: 20px;"> ${this.renderStats()} </div> </div> </div> <div style=" position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; "></div> `; document.body.appendChild(panel); // 添加ESC键关闭功能 const escHandler = (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); panel.remove(); document.removeEventListener('keydown', escHandler, true); } }; document.addEventListener('keydown', escHandler, true); // 点击遮罩关闭 const clickHandler = (e) => { if (e.target === panel || e.target === panel.lastElementChild) { panel.remove(); document.removeEventListener('keydown', escHandler, true); document.removeEventListener('click', clickHandler); } }; panel.addEventListener('click', clickHandler); } static renderHeatmap() { const stats = Config.getKeywordStats(); const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { return '<p style="color: #666; text-align: center;">暂无数据</p>'; } // 获取当前时间,只显示到当前小时 const now = new Date(); const currentHour = now.getHours(); // 只显示到当前小时的真实数据 const hours = Array.from({length: currentHour + 1}, (_, i) => i); const heatmapData = hours.map(hour => { // 使用真实统计数据的简化版本 const totalHits = Object.values(stats).reduce((sum, hits) => sum + hits, 0); const intensity = totalHits > 0 ? Math.min(0.8, (Object.values(stats).reduce((sum, hits) => sum + hits, 0) / 100)) : 0.1; return { hour, intensity }; }); const cells = heatmapData.map(data => { const opacity = Math.min(0.8, data.intensity * (data.hour + 1) / 24); const color = `rgba(143, 179, 208, ${opacity})`; return `<div style=" width: 30px; height: 20px; background: ${color}; border-radius: 3px; display: inline-block; margin: 1px; cursor: pointer; transition: transform 0.2s; " title="${data.hour}:00 - ${Math.round(opacity * 100)}%"></div>`; }).join(''); return ` <div style="display: flex; flex-wrap: wrap; gap: 2px; justify-content: center;"> ${cells} <div style="width: 100%; text-align: center; margin-top: 10px; font-size: 12px; color: #666;"> 00:00 - ${currentHour}:00 (当前时间) </div> </div> `; } static renderWordCloud() { const stats = Config.getKeywordStats(); const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { return '<p style="color: #666; text-align: center;">暂无关键词</p>'; } const sortedKeywords = keywords .map(keyword => ({ keyword, hits: stats[keyword] || 0 })) .sort((a, b) => b.hits - a.hits); const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1); const words = sortedKeywords.map(item => { const size = 12 + (item.hits / maxHits) * 24; const opacity = 0.4 + (item.hits / maxHits) * 0.6; return `<span style=" font-size: ${size}px; color: rgba(90, 125, 154, ${opacity}); margin: 5px; display: inline-block; transition: all 0.3s ease; cursor: pointer; " onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">${item.keyword}</span>`; }).join(''); return `<div style="line-height: 1.5;">${words}</div>`; } static renderStats() { const stats = Config.getKeywordStats(); const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { return '<p style="color: #666; text-align: center;">暂无数据</p>'; } const sortedKeywords = keywords .map(keyword => ({ keyword, hits: stats[keyword] || 0 })) .sort((a, b) => b.hits - a.hits) .slice(0, 8); // 只显示前8个 const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1); const bars = sortedKeywords.map((item, index) => { const height = 20 + (item.hits / maxHits) * 80; const color = `hsl(200, 50%, ${50 + (item.hits / maxHits) * 30}%)`; return ` <div style=" display: inline-block; margin: 0 5px; text-align: center; "> <div style=" width: 30px; height: ${height}px; background: ${color}; border-radius: 3px 3px 0 0; position: relative; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: all 0.3s ease; " title="${item.keyword}: ${item.hits}次"> <div style=" position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: #666; white-space: nowrap; ">${item.keyword}</div> </div> </div> `; }).join(''); return `<div style="text-align: center; padding: 20px 0;">${bars}</div>`; } } // 管理界面 class SettingsUI { static createPanel() { const panel = document.createElement('div'); panel.id = 'xhs-filter-panel'; panel.innerHTML = ` <div style=" position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 1px solid #ddd; border-radius: 12px; padding: 25px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); z-index: 10000; max-width: 600px; width: 95%; max-height: 80vh; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; "> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> <h3 style="margin: 0; color: #333; font-size: 20px;">📊 小红书作者ID过滤器</h3> <div style="display: flex; gap: 10px;"> <button id="show-visualization" style=" padding: 6px 12px; background: #8fb3d0; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; ">📊 可视化</button> <button onclick="this.parentElement.parentElement.parentElement.parentElement.remove()" style=" background: none; border: none; font-size: 24px; cursor: pointer; color: #666; ">×</button> </div> </div> <div style="margin-bottom: 15px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;"> <label style="font-weight: bold;">屏蔽关键词:</label> <span style="color: #666; font-size: 13px;">共 ${Config.BLOCKED_KEYWORDS.length} 个</span> </div> <div id="keyword-list" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 8px;"> ${this.renderKeywords()} </div> <div style="display: flex; gap: 10px;"> <input type="text" id="new-keyword" placeholder="输入关键词" style=" flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; "> <button id="add-keyword" style=" padding: 8px 16px; background: #ff2442; color: white; border: none; border-radius: 4px; cursor: pointer; ">添加</button> </div> </div> <div style="margin-bottom: 15px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;"> <label style="font-weight: bold;">关键词统计:</label> <button id="reset-stats" style=" padding: 4px 8px; background: #ff9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; ">清零</button> </div> <div id="keyword-stats" style="margin-bottom: 10px; max-height: 150px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 8px; font-size: 13px;"> ${this.renderKeywordStats()} </div> </div> <div style="margin-bottom: 15px;"> <div style="display: flex; align-items: center; margin-bottom: 10px;"> <input type="checkbox" id="hide-titles-checkbox" ${Config.HIDE_TITLES ? 'checked' : ''} style=" margin-right: 8px; width: 16px; height: 16px; cursor: pointer; appearance: auto; -webkit-appearance: checkbox; -moz-appearance: checkbox; opacity: 1; visibility: visible; position: static; "> <label for="hide-titles-checkbox" style="font-weight: bold; cursor: pointer;"> 隐藏帖子标题 </label> </div> <div style="font-size: 12px; color: #666; margin-left: 24px;"> 开启后将隐藏所有帖子的标题内容 </div> </div> <div style="display: flex; gap: 10px; margin-bottom: 15px;"> <button id="copy-keywords" style=" flex: 1; padding: 8px 16px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; ">复制全部</button> <button id="paste-keywords" style=" flex: 1; padding: 8px 16px; background: #4caf50; color: white; border: none; border-radius: 4px; cursor: pointer; ">批量粘贴</button> </div> <div style="text-align: right;"> <button id="reset-keywords" style=" padding: 8px 16px; background: #ff9800; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; ">重置默认</button> <button id="close-panel" style=" padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer; ">关闭</button> </div> </div> <div style=" position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999; "></div> `; document.body.appendChild(panel); // 自动聚焦到输入框 setTimeout(() => { const input = panel.querySelector('#new-keyword'); if (input) { input.focus(); input.select(); } }, 100); // 绑定事件 panel.querySelector('#add-keyword').addEventListener('click', () => { const input = panel.querySelector('#new-keyword'); const keyword = input.value.trim(); if (keyword) { const added = Config.addKeyword(keyword); if (added) { input.value = ''; panel.querySelector('#keyword-list').innerHTML = this.renderKeywords(); panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`; // 强制重新过滤所有帖子 window.xhsFilter.forceReFilter(); input.focus(); // 添加后重新聚焦 } else { alert(`关键词 "${keyword}" 已存在!`); input.focus(); // 错误提示后重新聚焦 } } }); // 绑定删除关键词事件(使用事件委托) panel.querySelector('#keyword-list').addEventListener('click', (e) => { if (e.target.classList.contains('delete-keyword')) { const keyword = e.target.dataset.keyword; if (confirm(`确定要删除关键词 "${keyword}" 吗?`)) { Config.removeKeyword(keyword); // 重新渲染关键词列表 panel.querySelector('#keyword-list').innerHTML = this.renderKeywords(); // 更新计数 panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`; // 重新过滤 window.xhsFilter.forceReFilter(); } } }); // 绑定可视化按钮事件 panel.querySelector('#show-visualization').addEventListener('click', () => { VisualizationManager.createVisualizationPanel(); }); panel.querySelector('#close-panel').addEventListener('click', () => { panel.remove(); }); // 点击遮罩关闭 panel.lastElementChild.addEventListener('click', () => { panel.remove(); }); // 重置默认关键词 panel.querySelector('#reset-keywords').addEventListener('click', () => { if (confirm('确定要重置为默认关键词吗?这将清除所有自定义设置。')) { Config.resetToDefault(); panel.querySelector('#keyword-list').innerHTML = this.renderKeywords(); location.reload(); } }); // 复制关键词功能 panel.querySelector('#copy-keywords').addEventListener('click', () => { const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { alert('暂无关键词可复制!'); return; } const keywordsText = keywords.join(','); navigator.clipboard.writeText(keywordsText).then(() => { alert(`已复制 ${keywords.length} 个关键词到剪贴板!`); }).catch(() => { // 降级方案 const textarea = document.createElement('textarea'); textarea.value = keywordsText; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); alert(`已复制 ${keywords.length} 个关键词到剪贴板!`); }); }); // 批量粘贴关键词功能 panel.querySelector('#paste-keywords').addEventListener('click', () => { navigator.clipboard.readText().then(text => { const keywords = text.split(/[,,、\s]+/).filter(k => k.trim()); if (keywords.length === 0) { alert('剪贴板中没有有效的关键词!'); return; } let addedCount = 0; keywords.forEach(keyword => { if (Config.addKeyword(keyword)) { addedCount++; } }); if (addedCount > 0) { panel.querySelector('#keyword-list').innerHTML = this.renderKeywords(); panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`; alert(`成功添加 ${addedCount} 个新关键词!`); } else { alert('所有关键词都已存在,没有添加新的!'); } }).catch(() => { // 降级方案:使用prompt输入 const text = prompt('请输入关键词,用逗号分隔:'); if (text) { const keywords = text.split(/[,,、\s]+/).filter(k => k.trim()); let addedCount = 0; keywords.forEach(keyword => { if (Config.addKeyword(keyword)) { addedCount++; } }); if (addedCount > 0) { panel.querySelector('#keyword-list').innerHTML = this.renderKeywords(); panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`; alert(`成功添加 ${addedCount} 个新关键词!`); } else { alert('所有关键词都已存在,没有添加新的!'); } } }); }); // 统计清零按钮 panel.querySelector('#reset-stats').addEventListener('click', () => { if (confirm('确定要清零所有关键词的统计信息吗?')) { Config.resetKeywordStats(); panel.querySelector('#keyword-stats').innerHTML = this.renderKeywordStats(); alert('统计信息已清零!'); } }); // 绑定隐藏标题复选框事件 panel.querySelector('#hide-titles-checkbox').addEventListener('change', (e) => { Config.HIDE_TITLES = e.target.checked; window.xhsTitleHider.applyTitleHiding(); console.log(`[小红书过滤器] 标题隐藏功能已${e.target.checked ? '启用' : '禁用'}`); }); // 回车添加关键词 panel.querySelector('#new-keyword').addEventListener('keypress', (e) => { if (e.key === 'Enter') { panel.querySelector('#add-keyword').click(); } }); // ESC键退出编辑界面(使用捕获阶段确保优先级) const escHandler = (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); panel.remove(); document.removeEventListener('keydown', escHandler, true); } }; document.addEventListener('keydown', escHandler, true); // 点击面板外部关闭 const clickHandler = (e) => { if (e.target === panel || e.target === panel.lastElementChild) { panel.remove(); document.removeEventListener('keydown', escHandler, true); document.removeEventListener('click', clickHandler); } }; panel.addEventListener('click', clickHandler); // 确保ESC键在输入框中也能工作 panel.querySelector('#new-keyword').addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); panel.remove(); document.removeEventListener('keydown', escHandler, true); document.removeEventListener('click', clickHandler); } }); } static renderKeywords() { const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { return '<p style="color: #666; font-style: italic;">暂无屏蔽关键词</p>'; } return ` <div style=" display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; max-height: 150px; overflow-y: auto; padding: 5px; "> ${keywords.map(keyword => ` <span style=" display: inline-flex; align-items: center; background: #f0f0f0; border: 1px solid #ddd; border-radius: 16px; padding: 4px 10px; font-size: 13px; color: #333; cursor: default; transition: all 0.2s; " onmouseover="this.style.background='#ffe0e0'" onmouseout="this.style.background='#f0f0f0'"> ${keyword} <span class="delete-keyword" data-keyword="${keyword.replace(/"/g, '"')}" style=" background: none; border: none; color: #ff2442; cursor: pointer; font-size: 16px; margin-left: 6px; padding: 0; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; user-select: none; ">×</span> </span> `).join('')} </div> `; } static renderKeywordStats() { const stats = Config.getKeywordStats(); const keywords = Config.BLOCKED_KEYWORDS; if (keywords.length === 0) { return '<p style="color: #666; font-style: italic;">暂无统计信息</p>'; } // 按命中次数降序排序 const sortedKeywords = keywords .map(keyword => ({ keyword, hits: stats[keyword] || 0 })) .sort((a, b) => b.hits - a.hits); // 计算最大命中次数用于比例显示 const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1); let totalHits = 0; const statsHtml = sortedKeywords.map(item => { totalHits += item.hits; const percentage = (item.hits / maxHits) * 100; return ` <div style="display: flex; align-items: center; margin-bottom: 6px; gap: 8px;"> <span style="font-size: 12px; min-width: 60px; white-space: nowrap;">${item.keyword}</span> <div style="flex: 1; background: #f5f5f5; border-radius: 10px; height: 6px; overflow: hidden;"> <div style="background: #8fb3d0; height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div> </div> <span style="font-size: 12px; color: #5a7d9a; font-weight: bold; min-width: 20px; text-align: right;">${item.hits}</span> </div> `; }).join(''); return ` <div> <div style="display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #eee; margin-bottom: 6px; font-weight: bold; font-size: 13px;"> <span>总计</span> <span style="color: #5a7d9a;">${totalHits}</span> </div> ${statsHtml} </div> `; } } // 键盘快捷键管理 class KeyboardShortcuts { static init() { let isPanelOpen = false; document.addEventListener('keydown', (e) => { // 检测 Ctrl+Shift+K 或 Cmd+Shift+K if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'K') { e.preventDefault(); // 检查是否已有面板打开 const existingPanel = document.getElementById('xhs-filter-panel'); if (existingPanel) { existingPanel.remove(); isPanelOpen = false; } else { SettingsUI.createPanel(); isPanelOpen = true; } } }); console.log('[小红书过滤器] 快捷键已启用:Ctrl+Shift+K 或 Cmd+Shift+K'); } } // 标题隐藏管理器 class TitleHider { constructor() { this.observer = null; this.init(); } init() { this.applyTitleHiding(); this.setupMutationObserver(); } applyTitleHiding() { const shouldHide = Config.HIDE_TITLES; const container = document.body; if (shouldHide) { container.classList.add('xhs-title-hidden'); console.log('[小红书过滤器] 已启用标题隐藏功能'); } else { container.classList.remove('xhs-title-hidden'); console.log('[小红书过滤器] 已禁用标题隐藏功能'); } } setupMutationObserver() { // 监听新内容加载 this.observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // 如果启用了标题隐藏,对新加载的内容应用 if (Config.HIDE_TITLES) { this.hideTitlesInNode(node); } } }); }); }); this.observer.observe(document.body, { childList: true, subtree: true }); } hideTitlesInNode(node) { if (!node || !node.querySelectorAll) return; // 查找并隐藏标题元素 const titleSelectors = [ '.title', '.footer .title', '[class*="title"]', 'a[data-v-a264b01a].title', 'span[data-v-51ec0135]' ]; titleSelectors.forEach(selector => { const elements = node.querySelectorAll(selector); elements.forEach(el => { if (el.textContent && el.textContent.trim()) { el.style.display = 'none'; el.style.visibility = 'hidden'; } }); }); } toggleTitleHiding() { const newState = !Config.HIDE_TITLES; Config.HIDE_TITLES = newState; this.applyTitleHiding(); // 更新设置面板中的复选框状态 const checkbox = document.getElementById('hide-titles-checkbox'); if (checkbox) { checkbox.checked = newState; } } } // 初始化 function init() { // 创建全局过滤器实例 window.xhsFilter = new PostFilter(); // 创建标题隐藏管理器 window.xhsTitleHider = new TitleHider(); // 注册菜单命令 GM_registerMenuCommand('设置过滤器', () => { SettingsUI.createPanel(); }); // 注册可视化菜单命令 GM_registerMenuCommand('📊 数据可视化', () => { VisualizationManager.createVisualizationPanel(); }); // 注册键盘快捷键 KeyboardShortcuts.init(); // 添加全局移除关键词方法 window.xhsFilter.removeKeyword = (keyword) => { Config.removeKeyword(keyword); // 刷新页面以重新过滤 location.reload(); }; console.log('[小红书过滤器] 已启动'); } // 启动脚本 init(); })();