Greasy Fork

Greasy Fork is available in English.

留言审核辅助工具WZ

自动扫描、高亮关键词、跨页检测重复内容

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         留言审核辅助工具WZ
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  自动扫描、高亮关键词、跨页检测重复内容
// @author       Furnace
// @match        https://sdxw-admin.iqilu.com/cq-admin/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ================= 核心配置区 =================
    // 结合你提供的 iView 结构定制的选择器
    const SELECTORS = {
        row: '.ivu-table-row',
        content: '.flowline-2',
        status: '.ivu-badge-status-text'
    };
    // ==============================================

    const defaultKeywords = "共产党、毛主席、法院、检察院、枉法、公安、派出所、劳动法、贪、集资、强拆、黑恶、黑社会、反动、淫、嫖、生殖、赌、毒品、黄赌毒、上帝、全能神、宗教、伊斯兰、基督、佛、回族、回民、纪委、省长信箱、上访、信访、城投、房改、军人、控告、黑物业、习近平、国补、国家补贴、烂尾、诈骗、换届、选举、黑物业、替百姓说话|驳回\n工资、农民工、欠款、欠薪、12345、供电、回迁、上房、交房、起诉、执行、人才补贴、基本农田、国土资源、盗采|仅管理可见\n已解决|通过";

    let commentHashes = JSON.parse(localStorage.getItem('audit_comment_hashes') || '{}');
    let scannedIds = JSON.parse(localStorage.getItem('audit_scanned_ids') || '{}'); // 新增:用于记录留言的唯一身份证
    let keywordsConfig = localStorage.getItem('audit_keywords') || defaultKeywords;
    let totalScanned = parseInt(localStorage.getItem('audit_total_scanned') || '0');

    function getKeywordsList() {
        let list = [];
        // 按行拆分
        keywordsConfig.split('\n').forEach(line => {
            const parts = line.split('|');
            const wordsPart = parts[0];
            const action = parts[1]?.trim() || '需审核'; // 如果没写操作,默认提示"需审核"

            if (wordsPart) {
                // 支持使用中文逗号、英文逗号或顿号来分隔多个关键词
                const words = wordsPart.split(/[,,、]/);
                words.forEach(w => {
                    if (w.trim()) {
                        list.push({ word: w.trim(), action: action });
                    }
                });
            }
        });
        return list;
    }

    function simpleHash(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = ((hash << 5) - hash) + str.charCodeAt(i);
            hash = hash & hash;
        }
        return hash.toString();
    }

    function createFloatingPanel() {
        if (document.getElementById('audit-panel')) return;

        const panel = document.createElement('div');
        panel.id = 'audit-panel';
        panel.style.cssText = `
            position: fixed; bottom: 20px; left: 20px; width: 240px;
            background: #fff; border: 1px solid #dcdee2; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            padding: 15px; border-radius: 8px; z-index: 9999; font-size: 14px; color: #515a6e;
            font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
        `;

        panel.innerHTML = `
            <div style="font-weight: bold; margin-bottom: 10px; font-size: 16px;">审核助手</div>
            <div style="margin-bottom: 10px;">今日扫描:<span id="audit-count" style="color: #2d8cf0; font-weight: bold; font-size: 16px;">${totalScanned}</span> 条</div>
            <button id="audit-btn-reset" style="margin-right: 5px; padding: 5px 10px; cursor: pointer; border: 1px solid #dcdee2; background: #fff; border-radius: 4px;">清空记录</button>
            <button id="audit-btn-settings" style="padding: 5px 10px; cursor: pointer; border: 1px solid #dcdee2; background: #fff; border-radius: 4px;">设置词库</button>

            <div id="audit-settings-area" style="display: none; margin-top: 10px;">
                <textarea id="audit-keyword-input" style="width: 100%; height: 120px; margin-bottom: 8px; padding: 5px; border: 1px solid #dcdee2; border-radius: 4px;" placeholder="关键词|操作建议 (每行一个)"></textarea>
                <button id="audit-btn-save" style="padding: 6px 12px; cursor: pointer; background: #19be6b; color: white; border: none; border-radius: 4px; width: 100%;">保存并刷新</button>
            </div>
        `;
        document.body.appendChild(panel);

        document.getElementById('audit-btn-reset').addEventListener('click', () => {
            if (confirm('确定要清空今天的扫描记录吗?(建议每次审核开始前点击)')) {
                localStorage.removeItem('audit_comment_hashes');
                localStorage.removeItem('audit_scanned_ids'); // 新增:清空已扫描的唯一ID库
                localStorage.removeItem('audit_total_scanned');
                location.reload();
            }
        });

        document.getElementById('audit-btn-settings').addEventListener('click', () => {
            const area = document.getElementById('audit-settings-area');
            const input = document.getElementById('audit-keyword-input');
            area.style.display = area.style.display === 'none' ? 'block' : 'none';
            input.value = localStorage.getItem('audit_keywords') || defaultKeywords;
        });

        document.getElementById('audit-btn-save').addEventListener('click', () => {
            localStorage.setItem('audit_keywords', document.getElementById('audit-keyword-input').value);
            location.reload();
        });
    }

    function processComments() {
        const rows = document.querySelectorAll(SELECTORS.row);
        if (rows.length === 0) {
            setTimeout(processComments, 1000);
            return;
        }

        const keywordsList = getKeywordsList();
        let newScannedCount = 0;

        rows.forEach(row => {
            // 防止单次页面生命周期内的重复遍历
            if (row.dataset.audited) return;
            row.dataset.audited = "true";

            const statusEl = row.querySelector(SELECTORS.status);
            const contentEl = row.querySelector(SELECTORS.content);
            if (!contentEl) return;

            const titleEl = contentEl.previousElementSibling;
            const isProcessed = statusEl && (statusEl.innerText.includes('已审核') || statusEl.innerText.includes('通过') || statusEl.innerText.includes('驳回'));
            const textContent = contentEl.innerText.trim();
            if (!textContent) return;

            // ===== 核心修改:利用正则直接抓取时间戳,生成双哈希 =====
            // 正则匹配类似 "2026-02-24 09:42:15" 的标准时间格式
            const timeMatch = row.innerText.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
            const timeStr = timeMatch ? timeMatch[0] : '';
            // 唯一身份ID (时间+内容):用于防刷新误判
            const rowUniqueId = simpleHash(timeStr + textContent);
            // 内容查重ID (纯内容):用于检测是不是有人发了同样的话
            const textHash = simpleHash(textContent);
            // ========================================================

            const isAlreadyScanned = scannedIds[rowUniqueId]; // 检查这具体的一条是否记录过

            // 处理“已审核”状态的豁免
            if (isProcessed) {
                if (!commentHashes[textHash] || commentHashes[textHash].status !== 'processed') {
                    commentHashes[textHash] = { count: 1, status: 'processed' };
                }
                // 即使是已处理的,也把它记入已扫描ID,防止重复计算
                if (!isAlreadyScanned) {
                    scannedIds[rowUniqueId] = true;
                    newScannedCount++;
                }
                return;
            }

            // ===== 计数与查重逻辑 (仅针对之前没见过的“新ID”) =====
            if (!isAlreadyScanned) {
                scannedIds[rowUniqueId] = true; // 登记该 ID
                newScannedCount++; // 新增扫描量
                // 检查内容是否重复
                if (commentHashes[textHash]) {
                    if (commentHashes[textHash].status !== 'processed') {
                        commentHashes[textHash].count += 1;
                    }
                } else {
                    commentHashes[textHash] = { count: 1, status: 'pending' };
                }
            }

            // ===== UI 渲染:无论是否新扫描,只要库里显示它重复了,就高亮 =====
            if (commentHashes[textHash] && commentHashes[textHash].count > 1 && commentHashes[textHash].status !== 'processed') {
                const cells = row.querySelectorAll('td');
                cells.forEach(td => { td.style.backgroundColor = '#fff3cd'; });
                if(cells.length === 0) row.style.backgroundColor = '#fff3cd';
                if (titleEl) addTag(titleEl, `重复 (共${commentHashes[textHash].count}次)`, '#856404', '#ffe8a1');
            }

            // ===== 提取标题文本,用于关键词匹配 =====
            const titleText = titleEl ? titleEl.innerText.trim() : '';
            const fullSearchText = titleText + " " + textContent; // 将标题和正文合并作为搜索范围

            // ===== 关键词匹配渲染 =====
            let matchedKeyword = null;
            for (let k of keywordsList) {
                if (fullSearchText.includes(k.word)) { // 改为在合并后的文本中查找
                    matchedKeyword = k;
                    break;
                }
            }

            if (matchedKeyword) {
                const cells = row.querySelectorAll('td');
                cells.forEach(td => { td.style.backgroundColor = '#f8d7da'; });
                if(cells.length === 0) row.style.backgroundColor = '#f8d7da';
                let color = matchedKeyword.action === '通过' ? '#19be6b' : (matchedKeyword.action === '驳回' ? '#ed4014' : '#ff9900');
                if (titleEl) addTag(titleEl, `建议:${matchedKeyword.action} (${matchedKeyword.word})`, '#fff', color);
            }
        });

        // 批量保存数据
        if (newScannedCount > 0) {
            localStorage.setItem('audit_comment_hashes', JSON.stringify(commentHashes));
            localStorage.setItem('audit_scanned_ids', JSON.stringify(scannedIds)); // 存入唯一ID记录
            totalScanned += newScannedCount;
            localStorage.setItem('audit_total_scanned', totalScanned.toString());
            const countEl = document.getElementById('audit-count');
            if(countEl) countEl.innerText = totalScanned;
        }
    }

    function addTag(element, text, color, bgColor) {
        const tag = document.createElement('span');
        tag.innerText = text;
        tag.style.cssText = `
            margin-left: 8px; padding: 2px 6px; font-size: 12px;
            border-radius: 4px; color: ${color}; background-color: ${bgColor};
            font-weight: normal; vertical-align: middle;
        `;
        // 追加在标题后面
        element.appendChild(tag);
    }

    // iView 页面可能因为路由切换或接口请求导致 DOM 延迟渲染,这里使用 MutationObserver 监听列表变化
    window.addEventListener('load', () => {
        createFloatingPanel();
        processComments();

        // 监听 DOM 变化,适配传统的 Ajax 翻页不刷新页面的情况
        const observer = new MutationObserver((mutations) => {
            let shouldProcess = false;
            for (let mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    shouldProcess = true;
                    break;
                }
            }
            if (shouldProcess) {
                // 防抖处理,避免频繁触发
                clearTimeout(window.processTimer);
                window.processTimer = setTimeout(processComments, 500);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    });

})();