您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
页面内管理屏蔽词 + 导入/导出 + 累计/本次屏蔽统计 + 可查看屏蔽列表,性能飞起,无需改脚本操作
// ==UserScript== // @name 知乎屏蔽词管理器 // @namespace http://tampermonkey.net/ // @version 1.0.8 // @description 页面内管理屏蔽词 + 导入/导出 + 累计/本次屏蔽统计 + 可查看屏蔽列表,性能飞起,无需改脚本操作 // @author You // @match https://www.zhihu.com/* // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function() { 'use strict'; const ITEM_SEL = '.ContentItem'; const TITLE_SEL = '.ContentItem-title a'; let keywords = []; let sessionCount = 0; let totalCount = 0; const blockedItems = []; const seen = new WeakSet(); // 载入词库 & 累计数 async function loadConfig() { const storedList = await GM_getValue('BLOCK_KEYWORDS', []); keywords = Array.isArray(storedList) ? storedList : []; totalCount = Number(await GM_getValue('BLOCKED_TOTAL', 0)); } // 保存累计数 async function saveTotal(count) { await GM_setValue('BLOCKED_TOTAL', count); } async function saveKeywords(list) { await GM_setValue('BLOCK_KEYWORDS', list); keywords = list; resetAll(); } function resetAll() { sessionCount = 0; blockedItems.length = 0; document.querySelectorAll(ITEM_SEL).forEach(el => { el.style.display = ''; seen.delete(el); io.observe(el); }); updateButtons(); } // UI 按钮区 const ui = document.createElement('div'); Object.assign(ui.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: 10000, display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', fontFamily: 'sans-serif' }); document.body.appendChild(ui); // 累计 / 本次 屏蔽按钮 const countBtn = document.createElement('div'); Object.assign(countBtn.style, { background: 'rgba(0,0,0,0.6)', color: '#fff', padding: '6px 10px', borderRadius: '4px', cursor: 'pointer', userSelect: 'none', }); countBtn.title = '点击查看本次 & 历史屏蔽列表'; ui.appendChild(countBtn); countBtn.addEventListener('click', showBlockedList); // 管理按钮 const manageBtn = document.createElement('div'); Object.assign(manageBtn.style, { background: 'rgba(0,0,0,0.6)', color: '#fff', padding: '6px 10px', borderRadius: '4px', cursor: 'pointer', userSelect: 'none' }); manageBtn.textContent = '⚙️ 管理'; manageBtn.title = '点击管理屏蔽词'; ui.appendChild(manageBtn); manageBtn.addEventListener('click', showKeywordPanel); function updateButtons() { countBtn.textContent = `累计屏蔽 ${totalCount} 条 / 本次 ${sessionCount} 条`; } // 屏蔽逻辑 function tryHide(el) { if (seen.has(el)) return; seen.add(el); const a = el.querySelector(TITLE_SEL); if (!a) return; const txt = a.textContent.trim(); if (keywords.some(k => txt.includes(k))) { el.style.display = 'none'; sessionCount++; totalCount++; saveTotal(totalCount); blockedItems.push({ title: txt, href: a.href }); updateButtons(); } } // IntersectionObserver 性能方案 const io = new IntersectionObserver(entries => { entries.forEach(ent => { if (ent.isIntersecting) { tryHide(ent.target); io.unobserve(ent.target); } }); }, { rootMargin: '200px', threshold: 0.01 }); // 样式 const baseCSS = ` .tm-mask {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);z-index:9998;} .tm-dialog { position:fixed;top:50%;left:50%;transform:translate(-50%,-50%); background:#fff;padding:20px;border-radius:8px; box-shadow:0 2px 10px rgba(0,0,0,0.2); z-index:9999;max-width:80%;max-height:80%;overflow:auto; font-size:14px;font-family:sans-serif; } .tm-dialog h3 {margin-top:0;font-size:16px;} .tm-dialog textarea {width:100%;box-sizing:border-box;margin:10px 0;font-family:monospace;} .tm-dialog .btns {text-align:right;margin-top:10px;} .tm-dialog button {margin-left:8px;padding:6px 12px;border:none;border-radius:4px;cursor:pointer;} .tm-dialog input[type=file] {display:none;} `; function appendStyle() { if (document.getElementById('tm-base-style')) return; const style = document.createElement('style'); style.id = 'tm-base-style'; style.textContent = baseCSS; document.head.appendChild(style); } // 查看列表(本次+累计) function showBlockedList() { if (document.getElementById('tm-blocker-list')) return; const panel = document.createElement('div'); panel.id = 'tm-blocker-list'; panel.innerHTML = ` <div class="tm-mask"></div> <div class="tm-dialog"> <h3>累计屏蔽 ${totalCount} 条 / 本次 ${sessionCount} 条</h3> <ul style="list-style:none;padding:0;margin:10px 0;"> ${blockedItems.length ? blockedItems.map(item => `<li style="margin-bottom:6px;"> <a href="${item.href}" target="_blank" style="color:#337ab7;text-decoration:none;"> ${item.title} </a> </li>` ).join('') : '<li>本次暂无屏蔽记录</li>'} </ul> <div class="btns"> <button id="tm-close-list">关闭</button> </div> </div>`; document.body.appendChild(panel); appendStyle(); panel.querySelector('#tm-close-list').onclick = () => panel.remove(); } // 管理面板(导入/导出/保存/取消) function showKeywordPanel() { if (document.getElementById('tm-keyword-panel')) return; const panel = document.createElement('div'); panel.id = 'tm-keyword-panel'; panel.innerHTML = ` <div class="tm-mask"></div> <div class="tm-dialog"> <h3>🛠 屏蔽词管理</h3> <textarea rows="10" placeholder="一行一个词">${keywords.join('\n')}</textarea> <div class="btns"> <button data-act="import">导入</button> <button data-act="export">导出</button> <button data-act="save">保存</button> <button data-act="cancel">取消</button> </div> <input type="file" id="tm-import-file" accept=".json,.txt"> </div>`; document.body.appendChild(panel); appendStyle(); const textarea = panel.querySelector('textarea'); const fileInput = panel.querySelector('#tm-import-file'); panel.addEventListener('click', e => { const act = e.target.getAttribute('data-act'); if (!act) return; if (act === 'save') { const arr = textarea.value.trim().split('\n').map(s => s.trim()).filter(Boolean); saveKeywords([...new Set(arr)]); panel.remove(); } else if (act === 'cancel') { panel.remove(); } else if (act === 'export') { const blob = new Blob([JSON.stringify(keywords, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download= 'blocked_keywords.json'; a.click(); URL.revokeObjectURL(url); } else if (act === 'import') { fileInput.click(); } }); fileInput.addEventListener('change', e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = evt => { let arr; try { const obj = JSON.parse(evt.target.result); if (Array.isArray(obj)) arr = obj.map(String); } catch { arr = evt.target.result.split('\n').map(s => s.trim()).filter(Boolean); } if (arr) saveKeywords([...new Set(arr)]); panel.remove(); }; reader.readAsText(file); }); } // 启动 (async function init() { await loadConfig(); document.querySelectorAll(ITEM_SEL).forEach(el => io.observe(el)); updateButtons(); new MutationObserver(muts => { muts.forEach(m => { m.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.matches(ITEM_SEL)) io.observe(node); else node.querySelectorAll(ITEM_SEL).forEach(el => io.observe(el)); } }); }); }).observe(document.body, { childList: true, subtree: true }); })(); })();