知乎屏蔽词管理器

页面内管理屏蔽词 + 实时计数 + 可查看屏蔽列表,永不卡顿,不用动脚本就能增删词语

目前為 2025-07-18 提交的版本,檢視 最新版本

// ==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 });
    })();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址