// ==UserScript==
// @name 知乎屏蔽词管理器
// @namespace http://tampermonkey.net/
// @version 1.0.5
// @description 页面内管理屏蔽词 + 实时计数 + 可查看屏蔽列表,永不卡顿,不用动脚本就能增删词语
// @author You
// @match https://www.zhihu.com/*
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// —— 配置区 ——
const DEFAULT_KEYWORDS = ['男','女','父亲','小红书','为什么','评价','父母','生活费','母亲'];
const ITEM_SEL = '.ContentItem';
const TITLE_SEL = '.ContentItem-title a';
// —— 全局变量 ——
let keywords = [];
let hiddenCount = 0;
const blockedItems = [];
const seen = new WeakSet();
// —— 读写词库 ——
async function loadKeywords() {
const stored = await GM_getValue('BLOCK_KEYWORDS', DEFAULT_KEYWORDS);
keywords = Array.isArray(stored) ? stored : DEFAULT_KEYWORDS;
}
async function saveKeywords(list) {
await GM_setValue('BLOCK_KEYWORDS', list);
keywords = list;
resetAll();
}
// —— 重置状态 ——
function resetAll() {
hiddenCount = 0;
blockedItems.length = 0;
document.querySelectorAll(ITEM_SEL).forEach(el => {
el.style.display = '';
seen.delete(el);
io.observe(el);
});
updateCounter();
}
// —— UI 创建 ——
const uiContainer = document.createElement('div');
Object.assign(uiContainer.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 10000,
display: 'flex',
gap: '8px',
fontSize: '12px',
fontFamily: 'sans-serif',
});
document.body.appendChild(uiContainer);
const counterBtn = document.createElement('div');
Object.assign(counterBtn.style, {
background: 'rgba(0,0,0,0.6)',
color: '#fff',
padding: '6px 10px',
borderRadius: '4px',
cursor: 'pointer',
userSelect: 'none'
});
counterBtn.title = '点击查看屏蔽列表';
uiContainer.appendChild(counterBtn);
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 = '点击管理屏蔽词';
uiContainer.appendChild(manageBtn);
counterBtn.addEventListener('click', showBlockedList);
manageBtn.addEventListener('click', showKeywordPanel);
function updateCounter() {
counterBtn.textContent = `已屏蔽 ${hiddenCount} 条`;
}
// —— 屏蔽逻辑 ——
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';
hiddenCount++;
blockedItems.push({ title: txt, href: a.href });
updateCounter();
}
}
// —— 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;
}
`;
// —— 显示屏蔽列表 ——
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>已屏蔽 ${hiddenCount} 条</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="save">保存</button>
<button data-act="cancel">取消</button>
</div>
</div>
`;
document.body.appendChild(panel);
appendStyle();
panel.addEventListener('click', e => {
const act = e.target.getAttribute('data-act');
if (!act) return;
if (act === 'save') {
const raw = panel.querySelector('textarea').value.trim();
const arr = raw.split('\n').map(s => s.trim()).filter(Boolean);
saveKeywords([...new Set(arr)]);
}
panel.remove();
});
}
// —— 添加通用样式 ——
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);
}
// —— 初始化 ——
(async function init() {
await loadKeywords();
document.querySelectorAll(ITEM_SEL).forEach(el => io.observe(el));
updateCounter();
// 监听新增条目
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 });
})();
})();