// ==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 = `