// ==UserScript== // @name 牢高亮 // @namespace http://tampermonkey.net/ // @version 3.2 // @description 高亮 // @author Hanabi // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ===================== 全局常量与变量 ===================== let tooltip = null; let domChangeTimer = null; let isHighlightEnabled = true; let configPanel = null; let configMask = null; let HIGHLIGHT_GROUPS = []; let mutationObserver = null; let CURRENT_URL = window.location.href.toLowerCase(); // 当前页面URL(小写,方便匹配) let activeHighlightElement = null; // 记录当前悬浮的高亮元素 // ===================== 样式定义 ===================== GM_addStyle(` /* 高亮样式(仅添加背景色,不修改元素布局) */ .custom-highlight { background: var(--highlight-bg-color) !important; color: var(--highlight-text-color) !important; padding: 0 1px !important; border-radius: 2px !important; cursor: help !important; box-sizing: content-box !important; display: inline !important; } /* 气泡备注样式 - 新增skip-highlight类,排除高亮处理 */ .highlight-tooltip.skip-highlight { position: fixed; z-index: 999999; padding: 8px 12px; background: #333; color: #fff; font-size: 12px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); max-width: 280px; word-wrap: break-word; pointer-events: none; opacity: 0; transition: opacity 0.2s; /* 防止滚动时闪烁 */ } .highlight-tooltip.skip-highlight::before { content: ''; position: absolute; bottom: -6px; left: 10px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #333; } /* 禁用状态样式 */ body.highlight-disabled .custom-highlight { background: transparent !important; color: inherit !important; cursor: default !important; } body.highlight-disabled .highlight-tooltip { display: none !important; } /* 配置面板样式 */ .highlight-config-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 99999999; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); padding: 20px; width: 80%; max-width: 700px; max-height: 80vh; overflow-y: auto; display: none; } .highlight-config-panel.show { display: block; } .config-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; } .config-panel-title { font-size: 18px; font-weight: 600; color: #333; } .config-header-actions { display: flex; gap: 8px; } .import-export-btn { padding: 4px 8px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; background: #607d8b; color: #fff; } .import-export-btn:hover { background: #506978; } .config-close-btn { background: #f5f5f5; border: none; border-radius: 4px; width: 28px; height: 28px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; } .config-close-btn:hover { background: #e0e0e0; } .group-item { border: 1px solid #e0e0e0; border-radius: 6px; padding: 16px; margin-bottom: 16px; background: #fafafa; } .group-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; } .group-info { flex: 1; } .group-name-input { width: 200px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 4px; } .group-remark-input { width: 300px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; color: #666; margin-bottom: 4px; } .color-group { display: flex; align-items: center; gap: 8px; margin-top: 4px; } .group-bgcolor-input { width: 60px; height: 28px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .group-textcolor-input { width: 60px; height: 28px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .group-actions { display: flex; gap: 4px; } .group-delete-btn { padding: 4px 8px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; background: #f44336; color: #fff; } .group-delete-btn:hover { background: #d32f2f; } .config-item { margin-bottom: 16px; } .config-label { display: block; margin-bottom: 8px; font-size: 14px; color: #333; font-weight: 500; } .highlight-textarea { width: 100%; height: 100px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; resize: vertical; line-height: 1.5; } /* 白名单输入框样式 */ .whitelist-textarea { width: 100%; height: 80px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; resize: vertical; line-height: 1.5; margin-top: 8px; } .config-tip { font-size: 12px; color: #999; margin-top: 4px; } .whitelist-tip { font-size: 11px; color: #666; margin-top: 4px; line-height: 1.4; } .add-group-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; background: #2196f3; color: #fff; margin: 8px 0; } .add-group-btn:hover { background: #1976d2; } .config-save-btn { display: block; width: 100%; padding: 8px 0; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; background: #4caf50; color: #fff; margin-top: 16px; } .config-save-btn:hover { background: #43a047; } .config-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 99999998; display: none; } .config-mask.show { display: block; } .empty-tip { text-align: center; color: #999; padding: 40px 0; font-size: 14px; } #import-config-input { display: none; } `); // ===================== 核心工具函数:URL白名单匹配 ===================== /** * 判断当前URL是否在分组的白名单中(模糊匹配) * @param {Array} whitelistUrls - 分组的白名单网址列表 * @returns {Boolean} 是否匹配 */ function isUrlInWhitelist(whitelistUrls) { if (!Array.isArray(whitelistUrls) || whitelistUrls.length === 0) return false; return whitelistUrls.some(url => { const cleanUrl = url.trim().toLowerCase(); if (!cleanUrl) return false; // 模糊匹配:当前URL包含白名单中的网址片段即判定为匹配 return CURRENT_URL.includes(cleanUrl); }); } // ===================== 核心:读取本地配置 ===================== function loadLocalConfig() { try { const savedGroups = GM_getValue('highlightGroups', []); const savedEnabled = GM_getValue('highlightEnabled', true); // 兼容旧配置,补充字体颜色、白名单字段 HIGHLIGHT_GROUPS = Array.isArray(savedGroups) ? savedGroups.map(group => ({ ...group, textColor: group.textColor || "#000000", // 默认字体色为黑色 whitelistUrls: Array.isArray(group.whitelistUrls) ? group.whitelistUrls : [] // 默认空白名单 })) : []; isHighlightEnabled = typeof savedEnabled === 'boolean' ? savedEnabled : true; document.body.classList.toggle('highlight-disabled', !isHighlightEnabled); console.log('✅ 配置读取成功', { groups: HIGHLIGHT_GROUPS.length, enabled: isHighlightEnabled, currentUrl: CURRENT_URL }); } catch (e) { console.error('❌ 读取配置失败', e); HIGHLIGHT_GROUPS = []; isHighlightEnabled = true; } } // ===================== URL变化监听与处理 ===================== /** * URL变化后重新初始化高亮 */ function handleUrlChange() { // 更新当前URL CURRENT_URL = window.location.href.toLowerCase(); console.log('🔄 URL已变化,重新适配高亮规则:', CURRENT_URL); // 重新加载配置(确保配置是最新的) loadLocalConfig(); // 清除旧高亮 clearAllHighlight(); // 重新初始化高亮(自动判断是否开启) if (isHighlightEnabled && HIGHLIGHT_GROUPS.length > 0) { initHighlight(); } } /** * 监听所有URL变化场景(包括SPA路由) */ function setupUrlChangeListeners() { // 1. 监听浏览器前进/后退 window.addEventListener('popstate', handleUrlChange); // 2. 监听hash变化 window.addEventListener('hashchange', handleUrlChange); // 3. 重写pushState/replaceState(适配SPA应用) const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); handleUrlChange(); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); handleUrlChange(); }; console.log('✅ URL变化监听已启用'); } // ===================== 配置导入导出 ===================== function exportConfig() { const exportData = { version: '3.1', createTime: new Date().toISOString(), highlightEnabled: isHighlightEnabled, highlightGroups: HIGHLIGHT_GROUPS }; const jsonStr = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `高亮配置_${new Date().getTime()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('配置导出成功!'); } function importConfig(file) { if (!file || (!file.type.includes('json') && !file.name.endsWith('.json'))) { alert('请选择JSON格式的配置文件!'); return; } const reader = new FileReader(); reader.onload = function(e) { try { const importData = JSON.parse(e.target.result); if (!importData.highlightGroups || !Array.isArray(importData.highlightGroups)) { throw new Error('缺少高亮分组数据'); } // 兼容导入的配置,补充字体颜色、白名单字段 HIGHLIGHT_GROUPS = importData.highlightGroups.map(group => ({ ...group, textColor: group.textColor || "#000000", color: group.color || "#ff9800", whitelistUrls: Array.isArray(group.whitelistUrls) ? group.whitelistUrls : [] })).filter(group => { if (!group.groupName || !Array.isArray(group.words)) return false; return true; }); if (typeof importData.highlightEnabled === 'boolean') { isHighlightEnabled = importData.highlightEnabled; GM_setValue('highlightEnabled', isHighlightEnabled); document.body.classList.toggle('highlight-disabled', !isHighlightEnabled); } GM_setValue('highlightGroups', HIGHLIGHT_GROUPS); renderConfigPanel(); clearAllHighlight(); initHighlight(true); alert('配置导入成功!'); } catch (error) { alert(`导入失败:${error.message}`); console.error('导入错误:', error); } }; reader.readAsText(file); } function triggerImport() { let importInput = document.getElementById('import-config-input'); if (!importInput) { importInput = document.createElement('input'); importInput.id = 'import-config-input'; importInput.type = 'file'; importInput.accept = '.json'; document.body.appendChild(importInput); importInput.addEventListener('change', function(e) { if (this.files.length > 0) { importConfig(this.files[0]); this.value = ''; } }); } importInput.click(); } // ===================== 配置面板 ===================== function initConfigPanelEvents() { const closeBtn = configPanel.querySelector('.config-close-btn'); if (closeBtn) { closeBtn.removeEventListener('click', hideConfigPanel); closeBtn.addEventListener('click', hideConfigPanel); } const exportBtn = configPanel.querySelector('.export-btn'); if (exportBtn) { exportBtn.removeEventListener('click', exportConfig); exportBtn.addEventListener('click', exportConfig); } const importBtn = configPanel.querySelector('.import-btn'); if (importBtn) { importBtn.removeEventListener('click', triggerImport); importBtn.addEventListener('click', triggerImport); } configPanel.querySelectorAll('.add-group-btn').forEach(btn => { btn.removeEventListener('click', addGroup); btn.addEventListener('click', addGroup); }); configPanel.querySelectorAll('.group-delete-btn').forEach((btn, index) => { btn.removeEventListener('click', () => deleteGroup(index)); btn.addEventListener('click', () => deleteGroup(index)); }); const saveBtn = configPanel.querySelector('.config-save-btn'); if (saveBtn) { saveBtn.removeEventListener('click', saveConfig); saveBtn.addEventListener('click', saveConfig); } } function createConfigPanel() { if (configPanel && configMask) return; // 创建遮罩层 configMask = document.createElement('div'); configMask.className = 'config-mask'; configMask.addEventListener('click', hideConfigPanel); document.body.appendChild(configMask); // 创建配置面板 configPanel = document.createElement('div'); configPanel.className = 'highlight-config-panel'; document.body.appendChild(configPanel); renderConfigPanel(); } function renderConfigPanel() { if (HIGHLIGHT_GROUPS.length === 0) { configPanel.innerHTML = `
分组高亮配置
暂无高亮分组,请点击下方按钮添加
`; } else { configPanel.innerHTML = `
分组高亮配置
${HIGHLIGHT_GROUPS.map((group, groupIndex) => `


提示:词汇之间用单个空格分隔,空行和多个连续空格会自动忽略
提示:每行输入一个网址/域名片段,当前页面URL包含该片段时,此分组词汇不高亮;留空则无豁免
`).join('')}
`; } initConfigPanelEvents(); } function showConfigPanel() { createConfigPanel(); configMask.classList.add('show'); configPanel.classList.add('show'); } function hideConfigPanel() { if (configMask) configMask.classList.remove('show'); if (configPanel) configPanel.classList.remove('show'); } function addGroup() { HIGHLIGHT_GROUPS.push({ groupName: "新分组", groupRemark: "", color: "#ff9800", // 默认背景色 textColor: "#000000", // 默认字体色 whitelistUrls: [], // 默认空白名单 words: [] }); renderConfigPanel(); } function deleteGroup(index) { if (HIGHLIGHT_GROUPS.length <= 1) { alert('至少保留一个分组!'); return; } HIGHLIGHT_GROUPS.splice(index, 1); renderConfigPanel(); } function saveConfig() { const newGroups = []; configPanel.querySelectorAll('.group-item').forEach(item => { const groupName = item.querySelector('.group-name-input').value.trim() || "未命名分组"; const groupRemark = item.querySelector('.group-remark-input').value.trim(); const groupBgColor = item.querySelector('.group-bgcolor-input').value; const groupTextColor = item.querySelector('.group-textcolor-input').value; const wordsTextarea = item.querySelector('.highlight-textarea'); const whitelistTextarea = item.querySelector('.whitelist-textarea'); // 处理词汇输入 const words = wordsTextarea.value .replace(/\s+/g, ' ') .trim() .split(' ') .filter(word => word !== '') .filter((word, index, arr) => arr.indexOf(word) === index); // 处理白名单网址(按行分割) const whitelistUrls = whitelistTextarea.value .trim() .split('\n') .map(url => url.trim()) .filter(url => url !== ''); if (words.length > 0) { newGroups.push({ groupName, groupRemark, color: groupBgColor, textColor: groupTextColor, whitelistUrls, words }); } }); // 保存配置 HIGHLIGHT_GROUPS = newGroups; GM_setValue('highlightGroups', HIGHLIGHT_GROUPS); // 清除旧高亮并重新生效 clearAllHighlight(); if (isHighlightEnabled) { initHighlight(true); } hideConfigPanel(); alert(`配置保存成功!共设置 ${newGroups.length} 个分组`); } // ===================== 辅助函数 ===================== function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function toggleHighlight() { isHighlightEnabled = !isHighlightEnabled; GM_setValue('highlightEnabled', isHighlightEnabled); document.body.classList.toggle('highlight-disabled', !isHighlightEnabled); if (isHighlightEnabled) { initHighlight(true); } else { clearAllHighlight(); } alert(`高亮功能已${isHighlightEnabled ? '开启' : '关闭'}!`); } // 核心:彻底清除高亮,完全恢复原有结构 function clearAllHighlight() { // 1. 移除所有高亮span,恢复原始文本节点 document.querySelectorAll('.custom-highlight').forEach(span => { const textNode = document.createTextNode(span.textContent); span.parentNode.replaceChild(textNode, span); }); // 2. 停止DOM监听 if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; } // 3. 移除滚动监听(包括气泡的滚动监听) window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', updateTooltipPosition); window.removeEventListener('resize', updateTooltipPosition); } // ===================== 气泡提示核心修复 ===================== // 创建气泡提示 - 新增skip-highlight类排除高亮 function createTooltip() { if (tooltip) return tooltip; tooltip = document.createElement('div'); tooltip.className = 'highlight-tooltip skip-highlight'; // 关键:添加skip-highlight类 document.body.appendChild(tooltip); // 添加滚动和窗口大小变化监听,实时更新气泡位置 window.addEventListener('scroll', updateTooltipPosition); window.addEventListener('resize', updateTooltipPosition); return tooltip; } // 实时更新气泡位置(防抖处理) let tooltipUpdateTimer = null; function updateTooltipPosition() { if (!tooltip || !activeHighlightElement || tooltip.style.opacity === '0') return; clearTimeout(tooltipUpdateTimer); tooltipUpdateTimer = setTimeout(() => { positionTooltip(activeHighlightElement, tooltip); }, 10); // 短延迟,避免滚动时频繁计算 } // 气泡位置计算(优化版:基于实时元素位置) function positionTooltip(target, tooltip) { if (!target || !tooltip) return; // 获取元素的实时视口位置 const rect = target.getBoundingClientRect(); // 基础位置:高亮词右侧偏上(基于视口,fixed定位无需加scroll偏移) let left = rect.left + 5; let top = rect.top - tooltip.offsetHeight - 5; // 边界处理:防止气泡超出视口 // 顶部边界 if (top < 10) top = rect.bottom + 5; // 左侧边界 if (left < 10) left = 10; // 右侧边界 if (left + tooltip.offsetWidth > window.innerWidth - 10) { left = rect.right - tooltip.offsetWidth - 5; } // 底部边界 if (top + tooltip.offsetHeight > window.innerHeight - 10) { top = rect.top - tooltip.offsetHeight - 5; } // 设置气泡位置(fixed定位,直接使用视口坐标) tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; } // ===================== 核心:文本高亮逻辑(重构) ===================== function highlightTextInElement(element) { // 跳过不需要处理的元素:新增skip-highlight类(气泡提示)、原有排除项 const skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'OBJECT', 'EMBED', 'INPUT', 'TEXTAREA', 'SELECT']; if ( skipTags.includes(element.tagName?.toUpperCase()) || element.classList.contains('custom-highlight') || element.classList.contains('skip-highlight') // 关键:排除气泡提示元素 ) { return; } // 遍历子节点 const childNodes = Array.from(element.childNodes); childNodes.forEach(node => { // 文本节点 - 处理高亮 if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); if (!text) return; // 收集所有需要高亮的词汇(过滤白名单分组) let allWords = []; HIGHLIGHT_GROUPS.forEach(group => { // 如果当前URL在该分组白名单中,跳过该分组 if (isUrlInWhitelist(group.whitelistUrls)) return; group.words.forEach(word => { if (word && word.trim()) { allWords.push({ word: word.trim(), bgColor: group.color, // 背景色 textColor: group.textColor, // 字体色 remark: group.groupRemark }); } }); }); if (allWords.length === 0) return; // 按词汇长度降序排序(避免短词匹配覆盖长词) allWords.sort((a, b) => b.word.length - a.word.length); // 构建正则表达式 const wordList = allWords.map(item => item.word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const regex = new RegExp(`(${wordList.join('|')})`, 'g'); // 没有匹配项则跳过 if (!regex.test(node.textContent)) return; regex.lastIndex = 0; // 创建文档片段 const frag = document.createDocumentFragment(); let lastIndex = 0; let match; // 匹配并替换 while ((match = regex.exec(node.textContent)) !== null) { const matchedWord = match[0]; const wordConfig = allWords.find(item => item.word === matchedWord); if (!wordConfig) continue; // 添加匹配前的文本 if (match.index > lastIndex) { frag.appendChild(document.createTextNode(node.textContent.slice(lastIndex, match.index))); } // 创建高亮span - 同时设置背景色和字体色 const span = document.createElement('span'); span.className = 'custom-highlight'; span.style.setProperty('--highlight-bg-color', wordConfig.bgColor); // 背景色 span.style.setProperty('--highlight-text-color', wordConfig.textColor); // 字体色 span.textContent = matchedWord; span.setAttribute('data-remark', wordConfig.remark || '无备注'); // 气泡事件(优化版:记录当前激活的元素) span.addEventListener('mouseenter', (e) => { if (!isHighlightEnabled) return; activeHighlightElement = span; // 记录当前悬浮的高亮元素 const tip = createTooltip(); tip.textContent = span.getAttribute('data-remark'); positionTooltip(span, tip); // 初始定位 tip.style.opacity = '1'; e.stopPropagation(); }); span.addEventListener('mouseleave', () => { activeHighlightElement = null; // 清空激活元素 if (tooltip) tooltip.style.opacity = '0'; }); frag.appendChild(span); lastIndex = regex.lastIndex; } // 添加剩余文本 if (lastIndex < node.textContent.length) { frag.appendChild(document.createTextNode(node.textContent.slice(lastIndex))); } // 替换原文本节点 if (frag.childNodes.length > 0) { node.parentNode.replaceChild(frag, node); } } // 元素节点 - 递归处理 else if (node.nodeType === Node.ELEMENT_NODE) { highlightTextInElement(node); } }); } // 高亮整个页面 function highlightAllText() { if (!isHighlightEnabled || HIGHLIGHT_GROUPS.length === 0) return; highlightTextInElement(document.body); } // 监听DOM变化 function observeDomChanges() { if (mutationObserver) return; mutationObserver = new MutationObserver((mutations) => { if (!isHighlightEnabled || HIGHLIGHT_GROUPS.length === 0) return; clearTimeout(domChangeTimer); domChangeTimer = setTimeout(() => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('skip-highlight')) { highlightTextInElement(node); } }); }); // 兜底处理 highlightAllText(); }, 50); }); mutationObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); } // 初始化高亮 function initHighlight() { if (!isHighlightEnabled || HIGHLIGHT_GROUPS.length === 0) return; // 先清除旧高亮 clearAllHighlight(); // 高亮所有文本 highlightAllText(); // 启动DOM监听 observeDomChanges(); // 滚动监听(高亮相关) window.removeEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll); } // 滚动处理(高亮相关) function handleScroll() { clearTimeout(domChangeTimer); domChangeTimer = setTimeout(() => { highlightAllText(); }, 50); } // ===================== 全局初始化 ===================== function init() { // 读取配置 loadLocalConfig(); // 初始化URL变化监听 setupUrlChangeListeners(); // 初始化高亮 if (isHighlightEnabled && HIGHLIGHT_GROUPS.length > 0) { initHighlight(); } } // ========== 关键修复:菜单注册 ========== // 1. 立即注册菜单(最外层执行,确保时机正确) try { // 注册配置菜单 GM_registerMenuCommand('📝 配置高亮分组', showConfigPanel); // 注册开关菜单 GM_registerMenuCommand('🔄 切换高亮开关', toggleHighlight); console.log('✅ 菜单注册成功'); } catch (e) { console.warn('⚠️ 菜单注册失败,请通过油猴插件的脚本菜单操作', e); } // 2. 页面加载完成后再次尝试注册(双重保险) if (document.readyState === 'complete') { init(); } else { window.addEventListener('load', init); } })();