浏览器文本高亮脚本
2026/8/16 6:49:26 网站建设 项目流程
一、使用效果

二、添加文本

添加文本 + 选择颜色,然后点击添加按钮,即可出现步骤一中的高亮效果。

tips:①支持批量导入和删除

②如果刷新不出来,按照刷新网页 >> 重启浏览器 >> 重启电脑的优先级进行

三、代码粘贴

把下述代码添加到油猴里面。

// ==UserScript== // @name 文本高亮工具 (自定义) // @namespace http://tampermonkey.net/ // @version 2.2 // @description 高亮页面中的自定义文本,支持批量导入、批量删除、拖拽图标、自动保存,修复文字可见性 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ─── 默认配置(空)───────────────────────────────────────── const DEFAULT_CONFIG = {}; // ─── 配置管理 ────────────────────────────────────────── function loadConfig() { const saved = GM_getValue('hlConfig', null); if (saved) { try { return JSON.parse(saved); } catch (e) { /* ignore */ } } return { ...DEFAULT_CONFIG }; } function saveConfig(config) { GM_setValue('hlConfig', JSON.stringify(config)); } function resetConfig() { saveConfig({ ...DEFAULT_CONFIG }); return { ...DEFAULT_CONFIG }; } let config = loadConfig(); // ─── 位置存储 ────────────────────────────────────────── function loadPosition() { const pos = GM_getValue('hlTogglePos', null); if (pos) { try { return JSON.parse(pos); } catch (e) { /* ignore */ } } return null; } function savePosition(left, top) { GM_setValue('hlTogglePos', JSON.stringify({ left, top })); } // ─── 样式 ────────────────────────────────────────────── GM_addStyle(` .hl-text { padding: 0 2px; border-radius: 2px; font-weight: 600; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); /* +++ 新增:强制白色文字 + 阴影,保证任何背景下可见 +++ */ color: #fff !important; text-shadow: 0 0 3px rgba(0,0,0,0.6), 0 0 1px rgba(0,0,0,0.3); } #hl-panel { position: fixed; bottom: 80px; right: 20px; width: 340px; max-height: 420px; background: #1e1e2f; color: #eee; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); padding: 16px 18px; z-index: 999999; display: none; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; overflow: auto; border: 1px solid #3a3a4a; transition: all 0.15s; } #hl-panel.open { display: block; } #hl-panel h3 { margin: 0 0 10px 0; font-size: 15px; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 8px; } #hl-panel h3 span { opacity: 0.5; font-weight: 400; font-size: 12px; } #hl-list { max-height: 210px; overflow-y: auto; margin-bottom: 10px; border-radius: 6px; background: #2a2a3a; padding: 4px 0; } .hl-item { display: flex; align-items: center; gap: 8px; padding: 5px 10px; border-bottom: 1px solid #33334a; font-size: 13px; } .hl-item:last-child { border-bottom: none; } .hl-item .hl-checkbox { flex-shrink: 0; accent-color: #ff9800; } .hl-item .hl-text-display { flex: 1; font-weight: 500; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hl-item .hl-color-preview { width: 28px; height: 22px; border-radius: 4px; border: 1px solid #555; flex-shrink: 0; cursor: pointer; } .hl-item .hl-del { background: none; border: none; color: #888; cursor: pointer; font-size: 16px; padding: 0 4px; line-height: 1; flex-shrink: 0; } .hl-item .hl-del:hover { color: #ff6b6b; } #hl-add-row { display: flex; gap: 6px; margin: 6px 0 10px 0; } #hl-add-row input[type="text"] { flex: 1; padding: 5px 8px; border-radius: 6px; border: 1px solid #444; background: #2a2a3a; color: #eee; font-size: 13px; outline: none; } #hl-add-row input[type="text"]:focus { border-color: #ff9800; } #hl-add-row input[type="color"] { width: 34px; height: 32px; border: none; padding: 0; border-radius: 6px; cursor: pointer; background: transparent; } #hl-add-row button { padding: 4px 14px; border-radius: 6px; border: none; background: #ff9800; color: #1e1e2f; font-weight: 600; cursor: pointer; font-size: 13px; } #hl-add-row button:hover { background: #ffb74d; } #hl-actions { display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid #33334a; padding-top: 10px; flex-wrap: wrap; } #hl-actions button { padding: 4px 14px; border-radius: 6px; border: 1px solid #444; background: transparent; color: #ccc; cursor: pointer; font-size: 12px; } #hl-actions button:hover { background: #33334a; } #hl-actions .hl-delete-selected { color: #ff6b6b; border-color: #553333; } #hl-actions .hl-delete-selected:hover { background: #442222; } #hl-actions .hl-close { color: #aaa; } #hl-actions .hl-batch { color: #7ec8e3; border-color: #2a4a5a; } #hl-actions .hl-batch:hover { background: #1a3a4a; } #hl-toggle { position: fixed; bottom: 20px; right: 20px; z-index: 999998; width: 44px; height: 44px; border-radius: 50%; background: #1e1e2f; border: 1px solid #3a3a4a; color: #ff9800; font-size: 22px; cursor: grab; box-shadow: 0 4px 16px rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; transition: transform 0.15s; user-select: none; touch-action: none; } #hl-toggle:active { cursor: grabbing; } #hl-toggle:hover { transform: scale(1.08); } #hl-toggle .badge { position: absolute; top: -4px; right: -4px; background: #ff9800; color: #1e1e2f; font-size: 9px; font-weight: 700; border-radius: 50%; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; } .hl-empty { color: #666; text-align: center; padding: 16px 0; font-style: italic; } #hl-list::-webkit-scrollbar { width: 4px; } #hl-list::-webkit-scrollbar-track { background: #222; border-radius: 4px; } #hl-list::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } /* 批量导入模态框 */ #hl-batch-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 1000000; align-items: center; justify-content: center; } #hl-batch-modal.open { display: flex; } #hl-batch-modal .modal-box { background: #1e1e2f; color: #eee; padding: 20px 24px; border-radius: 12px; width: 440px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; border: 1px solid #3a3a4a; box-shadow: 0 8px 32px rgba(0,0,0,0.8); } #hl-batch-modal .modal-box h4 { margin: 0 0 8px 0; font-size: 15px; } #hl-batch-modal .modal-box p { margin: 4px 0 10px 0; opacity: 0.6; font-size: 12px; } #hl-batch-modal textarea { flex: 1; min-height: 200px; background: #2a2a3a; border: 1px solid #444; border-radius: 6px; color: #eee; padding: 8px; font-size: 13px; font-family: monospace; resize: vertical; outline: none; } #hl-batch-modal textarea:focus { border-color: #ff9800; } #hl-batch-modal .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; } #hl-batch-modal .modal-actions button { padding: 6px 18px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; } #hl-batch-modal .modal-actions .btn-import { background: #ff9800; color: #1e1e2f; font-weight: 600; } #hl-batch-modal .modal-actions .btn-import:hover { background: #ffb74d; } #hl-batch-modal .modal-actions .btn-cancel { background: transparent; color: #aaa; border: 1px solid #444; } #hl-batch-modal .modal-actions .btn-cancel:hover { background: #33334a; } `); // ─── 高亮引擎 ────────────────────────────────────────── let highlightObserver = null; let highlightTimeout = null; function getColorFor(text) { return config[text] || '#FF9800'; } function buildRegex() { const texts = Object.keys(config); if (texts.length === 0) return null; const sorted = texts.slice().sort((a, b) => b.length - a.length); const escaped = sorted.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); return new RegExp(`(${escaped.join('|')})`, 'g'); } let regex = buildRegex(); function clearHighlights(root) { const elements = root.querySelectorAll('span.hl-text'); elements.forEach(el => { const parent = el.parentNode; if (parent) { const text = document.createTextNode(el.textContent); parent.replaceChild(text, el); parent.normalize(); } }); } function applyHighlights(root = document.body) { if (!regex) return; const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentNode; if (!parent) return NodeFilter.FILTER_REJECT; const tag = parent.tagName; if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'].includes(tag)) { return NodeFilter.FILTER_REJECT; } if (parent.closest && parent.closest('.hl-text')) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); const nodesToProcess = []; let node; while ((node = walker.nextNode())) { nodesToProcess.push(node); } for (const textNode of nodesToProcess) { const text = textNode.textContent; if (!text || !regex.test(text)) continue; regex.lastIndex = 0; const fragment = document.createDocumentFragment(); let lastIndex = 0; let match; const localRegex = buildRegex(); if (!localRegex) continue; localRegex.lastIndex = 0; while ((match = localRegex.exec(text)) !== null) { const matchText = match[0]; const matchIndex = match.index; const color = getColorFor(matchText); if (matchIndex > lastIndex) { fragment.appendChild(document.createTextNode(text.substring(lastIndex, matchIndex))); } const span = document.createElement('span'); span.className = 'hl-text'; span.textContent = matchText; span.style.backgroundColor = color; fragment.appendChild(span); lastIndex = matchIndex + matchText.length; } if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.substring(lastIndex))); } if (fragment.childNodes.length > 0) { const parent = textNode.parentNode; if (parent) { parent.replaceChild(fragment, textNode); } } } } function refreshHighlights() { regex = buildRegex(); clearHighlights(document.body); if (regex) { applyHighlights(document.body); } } // ─── MutationObserver ────────────────────────────────── function startObserver() { if (highlightObserver) highlightObserver.disconnect(); highlightObserver = new MutationObserver(() => { if (highlightTimeout) clearTimeout(highlightTimeout); highlightTimeout = setTimeout(() => { refreshHighlights(); highlightTimeout = null; }, 200); }); highlightObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); } // ─── UI 面板 ────────────────────────────────────────── function renderList() { const container = document.getElementById('hl-list'); if (!container) return; const keys = Object.keys(config); if (keys.length === 0) { container.innerHTML = '<div class="hl-empty">暂无高亮规则</div>'; return; } let html = ''; for (const text of keys) { const color = config[text]; html += ` <div class="hl-item" data-text="${text.replace(/"/g, '&quot;')}"> <input type="checkbox" class="hl-checkbox" data-text="${text.replace(/"/g, '&quot;')}"> <span class="hl-text-display">${text}</span> <input type="color" class="hl-color-preview" value="${color}" data-text="${text.replace(/"/g, '&quot;')}"> <button class="hl-del" data-text="${text.replace(/"/g, '&quot;')}">✕</button> </div> `; } container.innerHTML = html; container.querySelectorAll('.hl-color-preview').forEach(input => { input.addEventListener('input', function(e) { const text = this.dataset.text; const newColor = this.value; config[text] = newColor; saveConfig(config); refreshHighlights(); const preview = this.closest('.hl-item').querySelector('.hl-color-preview'); if (preview) preview.style.background = newColor; }); input.style.background = input.value; }); container.querySelectorAll('.hl-del').forEach(btn => { btn.addEventListener('click', function() { const text = this.dataset.text; delete config[text]; saveConfig(config); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); }); }); } function updateBadge() { const badge = document.querySelector('#hl-toggle .badge'); if (badge) { const count = Object.keys(config).length; badge.textContent = count > 99 ? '99+' : count; badge.style.display = count > 0 ? 'flex' : 'none'; } } function updateTitleCount() { const span = document.querySelector('#hl-panel h3 span'); if (span) span.textContent = `${Object.keys(config).length} 个规则`; } // ─── 批量删除 ────────────────────────────────────────── function deleteSelected() { const checkboxes = document.querySelectorAll('.hl-checkbox:checked'); if (checkboxes.length === 0) { alert('请至少勾选一条规则。'); return; } const texts = Array.from(checkboxes).map(cb => cb.dataset.text); if (!confirm(`确定要删除选中的 ${texts.length} 条规则吗?`)) return; for (const text of texts) { delete config[text]; } saveConfig(config); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); } // ─── 批量导入 ────────────────────────────────────────── function showBatchModal() { const modal = document.getElementById('hl-batch-modal'); if (!modal) return; modal.classList.add('open'); const textarea = modal.querySelector('textarea'); if (textarea) { textarea.value = ''; textarea.focus(); } } function closeBatchModal() { const modal = document.getElementById('hl-batch-modal'); if (modal) modal.classList.remove('open'); } function importBatch() { const textarea = document.getElementById('hl-batch-textarea'); if (!textarea) return; const raw = textarea.value; const lines = raw.split(/\r?\n/).filter(line => line.trim() !== ''); if (lines.length === 0) { alert('请至少输入一条规则。'); return; } let added = 0; let skipped = 0; const newConfig = { ...config }; for (const line of lines) { let text, color; if (line.includes(',')) { const parts = line.split(',').map(s => s.trim()); text = parts[0]; color = parts[1] || '#FF9800'; } else if (line.includes('#')) { const idx = line.lastIndexOf('#'); text = line.substring(0, idx).trim(); let c = line.substring(idx).trim(); if (/^#[0-9a-fA-F]{6}$/.test(c)) { color = c; } else { color = '#FF9800'; text = line.trim(); } } else { text = line.trim(); color = '#FF9800'; } if (!text) continue; if (newConfig[text] !== undefined) { skipped++; } newConfig[text] = color; added++; } if (added === 0) { alert('没有有效的规则被添加。'); return; } config = newConfig; saveConfig(config); regex = buildRegex(); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); closeBatchModal(); alert(`成功导入 ${added} 条规则${skipped > 0 ? `,其中 ${skipped} 条已存在并被覆盖` : ''}`); } // ─── 拖拽功能 ────────────────────────────────────────── function setupDraggable() { const toggle = document.getElementById('hl-toggle'); if (!toggle) return; const pos = loadPosition(); if (pos) { toggle.style.left = pos.left + 'px'; toggle.style.top = pos.top + 'px'; toggle.style.bottom = 'auto'; toggle.style.right = 'auto'; } let isDragging = false; let startX, startY, startLeft, startTop; const onStart = (e) => { e.preventDefault(); if (e.type === 'mousedown' && e.button !== 0) return; isDragging = false; const rect = toggle.getBoundingClientRect(); startX = e.clientX || e.touches[0].clientX; startY = e.clientY || e.touches[0].clientY; let left = parseFloat(toggle.style.left); let top = parseFloat(toggle.style.top); if (isNaN(left) || isNaN(top)) { left = rect.left; top = rect.top; toggle.style.left = left + 'px'; toggle.style.top = top + 'px'; toggle.style.bottom = 'auto'; toggle.style.right = 'auto'; } startLeft = left; startTop = top; toggle.style.cursor = 'grabbing'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onEnd, { passive: false }); }; const onMove = (e) => { e.preventDefault(); const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; const dx = clientX - startX; const dy = clientY - startY; let newLeft = startLeft + dx; let newTop = startTop + dy; const rect = toggle.getBoundingClientRect(); const w = rect.width || 44; const h = rect.height || 44; const maxX = window.innerWidth - w; const maxY = window.innerHeight - h; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); toggle.style.left = newLeft + 'px'; toggle.style.top = newTop + 'px'; isDragging = true; }; const onEnd = (e) => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); toggle.style.cursor = 'grab'; const left = parseFloat(toggle.style.left); const top = parseFloat(toggle.style.top); if (!isNaN(left) && !isNaN(top)) { savePosition(left, top); } isDragging = false; }; toggle.addEventListener('mousedown', onStart); toggle.addEventListener('touchstart', onStart, { passive: false }); let clickTimer = null; toggle.addEventListener('click', function(e) { if (isDragging) return; if (clickTimer) clearTimeout(clickTimer); clickTimer = setTimeout(() => { const panel = document.getElementById('hl-panel'); if (panel) { panel.classList.toggle('open'); if (panel.classList.contains('open')) { renderList(); } } clickTimer = null; }, 10); }); } // ─── 构建UI ────────────────────────────────────────── function buildUI() { const toggle = document.createElement('div'); toggle.id = 'hl-toggle'; toggle.innerHTML = ` ⚙ <span class="badge">${Object.keys(config).length}</span> `; document.body.appendChild(toggle); const panel = document.createElement('div'); panel.id = 'hl-panel'; panel.innerHTML = ` <h3>🔍 文本高亮 <span>${Object.keys(config).length} 个规则</span></h3> <div id="hl-list"></div> <div id="hl-add-row"> <input type="text" id="hl-add-text" placeholder="输入文本..."> <input type="color" id="hl-add-color" value="#FF9800"> <button id="hl-add-btn">添加</button> </div> <div id="hl-actions"> <button class="hl-batch" id="hl-batch-btn">📋 批量导入</button> <button class="hl-delete-selected" id="hl-delete-selected-btn">🗑 删除选中</button> <button class="hl-close" id="hl-close-btn">关闭</button> </div> `; document.body.appendChild(panel); const modal = document.createElement('div'); modal.id = 'hl-batch-modal'; modal.innerHTML = ` <div class="modal-box"> <h4>批量导入规则</h4> <p>每行一条,格式:<strong>文本,颜色</strong> 或 <strong>文本 #颜色</strong>(颜色可省略,默认 #FF9800)</p> <textarea id="hl-batch-textarea" placeholder="示例:&#10;苹果, #FF0000&#10;香蕉 #FFD700&#10;橘子"></textarea> <div class="modal-actions"> <button class="btn-cancel" id="hl-batch-cancel">取消</button> <button class="btn-import" id="hl-batch-import">导入</button> </div> </div> `; document.body.appendChild(modal); document.getElementById('hl-close-btn').addEventListener('click', () => { panel.classList.remove('open'); }); document.getElementById('hl-add-btn').addEventListener('click', () => { const input = document.getElementById('hl-add-text'); const colorInput = document.getElementById('hl-add-color'); const text = input.value.trim(); const color = colorInput.value; if (!text) return; if (config[text]) { alert(`"${text}" 已存在!`); return; } config[text] = color; saveConfig(config); regex = buildRegex(); refreshHighlights(); renderList(); updateBadge(); updateTitleCount(); input.value = ''; }); document.getElementById('hl-add-text').addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('hl-add-btn').click(); }); document.getElementById('hl-batch-btn').addEventListener('click', showBatchModal); document.getElementById('hl-batch-cancel').addEventListener('click', closeBatchModal); document.getElementById('hl-batch-import').addEventListener('click', importBatch); modal.addEventListener('click', (e) => { if (e.target === modal) closeBatchModal(); }); document.getElementById('hl-delete-selected-btn').addEventListener('click', deleteSelected); document.addEventListener('click', (e) => { if (panel.classList.contains('open')) { const target = e.target; if (!panel.contains(target) && target !== toggle && !modal.contains(target)) { panel.classList.remove('open'); } } }); renderList(); updateBadge(); updateTitleCount(); setupDraggable(); } // ─── 初始化 ──────────────────────────────────────────── function init() { buildUI(); refreshHighlights(); startObserver(); setTimeout(refreshHighlights, 800); window.addEventListener('load', () => { setTimeout(refreshHighlights, 300); }); } if (document.readyState === 'complete' || document.readyState === 'interactive') { init(); } else { document.addEventListener('DOMContentLoaded', init); } })();

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询