Greasy Fork is available in English.
自动扫描、高亮关键词、跨页检测重复内容
// ==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 });
});
})();