// ==UserScript== // @name 网页划词高亮工具 // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description 提供网页划词高亮功能 // @author sunny43 // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== (function () { 'use strict'; const STYLE_PREFIX = 'sunny43-'; // 全局变量 let highlights = []; let currentPageUrl = window.location.href; let currentDomain = window.location.hostname; let settings = GM_getValue('highlight_settings', { colors: ['#ff909c', '#b89fff', '#74b4ff', '#70d382', '#ffcb7e'], activeColor: '#ff909c', minTextLength: 1, enableFuzzyMatch: true, maxContextDistance: 50, sidebarDescription: '高亮工具', sidebarWidth: 320, showFloatingButton: true }); let savedRange = null; // 保存选区范围 let ignoreNextClick = false; // 忽略下一次点击的标志 let menuDisplayTimer = null; // 菜单显示定时器 let menuOperationInProgress = false; // 添加菜单操作锁定 // 禁用列表 let disabledList = GM_getValue('disabled_list', { domains: [], urls: [] }); // 检查当前页面是否禁用高亮功能 let isHighlightDisabled = disabledList.domains.includes(currentDomain) || disabledList.urls.includes(currentPageUrl); let updateSidebarHighlights = null; GM_addStyle(` /* 高亮菜单样式 */ .${STYLE_PREFIX}highlight-menu { position: absolute; background: #333336; border: none; border-radius: 24px; box-shadow: 0 4px 16px rgba(0,0,0,0.3); padding: 10px 8px; z-index: 9999; display: flex; flex-direction: row; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #fff; opacity: 0; /* 初始隐藏 */ transition: opacity 0.2s ease-in; pointer-events: none; /* 隐藏时不响应事件 */ } .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}show { opacity: 1; pointer-events: auto; /* 显示时响应事件 */ } /* 菜单箭头样式 */ .${STYLE_PREFIX}highlight-menu::after { content: ''; position: absolute; bottom: -6px; left: var(--arrow-left, 50%); width: 12px; height: 6px; background-color: #333336; clip-path: polygon(0 0, 100% 0, 50% 100%); margin-left: -6px; } .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}arrow-top::after { top: -6px; bottom: auto; clip-path: polygon(0 100%, 100% 100%, 50% 0); } /* 颜色选择区域 */ .${STYLE_PREFIX}highlight-menu-colors { display: flex; flex-direction: row; align-items: center; margin: 0 2px; flex-wrap: nowrap; flex: 0 0 auto; } /* 颜色选择按钮 */ .${STYLE_PREFIX}highlight-menu-color { width: 22px; height: 22px; border-radius: 50%; margin: 0 3px; cursor: pointer; position: relative; display: flex; align-items: center; justify-content: center; transition: transform 0.15s ease; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12); flex-shrink: 0; } .${STYLE_PREFIX}highlight-menu-color:hover { transform: scale(1.12); } .${STYLE_PREFIX}highlight-menu-color.active::after { content: ""; width: 12px; height: 12px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23333336' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: contain; } /* 菜单按钮通用样式 */ .${STYLE_PREFIX}highlight-menu-action { height: 22px; margin: 0 2px; cursor: pointer; padding: 0 10px; border-radius: 12px; color: #fff; font-size: 13px; background: rgba(255,255,255,0.1); border: none; transition: all 0.15s ease; white-space: nowrap; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .${STYLE_PREFIX}highlight-menu-action:hover { background: rgba(255,255,255,0.2); } /* 删除按钮样式 */ .${STYLE_PREFIX}highlight-action-delete { color: #f0f0f0; font-weight: 500; position: relative; overflow: hidden; transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1); margin-left: 3px; } .${STYLE_PREFIX}highlight-action-delete:hover { background: rgba(255,82,82,0.12); color: #ff6b6b; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(255,82,82,0.25); } .${STYLE_PREFIX}highlight-action-delete:active { transform: translateY(0px); background: rgba(255,82,82,0.2); } /* 闪烁效果用于高亮跳转 */ @keyframes ${STYLE_PREFIX}highlightFlash { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } .${STYLE_PREFIX}highlight-flash { animation: ${STYLE_PREFIX}highlightFlash 0.5s ease 4; box-shadow: 0 0 0 3px rgba(255, 255, 0, 0.7) !important; position: relative; z-index: 10; } /* 浮动按钮样式 */ #${STYLE_PREFIX}floating-button { position: fixed; bottom: 20px; right: 20px; z-index: 10000; width: 33px; height: 33px; border: none; border-radius: 50%; cursor: pointer; background-color: #333336; /* 与菜单一致的背景 */ color: #fff; font-size: 16px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); transition: background-color 0.2s ease, transform 0.2s ease; display: flex; align-items: center; justify-content: center; } #${STYLE_PREFIX}floating-button:hover { background-color: #4a4a4a; transform: scale(1.05); } #${STYLE_PREFIX}floating-button:active { transform: scale(0.95); } /* 侧边栏样式 */ #${STYLE_PREFIX}sidebar { position: fixed; top: 0; right: -300px; width: 300px; height: 100%; background: linear-gradient(to bottom, #2c2c30, #1e1e22); box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3); transition: right 0.3s ease; z-index: 9999; } /* 侧边栏打开时 */ #${STYLE_PREFIX}sidebar.open { right: 0; } `); // 保存禁用列表 function saveDisabledList() { GM_setValue('disabled_list', disabledList); // 刷新当前状态 isHighlightDisabled = disabledList.domains.includes(currentDomain) || disabledList.urls.includes(currentPageUrl); // 更新浮动按钮显示状态 const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`); if (floatingButton) { floatingButton.style.display = (settings.showFloatingButton && !isHighlightDisabled) ? 'flex' : 'none'; } } // 禁用域名 function disableDomain(domain) { if (!disabledList.domains.includes(domain)) { disabledList.domains.push(domain); saveDisabledList(); } } // 启用域名 function enableDomain(domain) { disabledList.domains = disabledList.domains.filter(d => d !== domain); saveDisabledList(); } // 禁用URL function disableUrl(url) { if (!disabledList.urls.includes(url)) { disabledList.urls.push(url); saveDisabledList(); } } // 启用URL function enableUrl(url) { disabledList.urls = disabledList.urls.filter(u => u !== url); saveDisabledList(); } // 加载当前页面的高亮 function loadHighlights() { const allHighlights = GM_getValue('highlights', {}); highlights = allHighlights[currentPageUrl] || []; return highlights; } // 保存高亮到存储 function saveHighlights() { const allHighlights = GM_getValue('highlights', {}); allHighlights[currentPageUrl] = highlights; GM_setValue('highlights', allHighlights); } // 保存设置 function saveSettings() { GM_setValue('highlight_settings', settings); } // 移除高亮菜单 function removeHighlightMenu() { if (window.currentMenuCloseHandler) { document.removeEventListener('click', window.currentMenuCloseHandler); window.currentMenuCloseHandler = null; } const existingMenus = document.querySelectorAll(`.${STYLE_PREFIX}highlight-menu`); if (existingMenus.length) { existingMenus.forEach(menu => { menu.classList.remove(`${STYLE_PREFIX}show`); setTimeout(() => { if (menu && menu.parentNode) { menu.parentNode.removeChild(menu); } }, 200); }); } clearTimeout(menuDisplayTimer); ignoreNextClick = false; menuOperationInProgress = false; } // 高亮选中文本 function highlightSelection(color) { if (isHighlightDisabled) { return null; } const selection = window.getSelection(); if (!selection.rangeCount) return null; const range = selection.getRangeAt(0); const selectedText = selection.toString().trim(); if (!selectedText || selectedText.length < settings.minTextLength) { return null; } const highlightId = 'highlight-' + Date.now() + '-' + Math.floor(Math.random() * 10000); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlightId; highlightElement.style.backgroundColor = color; // ★ 先从未修改前的文本中提取上下文 let prefix = '', suffix = ''; if (range.startContainer.nodeType === Node.TEXT_NODE) { const originalText = range.startContainer.textContent; const startOffset = range.startOffset; const endOffset = startOffset + selectedText.length; prefix = extractValidContext(originalText, startOffset, 20, "backward"); suffix = extractValidContext(originalText, endOffset, 20, "forward"); } try { // 再进行DOM操作前,提取上下文后才调用 extractContents const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); const highlight = { id: highlightId, text: selectedText, color: color, timestamp: Date.now(), url: currentPageUrl, prefix: prefix, // 前置上下文 suffix: suffix // 后置上下文 }; highlights.push(highlight); saveHighlights(); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); removeHighlightMenu(); setTimeout(() => { showHighlightEditMenu(e, highlightId); }, 10); }); // 检查侧边栏是否打开,如果打开则刷新高亮列表 const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`); if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) { updateSidebarHighlights(); } selection.removeAllRanges(); return highlightId; } catch (e) { console.warn('高亮失败:', e); try { findAndHighlight(selectedText, color, highlightId); // 检查侧边栏是否打开,如果打开则刷新高亮列表 const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`); if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) { updateSidebarHighlights(); } return highlightId; } catch (error) { console.error('替代高亮方法也失败:', error); return null; } } } // 根据ID删除高亮 function removeHighlightById(highlightId) { const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); if (highlightElement) { const textNode = document.createTextNode(highlightElement.textContent); highlightElement.parentNode.replaceChild(textNode, highlightElement); } highlights = highlights.filter(h => h.id !== highlightId); saveHighlights(); // 检查侧边栏是否打开,如果打开则刷新高亮列表 const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`); if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) { updateSidebarHighlights(); } } // 使用 MutationObserver 监听 DOM 变化,动态恢复高亮 function observeDomChanges() { let debounceTimer; // 新增变量用于防抖 const observer = new MutationObserver((mutations) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { applyHighlights(); }, 300); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } // 更改高亮颜色 function changeHighlightColor(highlightId, newColor) { const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); if (highlightElement) { highlightElement.style.backgroundColor = newColor; } const index = highlights.findIndex(h => h.id === highlightId); if (index !== -1) { highlights[index].color = newColor; saveHighlights(); } // 检查侧边栏是否打开,如果打开则刷新高亮列表 const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`); if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) { updateSidebarHighlights(); } } // 显示/隐藏侧边栏 function toggleSidebar(forceShow = true) { const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`); const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`); if (!sidebar) return; if (forceShow) { sidebar.style.right = '0px'; // 显示侧边栏时隐藏浮动按钮 if (floatingButton) { floatingButton.style.display = 'none'; } if (updateSidebarHighlights) { updateSidebarHighlights(); } } else { const width = sidebar.style.width || '300px'; const wasVisible = sidebar.style.right === '0px'; sidebar.style.right = wasVisible ? `-${width}` : '0px'; // 更新浮动按钮显示状态 if (floatingButton) { if (wasVisible) { // 关闭侧边栏时,根据设置和禁用状态决定是否显示浮动按钮 floatingButton.style.display = (settings.showFloatingButton && !isHighlightDisabled) ? 'flex' : 'none'; } else { // 打开侧边栏时,隐藏浮动按钮 floatingButton.style.display = 'none'; } } if (sidebar.style.right === '0px' && updateSidebarHighlights) { updateSidebarHighlights(); } } } // 切换浮动按钮显示/隐藏 function toggleFloatingButton() { const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`); if (!floatingButton) return; settings.showFloatingButton = !settings.showFloatingButton; // 即使设置为显示,在禁用页面也不显示按钮 floatingButton.style.display = (settings.showFloatingButton && !isHighlightDisabled) ? 'flex' : 'none'; saveSettings(); } // 显示高亮编辑菜单 function showHighlightEditMenu(event, highlightId) { if (isHighlightDisabled) { return; } removeHighlightMenu(); if (menuOperationInProgress) return; menuOperationInProgress = true; event.preventDefault(); event.stopPropagation(); ignoreNextClick = true; const highlight = highlights.find(h => h.id === highlightId); if (!highlight) { menuOperationInProgress = false; return; } const menu = createHighlightMenu(false); menu.dataset.currentHighlightId = highlightId; menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(colorBtn => { colorBtn.classList.remove('active'); }); const activeColorButton = menu.querySelector(`.${STYLE_PREFIX}highlight-menu-color[data-color="${highlight.color}"]`); if (activeColorButton) { activeColorButton.classList.add('active'); } const menuHeight = 50; let menuTop = event.clientY + window.scrollY - menuHeight - 10; let showAbove = true; if (event.clientY < menuHeight + 10) { menuTop = event.clientY + window.scrollY + 10; showAbove = false; } menu.style.top = `${menuTop}px`; const menuWidth = menu.offsetWidth || 200; let menuLeft; if (event.clientX - (menuWidth / 2) < 5) { menuLeft = 5; } else if (event.clientX + (menuWidth / 2) > window.innerWidth - 5) { menuLeft = window.innerWidth - menuWidth - 5; } else { menuLeft = event.clientX - (menuWidth / 2); } menu.style.left = `${menuLeft}px`; const arrowLeft = event.clientX - menuLeft; const minArrowLeft = 12; const maxArrowLeft = menuWidth - 12; const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft)); menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`); if (!showAbove) { menu.classList.add(`${STYLE_PREFIX}arrow-top`); } else { menu.classList.remove(`${STYLE_PREFIX}arrow-top`); } requestAnimationFrame(() => { menu.classList.add(`${STYLE_PREFIX}show`); // 使用 once:true 来自动清理事件监听 document.addEventListener('click', function closeMenu(e) { if (ignoreNextClick) { ignoreNextClick = false; return; } if (!menu.contains(e.target)) { removeHighlightMenu(); } }, { once: true }); setTimeout(() => { ignoreNextClick = false; menuOperationInProgress = false; }, 50); }); } // 查找并高亮文本 function findAndHighlight(searchText, color, highlightId) { // 遍历所有文本节点查找匹配内容 const treeWalker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null ); while (treeWalker.nextNode()) { const node = treeWalker.currentNode; const textContent = node.textContent; if (!textContent || textContent.trim().length === 0) continue; const idx = textContent.indexOf(searchText); if (idx !== -1) { const range = document.createRange(); range.setStart(node, idx); range.setEnd(node, idx + searchText.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlightId; highlightElement.style.backgroundColor = color; try { const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlightId); }); // 新高亮直接返回 true return true; } catch (e) { console.warn('应用高亮失败:', e); } } } return false; } // 应用页面上的所有高亮 function applyHighlights() { // 按 timestamp 降序排序(从后向前恢复) highlights.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); highlights.forEach(highlight => { const restored = advancedRestoreHighlight(highlight); if (!restored) { console.warn('多步恢复失败:', highlight.id); } }); } // 创建高亮菜单 function createHighlightMenu(isNewHighlight = true) { removeHighlightMenu(); ignoreNextClick = true; const menu = document.createElement('div'); menu.className = `${STYLE_PREFIX}highlight-menu`; menu.innerHTML = `
${settings.colors.map(color => `
`).join('')}
`; // 无论如何先置空操作ID menu.dataset.currentHighlightId = ''; document.body.appendChild(menu); // 如果是新建高亮,确保所有颜色块没有激活状态 if (isNewHighlight) { menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => { el.classList.remove('active'); }); } menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => { el.addEventListener('click', (e) => { const color = el.dataset.color; const isActive = el.classList.contains('active'); const currentHighlightId = menu.dataset.currentHighlightId; if (isActive) { if (currentHighlightId) { removeHighlightById(currentHighlightId); menu.dataset.currentHighlightId = ''; menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`) .forEach(colorEl => colorEl.classList.remove('active')); const sel = window.getSelection(); sel.removeAllRanges(); if (savedRange) { sel.addRange(savedRange.cloneRange()); } } else { window.getSelection().removeAllRanges(); menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`) .forEach(colorEl => colorEl.classList.remove('active')); } removeHighlightMenu(); } else { settings.activeColor = color; saveSettings(); if (currentHighlightId) { changeHighlightColor(currentHighlightId, color); } else { const selection = window.getSelection(); if (selection.toString().trim() === '' && savedRange) { selection.removeAllRanges(); selection.addRange(savedRange.cloneRange()); } const newHighlightId = highlightSelection(color); if (newHighlightId) { menu.dataset.currentHighlightId = newHighlightId; } } menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`) .forEach(colorEl => colorEl.classList.toggle('active', colorEl.dataset.color === color)); } e.stopPropagation(); }); }); return menu; } // 显示高亮菜单 function showHighlightMenu() { if (isHighlightDisabled) { return; } if (menuOperationInProgress) return; menuOperationInProgress = true; const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText === '') { menuOperationInProgress = false; return; } const menu = createHighlightMenu(true); const range = selection.getRangeAt(0); const rects = range.getClientRects(); if (rects.length === 0) { menuOperationInProgress = false; return; } const targetRect = rects[0]; const menuHeight = 50; let initialTop = window.scrollY + targetRect.top - menuHeight - 8; let showAbove = true; if (targetRect.top < menuHeight + 10) { initialTop = window.scrollY + targetRect.bottom + 8; showAbove = false; } menu.style.top = `${initialTop}px`; setTimeout(() => { const menuWidth = menu.offsetWidth; const textCenterX = targetRect.left + (targetRect.width / 2); let menuLeft; if (textCenterX - (menuWidth / 2) < 5) { menuLeft = 5; } else if (textCenterX + (menuWidth / 2) > window.innerWidth - 5) { menuLeft = window.innerWidth - menuWidth - 5; } else { menuLeft = textCenterX - (menuWidth / 2); } menu.style.left = `${menuLeft}px`; menu.style.transform = 'none'; const arrowLeft = textCenterX - menuLeft; const minArrowLeft = 12; const maxArrowLeft = menuWidth - 12; const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft)); menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`); if (!showAbove) { menu.classList.add(`${STYLE_PREFIX}arrow-top`); } else { menu.classList.remove(`${STYLE_PREFIX}arrow-top`); } requestAnimationFrame(() => { menu.classList.add(`${STYLE_PREFIX}show`); }); }, 0); document.addEventListener('click', function closeMenu(e) { if (ignoreNextClick) { ignoreNextClick = false; return; } if (!menu.contains(e.target)) { removeHighlightMenu(); } }, { once: true }); setTimeout(() => { ignoreNextClick = false; menuOperationInProgress = false; }, 100); } // 注册事件 function registerEvents() { document.addEventListener('mouseup', function (e) { if (isHighlightDisabled) { return; } if (e.target.closest(`.${STYLE_PREFIX}highlight-menu`)) { return; } const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText.length < (settings.minTextLength || 1)) { return; } if (selection.rangeCount > 0) { savedRange = selection.getRangeAt(0).cloneRange(); } removeHighlightMenu(); clearTimeout(menuDisplayTimer); ignoreNextClick = true; menuDisplayTimer = setTimeout(() => { showHighlightMenu(); }, 10); }); } function fuzzyContextMatch(highlight) { // 如果未启用模糊匹配,则直接返回 false if (!settings.enableFuzzyMatch) return false; if (!highlight.text) return false; const textNodes = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null ); const pattern = highlight.text.trim(); // 预处理前缀和后缀 const storedPrefix = highlight.prefix ? highlight.prefix.trim() : ''; const storedSuffix = highlight.suffix ? highlight.suffix.trim() : ''; // 存储所有匹配项及其上下文评分 const matches = []; // 记录匹配的总数,以便特殊处理单一匹配的情况 let matchCount = 0; while (textNodes.nextNode()) { const node = textNodes.currentNode; const textContent = node.textContent; if (!textContent || textContent.trim().length === 0) continue; // 查找所有可能的匹配位置 let startIdx = 0; while (startIdx < textContent.length) { const idx = textContent.indexOf(pattern, startIdx); if (idx === -1) break; matchCount++; // 获取上下文 const actualPrefix = textContent.substring(Math.max(0, idx - 40), idx).trim(); const actualSuffix = textContent.substring(idx + pattern.length, idx + pattern.length + 40).trim(); // 计算上下文匹配分数 let score = 0; // 前缀匹配分数计算 (更宽松版) if (storedPrefix && actualPrefix) { if (actualPrefix.includes(storedPrefix)) { score += 10; // 前缀完全包含加10分 } else { // 尝试寻找部分匹配 for (let i = 1; i <= Math.min(storedPrefix.length, 12); i++) { const prefixEnd = storedPrefix.slice(-i); if (actualPrefix.endsWith(prefixEnd)) { score += i / 2; // 匹配前缀尾部,加分 break; } } // 尝试在前缀中查找关键片段 if (storedPrefix.length > 6) { for (let i = 0; i < storedPrefix.length - 5; i++) { const fragment = storedPrefix.substring(i, i + 5); if (actualPrefix.includes(fragment)) { score += 2.5; // 找到显著片段加2.5分 break; } } } } } // 后缀匹配分数计算 (更宽松版) if (storedSuffix && actualSuffix) { if (actualSuffix.includes(storedSuffix)) { score += 10; // 后缀完全包含加10分 } else { // 尝试寻找部分匹配 for (let i = 1; i <= Math.min(storedSuffix.length, 12); i++) { const suffixStart = storedSuffix.slice(0, i); if (actualSuffix.startsWith(suffixStart)) { score += i / 2; // 匹配后缀头部,加分 break; } } // 尝试在后缀中查找关键片段 if (storedSuffix.length > 6) { for (let i = 0; i < storedSuffix.length - 5; i++) { const fragment = storedSuffix.substring(i, i + 5); if (actualSuffix.includes(fragment)) { score += 2.5; // 找到显著片段加2.5分 break; } } } } } // 如果是单个字符的高亮,给予额外分数,避免完全相同的短文本被错过 if (pattern.length <= 3 && (actualPrefix.includes(storedPrefix) || actualSuffix.includes(storedSuffix))) { score += 5; } // 记录匹配项 matches.push({ node, idx, score, actualPrefix, actualSuffix }); startIdx = idx + 1; // 继续搜索下一个匹配 } } // 特殊情况:如果页面上只有一个匹配,直接使用它 if (matchCount === 1 && matches.length === 1) { const match = matches[0]; try { const range = document.createRange(); range.setStart(match.node, match.idx); range.setEnd(match.node, match.idx + pattern.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlight.id; highlightElement.style.backgroundColor = highlight.color; const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlight.id); }); return true; } catch (e) { console.warn('唯一模糊匹配高亮失败:', e); } } // 如果有多个匹配项,选择分数最高的 if (matches.length > 0) { // 按分数降序排序 matches.sort((a, b) => b.score - a.score); const bestMatch = matches[0]; // 降低得分阈值到2,使更多匹配可以被接受 const threshold = matchCount > 1 ? 2 : 0.5; if (bestMatch.score >= threshold) { // 构造高亮 try { const range = document.createRange(); range.setStart(bestMatch.node, bestMatch.idx); range.setEnd(bestMatch.node, bestMatch.idx + pattern.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlight.id; highlightElement.style.backgroundColor = highlight.color; const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlight.id); }); return true; } catch (e) { console.warn('模糊匹配插入高亮失败:', e); } } else { console.log('模糊匹配分数过低,尝试进一步降低要求:', { text: pattern, bestScore: bestMatch.score }); // 如果分数太低但至少有一个匹配,可以考虑用最后的回退机制 if (matches.length > 0 && pattern.length <= 5) { // 对于短文本,如果我们有任何匹配并且上下文也有一些匹配,就使用它 const bestMatch = matches[0]; try { const range = document.createRange(); range.setStart(bestMatch.node, bestMatch.idx); range.setEnd(bestMatch.node, bestMatch.idx + pattern.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlight.id; highlightElement.style.backgroundColor = highlight.color; console.log('使用回退机制恢复短文本高亮:', pattern); const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlight.id); }); return true; } catch (e) { console.warn('回退机制高亮失败:', e); } } } } return false; } function advancedRestoreHighlight(highlight) { // 检查是否已经有相同ID的高亮存在 const existingHighlight = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlight.id}"]`); if (existingHighlight) { // 高亮已存在,不需要重复恢复 return true; } // 使用文本上下文匹配恢复高亮 const contextRestored = restoreHighlightUsingContext(highlight); if (contextRestored) { // 只保存最新一次恢复记录 highlight.recoveryHistory = { timestamp: Date.now(), method: 'contextMatch', success: true }; saveHighlights(); // 保存更新后的恢复历史 return true; } // 尝试模糊匹配 if (fuzzyContextMatch(highlight)) { highlight.recoveryHistory = { timestamp: Date.now(), method: 'fuzzyContext', success: true }; saveHighlights(); return true; } // 记录失败状态 highlight.recoveryHistory = { timestamp: Date.now(), method: 'fallback', success: false }; saveHighlights(); return false; } function restoreHighlightUsingContext(highlight) { // 遍历页面中所有文本节点 const treeWalker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null ); // 如果存在前缀和后缀信息,预处理它们以便于比较 const storedPrefix = highlight.prefix ? highlight.prefix.trim() : ''; const storedSuffix = highlight.suffix ? highlight.suffix.trim() : ''; // 存储所有可能的匹配及其得分 const matches = []; // 记录找到多少个完全相同的文本 let exactTextMatches = 0; while (treeWalker.nextNode()) { const node = treeWalker.currentNode; const textContent = node.textContent; if (!textContent || textContent.trim().length === 0) continue; const idx = textContent.indexOf(highlight.text); if (idx !== -1) { // 记录找到了一个文本匹配 exactTextMatches++; // 获取当前节点中匹配区域前后的上下文 const actualPrefix = textContent.substring(Math.max(0, idx - 30), idx).trim(); const actualSuffix = textContent.substring(idx + highlight.text.length, idx + highlight.text.length + 30).trim(); // 计算上下文匹配得分 let score = 1; // 基础分:找到了文本 // 前缀匹配得分计算 (改进版) if (storedPrefix && actualPrefix) { if (actualPrefix.includes(storedPrefix)) { score += 10; // 前缀完全包含加10分 } else if (storedPrefix.length > 3) { // 尝试匹配前缀的尾部 for (let i = Math.min(storedPrefix.length, actualPrefix.length); i >= 3; i--) { if (storedPrefix.slice(-i) === actualPrefix.slice(-i)) { score += i / 2; // 匹配长度越长分数越高 break; } } // 另外尝试寻找前缀中的部分匹配 if (storedPrefix.length >= 8) { for (let i = 0; i < storedPrefix.length - 6; i++) { const fragment = storedPrefix.substring(i, i + 6); if (actualPrefix.includes(fragment)) { score += 3; // 找到部分匹配加3分 break; } } } } } // 后缀匹配得分计算 (改进版) if (storedSuffix && actualSuffix) { if (actualSuffix.includes(storedSuffix)) { score += 10; // 后缀完全包含加10分 } else if (storedSuffix.length > 3) { // 尝试匹配后缀的开头 for (let i = Math.min(storedSuffix.length, actualSuffix.length); i >= 3; i--) { if (storedSuffix.slice(0, i) === actualSuffix.slice(0, i)) { score += i / 2; // 匹配长度越长分数越高 break; } } // 另外尝试寻找后缀中的部分匹配 if (storedSuffix.length >= 8) { for (let i = 0; i < storedSuffix.length - 6; i++) { const fragment = storedSuffix.substring(i, i + 6); if (actualSuffix.includes(fragment)) { score += 3; // 找到部分匹配加3分 break; } } } } } matches.push({ node, idx, score, actualPrefix, actualSuffix }); } } // 特殊情况处理:如果页面上只有一个文本匹配,直接使用它 if (exactTextMatches === 1 && matches.length === 1) { const match = matches[0]; try { const range = document.createRange(); range.setStart(match.node, match.idx); range.setEnd(match.node, match.idx + highlight.text.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlight.id; highlightElement.style.backgroundColor = highlight.color; const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlight.id); }); return true; } catch (e) { console.warn('唯一文本匹配恢复高亮失败:', e); } } // 如果找到多个匹配项,选择得分最高的 if (matches.length > 0) { // 按得分降序排序 matches.sort((a, b) => b.score - a.score); const bestMatch = matches[0]; // 降低匹配阈值,从5降到3,使得更多的匹配可以被接受 const minScore = exactTextMatches > 1 ? 3 : 1; if (bestMatch.score >= minScore) { try { const range = document.createRange(); range.setStart(bestMatch.node, bestMatch.idx); range.setEnd(bestMatch.node, bestMatch.idx + highlight.text.length); const highlightElement = document.createElement('span'); highlightElement.className = `${STYLE_PREFIX}highlight-marked`; highlightElement.dataset.highlightId = highlight.id; highlightElement.style.backgroundColor = highlight.color; const fragment = range.extractContents(); highlightElement.appendChild(fragment); range.insertNode(highlightElement); highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showHighlightEditMenu(e, highlight.id); }); return true; } catch (e) { console.warn('上下文匹配恢复高亮失败:', e); } } else { console.log('匹配分数过低,尝试模糊匹配:', { text: highlight.text, bestScore: bestMatch.score, matchCount: matches.length }); } } return false; } function extractValidContext(text, start, count, direction) { // direction: "backward" 从 start 往前提取, "forward" 从 start 往后提取 let result = ""; let processedChars = 0; // 对于短文本或单个字符,我们提取更多上下文 const adjustedCount = count * (text.length <= 3 ? 2 : 1); if (direction === "backward") { for (let i = start - 1; i >= 0 && processedChars < adjustedCount * 2; i--) { const ch = text.charAt(i); // 只计算有效字符(中文、英文、数字) if (/[\u4e00-\u9fffA-Za-z0-9]/.test(ch)) { result = ch + result; processedChars++; if (processedChars >= adjustedCount) break; } else { // 空格和标点也记录,但不计入有效字符数 result = ch + result; } } } else { // forward for (let i = start; i < text.length && processedChars < adjustedCount * 2; i++) { const ch = text.charAt(i); // 只计算有效字符(中文、英文、数字) if (/[\u4e00-\u9fffA-Za-z0-9]/.test(ch)) { result += ch; processedChars++; if (processedChars >= adjustedCount) break; } else { // 空格和标点也记录,但不计入有效字符数 result += ch; } } } return result; } // 添加浮动按钮和侧边栏功能 function createFloatingButtonAndSidebar() { // 创建浮动按钮 const floatingButton = document.createElement('button'); floatingButton.id = `${STYLE_PREFIX}floating-button`; // 使用 SVG 图标,代表"汉堡菜单" floatingButton.innerHTML = ` `; // 根据设置和禁用状态决定是否显示 floatingButton.style.display = (settings.showFloatingButton && !isHighlightDisabled) ? 'flex' : 'none'; document.body.appendChild(floatingButton); // 创建侧边栏(初始隐藏) const sidebar = document.createElement('div'); sidebar.id = `${STYLE_PREFIX}sidebar`; Object.assign(sidebar.style, { position: 'fixed', top: '0', right: '-300px', width: '300px', height: '100%', boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.3)', transition: 'none', zIndex: '9999', overflow: 'hidden', // 改为hidden,内部内容区域单独设置overflow display: 'flex', flexDirection: 'column', color: '#f0f0f0', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', background: 'linear-gradient(to bottom, #262630, #1a1a22)', // 更暗、更专业的渐变色 boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.25)', // 增强阴影深度感 borderLeft: '1px solid rgba(255, 255, 255, 0.06)', // 添加微妙的边框 }); // 构建侧边栏内部结构 sidebar.innerHTML = `
${settings.sidebarDescription || '网页划词高亮工具'}
`; document.body.appendChild(sidebar); setTimeout(() => { sidebar.style.transition = 'right 0.3s ease'; }, 10); // 设置侧边栏内部元素样式 const header = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-header`); Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', boxSizing: 'border-box', borderBottom: '1px solid rgba(255, 255, 255, 0.08)', // 更微妙的边框 height: '42px', // 增加高度使其更易于点击 background: 'rgba(0, 0, 0, 0.15)', // 轻微的背景色调 padding: '0 16px', // 更合理的内边距 }); const title = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-title`); Object.assign(title.style, { fontSize: '13px', fontWeight: '600', flex: '1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'default', letterSpacing: '0.3px', // 增加字母间距提高可读性 opacity: '0.9', }); const controls = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-controls`); Object.assign(controls.style, { display: 'flex', gap: '8px' }); // 设置按钮样式 sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-controls button`).forEach(btn => { Object.assign(btn.style, { background: 'none', border: 'none', cursor: 'pointer', padding: '3px', // 从5px减小到3px display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ccc', borderRadius: '3px', transition: 'background-color 0.2s' }); // 添加按钮悬停效果 btn.addEventListener('mouseenter', () => { btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; }); btn.addEventListener('mouseleave', () => { btn.style.backgroundColor = 'transparent'; }); }); // 设置标签页样式 const tabs = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-tabs`); Object.assign(tabs.style, { display: 'flex', borderBottom: '1px solid rgba(255, 255, 255, 0.08)', padding: '0', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.12)', height: '38px' }); sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(tab => { Object.assign(tab.style, { height: '100%', // 占满容器高度 cursor: 'pointer', fontWeight: '500', // 稍微加粗 background: 'none', border: 'none', color: '#ccc', borderBottom: '2px solid transparent', margin: '0', transition: 'all 0.2s ease', flex: '1', display: 'flex', alignItems: 'center', justifyContent: 'center', letterSpacing: '0.3px', // 增加字母间距提高可读性 fontSize: '12px', // 减小字体 padding: '0 12px', // 添加水平内边距 borderRadius: '0', // 移除边角圆角 opacity: '0.75', // 非激活状态降低不透明度 }); }); // 激活的标签页样式 const activeTab = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-tab.active`); if (activeTab) { Object.assign(activeTab.style, { color: '#fff', borderBottom: '2px solid rgba(190, 60, 60, 0.8)', // 改为指定颜色 backgroundColor: 'rgba(255, 144, 156, 0.08)', // 使用主色的半透明版本 opacity: '1', }); } // 内容区域样式 const contentArea = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-content`); Object.assign(contentArea.style, { flex: '1', overflow: 'hidden', position: 'relative' }); // 设置面板样式 sidebar.querySelectorAll(`.${STYLE_PREFIX}tab-panel`).forEach(panel => { Object.assign(panel.style, { height: '100%', width: '100%', position: 'absolute', top: '0', left: '0', padding: '14px 14px 14px 14px', // 修改为:上右下左,底部padding设为0 boxSizing: 'border-box', overflow: 'auto', display: 'none' }); }); // 显示当前活动面板 const activePanel = sidebar.querySelector(`.${STYLE_PREFIX}tab-panel.active`); if (activePanel) { activePanel.style.display = 'block'; } // 添加侧边栏拖拽调整区域(位于侧边栏的最左侧) const resizer = document.createElement('div'); Object.assign(resizer.style, { position: 'absolute', left: '0', top: '0', width: '5px', height: '100%', cursor: 'ew-resize', backgroundColor: 'transparent' }); sidebar.appendChild(resizer); // 拖拽事件逻辑 resizer.addEventListener('mousedown', initResize); function initResize(e) { e.preventDefault(); window.addEventListener('mousemove', resizeSidebar); window.addEventListener('mouseup', stopResize); } function resizeSidebar(e) { // 计算出新的宽度:侧边栏右对齐,宽度 = 窗口宽度 - 鼠标水平位置 const newWidth = window.innerWidth - e.clientX; // 限制最小宽度为 150px,最大宽度为窗口 80% if (newWidth >= 150 && newWidth <= window.innerWidth * 0.8) { sidebar.style.width = newWidth + 'px'; // 更新设置中的宽度 settings.sidebarWidth = newWidth; saveSettings(); } } function stopResize(e) { window.removeEventListener('mousemove', resizeSidebar); window.removeEventListener('mouseup', stopResize); } // 标签页切换事件 sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(tab => { tab.addEventListener('click', () => { // 移除所有标签页和面板的活动状态 sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(t => { t.classList.remove('active'); t.style.color = '#ccc'; t.style.borderBottom = '2px solid transparent'; t.style.backgroundColor = 'transparent'; }); sidebar.querySelectorAll(`.${STYLE_PREFIX}tab-panel`).forEach(p => { p.classList.remove('active'); p.style.display = 'none'; }); // 激活当前标签和面板 tab.classList.add('active'); tab.style.color = '#fff'; tab.style.borderBottom = '2px solid rgba(190, 60, 60, 0.8)'; // 改为指定颜色 const panelId = tab.getAttribute('data-tab'); const panel = sidebar.querySelector(`.${STYLE_PREFIX}tab-panel[data-panel="${panelId}"]`); if (panel) { panel.classList.add('active'); panel.style.display = 'block'; } }); // 添加悬停效果 tab.addEventListener('mouseenter', () => { if (!tab.classList.contains('active')) { tab.style.backgroundColor = 'rgba(255, 144, 156, 0.05)'; // 轻微的主色背景 tab.style.borderBottom = '2px solid'; // 淡化的主色边框 } }); tab.addEventListener('mouseleave', () => { if (!tab.classList.contains('active')) { tab.style.backgroundColor = 'transparent'; tab.style.borderBottom = '2px solid transparent'; } }); }); // 标题双击编辑功能 const titleElement = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-title`); titleElement.addEventListener('dblclick', () => { const currentTitle = titleElement.textContent.trim() || '高亮工具'; const input = document.createElement('input'); input.type = 'text'; input.value = currentTitle; // 美化输入框样式 Object.assign(input.style, { width: '100%', fontSize: '14px', // 从16px减小到14px fontWeight: '500', padding: '2px 6px', // 调整内边距使其更美观 boxSizing: 'border-box', border: '1px solid rgba(116, 180, 255, 0.5)', borderRadius: '3px', // 添加圆角 outline: 'none', background: 'rgba(0, 0, 0, 0.2)', color: '#fff', transition: 'all 0.15s ease', // 添加过渡效果 boxShadow: '0 0 0 1px rgba(116, 180, 255, 0.3)' }); // 替换标题内容为输入框 titleElement.innerHTML = ''; titleElement.appendChild(input); input.focus(); input.select(); // 添加输入框聚焦样式 input.addEventListener('focus', () => { input.style.background = 'rgba(20, 20, 20, 0.4)'; input.style.boxShadow = '0 0 0 2px rgba(116, 180, 255, 0.4)'; }); // 确认修改:输入框失焦或按下 Enter 键时更新标题 const confirmChange = () => { const newTitle = input.value.trim() || '高亮工具'; settings.sidebarDescription = newTitle; titleElement.textContent = newTitle; saveSettings(); }; input.addEventListener('blur', confirmChange); input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { input.blur(); } else if (event.key === 'Escape') { // 按ESC键取消编辑 titleElement.textContent = currentTitle; input.blur(); } }); }); // 关闭按钮事件 const closeButton = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-close`); closeButton.addEventListener('click', () => { sidebar.style.right = `-${parseInt(sidebar.style.width)}px`; // 侧边栏关闭时,如果设置允许显示浮动按钮且当前页面未禁用,则恢复显示浮动按钮 if (settings.showFloatingButton && !isHighlightDisabled) { floatingButton.style.display = 'flex'; } }); // 浮动按钮点击后切换侧边栏的显示和隐藏 floatingButton.addEventListener('click', () => { if (sidebar.style.right === '0px') { sidebar.style.right = `-${parseInt(sidebar.style.width)}px`; // 如果设置允许显示浮动按钮且当前页面未禁用,则显示浮动按钮 if (settings.showFloatingButton && !isHighlightDisabled) { floatingButton.style.display = 'flex'; } } else { sidebar.style.right = '0px'; // 当侧边栏显示时,隐藏浮动按钮 floatingButton.style.display = 'none'; // 刷新高亮列表 if (updateSidebarHighlights) { updateSidebarHighlights(); } } }); // 初始设置宽度 if (settings.sidebarWidth) { sidebar.style.width = `${settings.sidebarWidth}px`; sidebar.style.right = `-${settings.sidebarWidth}px`; // 确保初始位置与实际宽度匹配 } else { sidebar.style.right = '-300px'; // 默认宽度的对应位置 } // 渲染高亮列表面板 function renderHighlightsList() { const highlightsListContainer = sidebar.querySelector(`.${STYLE_PREFIX}highlights-list`); if (!highlightsListContainer) return; // 清空容器 highlightsListContainer.innerHTML = ''; Object.assign(highlightsListContainer.style, { height: 'calc(100vh - 120px)', // 调整合适高度,减去顶栏和标签栏高度 overflow: 'hidden', display: 'flex', // 添加flex布局 flexDirection: 'column', // 确保子元素垂直排列 paddingBottom: '0' // 确保底部无padding }); // 创建高亮列表 const listContainer = document.createElement('div'); listContainer.className = `${STYLE_PREFIX}highlights-items`; Object.assign(listContainer.style, { display: 'flex', flexDirection: 'column', gap: '8px', height: 'calc(100% - 22px)', // 从12px调整到22px,给上移的底部按钮腾出额外空间 overflow: 'auto', paddingRight: '8px', paddingBottom: '4px' // 已经是5px,保持不变 }); // 自定义滚动条样式 const styleEl = document.createElement('style'); styleEl.textContent = ` .${STYLE_PREFIX}highlights-items::-webkit-scrollbar { width: 5px; /* 更细的滚动条 */ } .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.03); /* 更微妙的轨道 */ border-radius: 3px; } .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); /* 更微妙的滑块 */ border-radius: 3px; } .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); } `; document.head.appendChild(styleEl); // 排序高亮,按时间倒序 const sortedHighlights = [...highlights].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); if (sortedHighlights.length === 0) { // 显示空状态 const emptyState = document.createElement('div'); Object.assign(emptyState.style, { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 20px', textAlign: 'center', color: '#999', fontSize: '14px' }); // 使用SVG图标作为空状态图标 emptyState.innerHTML = `

暂无高亮内容
选中文本并点击颜色进行高亮

`; listContainer.appendChild(emptyState); } else { // 渲染所有高亮项目 sortedHighlights.forEach((highlight, index) => { const highlightItem = createHighlightItem(highlight, index); listContainer.appendChild(highlightItem); }); } highlightsListContainer.appendChild(listContainer); // 创建底部固定按钮栏 const bottomActionBar = document.createElement('div'); bottomActionBar.className = `${STYLE_PREFIX}highlights-bottom-actions`; Object.assign(bottomActionBar.style, { display: 'flex', position: 'absolute', bottom: '0', left: '0', right: '0', padding: '10px 16px', // 调整内边距 borderTop: '1px solid rgba(255, 255, 255, 0.06)', // 更微妙的分隔线 background: 'rgba(26, 26, 32, 0.9)', // 半透明背景 backdropFilter: 'blur(10px)', // 磨砂玻璃效果 zIndex: '1', gap: '10px', }); // 创建刷新按钮 const refreshBtn = document.createElement('button'); refreshBtn.textContent = '刷新列表'; Object.assign(refreshBtn.style, { flex: '1', background: 'rgba(255, 255, 255, 0.06)', // 保持原来的背景色 border: '1px solid rgba(255, 255, 255, 0.08)', // 保持原来的边框 borderRadius: '4px', padding: '8px 12px', // 添加水平内边距与清除按钮一致 color: '#e0e0e0', fontSize: '13px', // 增大字体与清除按钮一致 fontWeight: '500', cursor: 'pointer', // 添加指针样式 transition: 'all 0.2s ease' // 添加过渡效果 }); // 添加悬停效果 refreshBtn.addEventListener('mouseenter', () => { refreshBtn.style.background = 'rgba(255, 255, 255, 0.15)'; }); refreshBtn.addEventListener('mouseleave', () => { refreshBtn.style.background = 'rgba(255, 255, 255, 0.06)'; }); refreshBtn.addEventListener('click', () => { // 刷新高亮列表 loadHighlights(); applyHighlights(); renderHighlightsList(); }); // 创建清除按钮 const clearBtn = document.createElement('button'); clearBtn.textContent = '清除全部'; Object.assign(clearBtn.style, { flex: '1', background: 'rgba(190, 60, 60, 0.8)', // 更和谐的深红色 border: '1px solid rgba(190, 60, 60, 0.2)', // 淡化边框 color: '#f5e0e0', // 更柔和的白色文字 borderRadius: '4px', padding: '8px 12px', fontSize: '13px', fontWeight: '500', cursor: 'pointer', transition: 'all 0.2s ease' }); // 添加悬停效果 clearBtn.addEventListener('mouseenter', () => { clearBtn.style.background = 'rgba(205, 70, 70, 0.85)'; clearBtn.style.color = '#f7e7e7'; }); clearBtn.addEventListener('mouseleave', () => { clearBtn.style.background = 'rgba(190, 60, 60, 0.8)'; clearBtn.style.color = '#f5e0e0'; }); clearBtn.addEventListener('click', () => { if (highlights.length === 0) return; // 确认删除 if (confirm('确定要删除所有高亮吗?此操作不可撤销。')) { // 移除DOM中的高亮元素 document.querySelectorAll(`.${STYLE_PREFIX}highlight-marked`).forEach(el => { const textNode = document.createTextNode(el.textContent); el.parentNode.replaceChild(textNode, el); }); // 清空高亮数组 highlights = []; saveHighlights(); renderHighlightsList(); } }); bottomActionBar.appendChild(refreshBtn); bottomActionBar.appendChild(clearBtn); highlightsListContainer.appendChild(bottomActionBar); } updateSidebarHighlights = renderHighlightsList; // 创建单个高亮项目 function createHighlightItem(highlight, index) { const item = document.createElement('div'); item.className = `${STYLE_PREFIX}highlight-item`; item.dataset.highlightId = highlight.id; Object.assign(item.style, { backgroundColor: 'rgba(30, 30, 36, 0.6)', // 更深、更专业的卡片背景 borderRadius: '5px', // 更小的圆角 padding: '10px 12px', // 增加水平内边距 position: 'relative', transition: 'all 0.2s cubic-bezier(0.1, 0.9, 0.2, 1)', // 更平滑的过渡动画 border: '1px solid rgba(255, 255, 255, 0.04)', // 更微妙的边框 backdropFilter: 'blur(8px)', // 添加磨砂玻璃效果 (现代浏览器) }) // 颜色指示器 const colorIndicator = document.createElement('div'); Object.assign(colorIndicator.style, { position: 'absolute', top: '0', left: '0', width: '3px', height: '100%', backgroundColor: highlight.color, borderTopLeftRadius: '12px', borderBottomLeftRadius: '12px' }); // 高亮内容 const content = document.createElement('div'); Object.assign(content.style, { paddingLeft: '3px', color: '#fff', fontSize: '14px', lineHeight: '1.4', marginBottom: '8px', wordBreak: 'break-word', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: '2', overflow: 'hidden', textOverflow: 'ellipsis' }); // 处理高亮文本,避免XSS const textNode = document.createTextNode(highlight.text); content.appendChild(textNode); // 底部信息栏 const infoBar = document.createElement('div'); Object.assign(infoBar.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', marginTop: '6px', paddingLeft: '3px' // 与内容区域左边距统一 }); // 时间信息 const timeInfo = document.createElement('div'); Object.assign(timeInfo.style, { color: '#999', fontSize: '12px' }); // 格式化时间 const date = new Date(highlight.timestamp); const formattedDate = `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; timeInfo.textContent = formattedDate; // 操作按钮容器 const actionButtons = document.createElement('div'); Object.assign(actionButtons.style, { display: 'flex', gap: '10px' }); // 跳转按钮 const jumpButton = document.createElement('button'); Object.assign(jumpButton.style, { background: 'none', border: 'none', padding: '3px', cursor: 'pointer', color: '#74b4ff', display: 'flex', alignItems: 'center', fontSize: '12px', transition: 'color 0.15s ease' }); jumpButton.title = '跳转到此高亮'; jumpButton.innerHTML = ` `; jumpButton.addEventListener('mouseenter', () => { jumpButton.style.color = '#a0cfff'; }); jumpButton.addEventListener('mouseleave', () => { jumpButton.style.color = '#74b4ff'; }); jumpButton.addEventListener('click', (e) => { e.stopPropagation(); scrollToHighlight(highlight.id); }); // 删除按钮 const deleteButton = document.createElement('button'); Object.assign(deleteButton.style, { background: 'none', border: 'none', padding: '3px', cursor: 'pointer', color: 'rgba(190, 60, 60, 0.8)', display: 'flex', alignItems: 'center', fontSize: '12px', transition: 'color 0.15s ease' }); deleteButton.title = '删除此高亮'; deleteButton.innerHTML = ` `; deleteButton.addEventListener('mouseenter', () => { deleteButton.style.color = 'rgba(255, 80, 80, 0.95)'; // 更亮、更饱和的红色 }); deleteButton.addEventListener('mouseleave', () => { deleteButton.style.color = 'rgba(190, 60, 60, 0.8)'; // 恢复原来的颜色 }); deleteButton.addEventListener('click', (e) => { e.stopPropagation(); removeHighlightById(highlight.id); item.style.opacity = '0'; item.style.height = '0'; item.style.padding = '0'; item.style.margin = '0'; item.style.overflow = 'hidden'; setTimeout(() => { renderHighlightsList(); }, 300); }); actionButtons.appendChild(jumpButton); actionButtons.appendChild(deleteButton); infoBar.appendChild(timeInfo); infoBar.appendChild(actionButtons); // 添加项目悬停效果 item.addEventListener('mouseenter', () => { item.style.backgroundColor = 'rgba(40, 40, 50, 0.75)'; // 更微妙的变化 item.style.transform = 'translateY(-2px)'; // 轻微上浮 item.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.1)'; // 分层阴影 }); item.addEventListener('mouseleave', () => { item.style.backgroundColor = 'rgba(0, 0, 0, 0.2)'; item.style.transform = 'translateY(0)'; item.style.boxShadow = 'none'; }); item.appendChild(colorIndicator); item.appendChild(content); item.appendChild(infoBar); return item; } // 滚动到指定高亮 function scrollToHighlight(highlightId) { const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); if (highlightElement) { // 平滑滚动到元素 highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 添加闪烁效果,并设置临时样式使其更加明显 highlightElement.classList.add(`${STYLE_PREFIX}highlight-flash`); // 保存原有的样式以便恢复 const originalTransition = highlightElement.style.transition; // 添加过渡效果使视觉反馈更平滑 highlightElement.style.transition = 'all 0.3s ease'; setTimeout(() => { highlightElement.classList.remove(`${STYLE_PREFIX}highlight-flash`); highlightElement.style.transition = originalTransition; }, 2500); } } // 初始渲染高亮列表 renderHighlightsList(); // 渲染禁用管理面板内容 function renderDisabledPanel() { const container = sidebar.querySelector(`.${STYLE_PREFIX}disabled-container`); if (!container) return; // 清空容器 container.innerHTML = ''; // 添加当前页面管理区域 const currentPageSection = document.createElement('div'); currentPageSection.className = `${STYLE_PREFIX}disabled-section`; Object.assign(currentPageSection.style, { marginBottom: '20px' }); const currentPageTitle = document.createElement('div'); currentPageTitle.className = `${STYLE_PREFIX}disabled-title`; currentPageTitle.innerHTML = `当前页面`; Object.assign(currentPageTitle.style, { fontSize: '14px', fontWeight: '600', color: '#eee', marginBottom: '10px', display: 'flex', alignItems: 'center', gap: '8px' }); // 当前页面状态 const currentStatus = document.createElement('div'); currentStatus.className = `${STYLE_PREFIX}current-status`; currentStatus.innerHTML = renderCurrentPageStatus(); currentPageSection.appendChild(currentPageTitle); currentPageSection.appendChild(currentStatus); container.appendChild(currentPageSection); // 禁用域名列表区域 const domainsSection = document.createElement('div'); domainsSection.className = `${STYLE_PREFIX}disabled-section`; Object.assign(domainsSection.style, { marginBottom: '20px' }); const domainsTitle = document.createElement('div'); domainsTitle.className = `${STYLE_PREFIX}disabled-title`; domainsTitle.innerHTML = `禁用域名列表`; Object.assign(domainsTitle.style, { fontSize: '14px', fontWeight: '600', color: '#eee', marginBottom: '10px', display: 'flex', alignItems: 'center', gap: '8px' }); const domainsList = document.createElement('div'); domainsList.className = `${STYLE_PREFIX}domains-list`; domainsList.innerHTML = renderDisabledDomains(); // 添加域名表单 const addDomainForm = document.createElement('div'); addDomainForm.className = `${STYLE_PREFIX}add-disabled-form`; Object.assign(addDomainForm.style, { display: 'flex', marginTop: '12px', gap: '0' }); const domainInput = document.createElement('input'); domainInput.className = `${STYLE_PREFIX}add-disabled-input`; domainInput.id = 'add-domain-input'; domainInput.placeholder = '输入域名...'; Object.assign(domainInput.style, { flex: '1', backgroundColor: 'rgba(255, 255, 255, 0.07)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '4px 0 0 4px', padding: '8px 12px', fontSize: '13px', color: '#fff', outline: 'none' }); const addDomainBtn = document.createElement('button'); addDomainBtn.className = `${STYLE_PREFIX}add-disabled-button`; addDomainBtn.id = 'add-domain-btn'; addDomainBtn.textContent = '添加'; Object.assign(addDomainBtn.style, { backgroundColor: 'rgba(190, 60, 60, 0.8)', color: '#f5e0e0', border: 'none', borderRadius: '0 4px 4px 0', padding: '8px 16px', fontSize: '13px', fontWeight: '500', cursor: 'pointer', transition: 'all 0.2s ease' }); addDomainForm.appendChild(domainInput); addDomainForm.appendChild(addDomainBtn); domainsSection.appendChild(domainsTitle); domainsSection.appendChild(domainsList); domainsSection.appendChild(addDomainForm); container.appendChild(domainsSection); // 禁用URL列表区域 const urlsSection = document.createElement('div'); urlsSection.className = `${STYLE_PREFIX}disabled-section`; const urlsTitle = document.createElement('div'); urlsTitle.className = `${STYLE_PREFIX}disabled-title`; urlsTitle.innerHTML = `禁用网址列表`; Object.assign(urlsTitle.style, { fontSize: '14px', fontWeight: '600', color: '#eee', marginBottom: '10px', display: 'flex', alignItems: 'center', gap: '8px' }); const urlsList = document.createElement('div'); urlsList.className = `${STYLE_PREFIX}urls-list`; urlsList.innerHTML = renderDisabledUrls(); // 添加URL表单 const addUrlForm = document.createElement('div'); addUrlForm.className = `${STYLE_PREFIX}add-disabled-form`; Object.assign(addUrlForm.style, { display: 'flex', marginTop: '12px', gap: '0' }); const urlInput = document.createElement('input'); urlInput.className = `${STYLE_PREFIX}add-disabled-input`; urlInput.id = 'add-url-input'; urlInput.placeholder = '输入网址...'; Object.assign(urlInput.style, { flex: '1', backgroundColor: 'rgba(255, 255, 255, 0.07)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '4px 0 0 4px', padding: '8px 12px', fontSize: '13px', color: '#fff', outline: 'none' }); const addUrlBtn = document.createElement('button'); addUrlBtn.className = `${STYLE_PREFIX}add-disabled-button`; addUrlBtn.id = 'add-url-btn'; addUrlBtn.textContent = '添加'; Object.assign(addUrlBtn.style, { backgroundColor: 'rgba(190, 60, 60, 0.8)', color: '#f5e0e0', border: 'none', borderRadius: '0 4px 4px 0', padding: '8px 16px', fontSize: '13px', fontWeight: '500', cursor: 'pointer', transition: 'all 0.2s ease' }); addUrlForm.appendChild(urlInput); addUrlForm.appendChild(addUrlBtn); urlsSection.appendChild(urlsTitle); urlsSection.appendChild(urlsList); urlsSection.appendChild(addUrlForm); container.appendChild(urlsSection); // 绑定事件 bindDisabledPanelEvents(); } // 渲染当前页面状态 function renderCurrentPageStatus() { const isDomainDisabled = disabledList.domains.includes(currentDomain); const isUrlDisabled = disabledList.urls.includes(currentPageUrl); if (isDomainDisabled || isUrlDisabled) { return `
${isDomainDisabled ? `此域名 (${currentDomain}) 已禁用高亮` : '此网址已禁用高亮'}
启用
`; } else { return `
`; } } // 渲染禁用域名列表 function renderDisabledDomains() { if (disabledList.domains.length === 0) { return `
没有禁用的域名
`; } return disabledList.domains.map(domain => `
${domain}
删除
`).join(''); } // 渲染禁用URL列表 function renderDisabledUrls() { if (disabledList.urls.length === 0) { return `
没有禁用的网址
`; } return disabledList.urls.map(url => { // 为了美观,截断过长的URL const displayUrl = url.length > 40 ? url.substring(0, 37) + '...' : url; return `
${displayUrl}
删除
`; }).join(''); } // 绑定禁用管理面板事件 function bindDisabledPanelEvents() { // 禁用当前域名按钮 const disableDomainBtn = document.getElementById('disable-domain-btn'); if (disableDomainBtn) { disableDomainBtn.addEventListener('click', () => { if (confirm(`确定要禁用域名 "${currentDomain}" 上的高亮功能吗?`)) { disableDomain(currentDomain); renderDisabledPanel(); } }); } // 禁用当前网址按钮 const disableUrlBtn = document.getElementById('disable-url-btn'); if (disableUrlBtn) { disableUrlBtn.addEventListener('click', () => { if (confirm('确定要禁用当前网址的高亮功能吗?')) { disableUrl(currentPageUrl); renderDisabledPanel(); } }); } // 添加样式 const styleSheet = document.createElement('style'); styleSheet.textContent = ` .${STYLE_PREFIX}disabled-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; // 增加水平内边距 background-color: rgba(40, 40, 50, 0.4); // 更深的背景 border-radius: 4px; margin-bottom: 6px; // 减少垂直间距 transition: all 0.2s ease; border: 1px solid rgba(255, 255, 255, 0.03); // 添加微妙的边框 } .${STYLE_PREFIX}disabled-item:hover { background-color: rgba(60, 60, 70, 0.4); // 更微妙的悬停效果 transform: translateX(2px); // 添加轻微的位移感 } .${STYLE_PREFIX}disabled-info { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #ddd; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .${STYLE_PREFIX}disabled-action { color: #ff8f8f; font-size: 12px; cursor: pointer; padding: 2px 6px; border-radius: 3px; transition: all 0.2s; opacity: 0.8; } .${STYLE_PREFIX}disabled-action:hover { background-color: rgba(255, 82, 82, 0.15); // 更和谐的悬停效果 opacity: 1; // 悬停时完全不透明 } .${STYLE_PREFIX}empty-list { padding: 10px; color: #888; font-style: italic; font-size: 13px; text-align: center; background-color: rgba(0, 0, 0, 0.15); border-radius: 4px; } .${STYLE_PREFIX}current-page-actions { display: flex; gap: 10px; } .${STYLE_PREFIX}disable-btn { flex: 1; background: rgba(255, 255, 255, 0.08); border: none; border-radius: 4px; padding: 8px 12px; color: #e0e0e0; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .${STYLE_PREFIX}disable-btn:hover { background-color: rgba(255, 255, 255, 0.15); } .${STYLE_PREFIX}add-disabled-input:focus { border-color: #74b4ff; background-color: rgba(255, 255, 255, 0.1); } .${STYLE_PREFIX}add-disabled-button:hover { background-color: rgba(205, 70, 70, 0.85); color: #f7e7e7; } `; document.head.appendChild(styleSheet); // 删除按钮事件 document.querySelectorAll(`.${STYLE_PREFIX}disabled-action`).forEach(btn => { btn.addEventListener('click', (e) => { const type = e.target.dataset.type; const value = e.target.dataset.value; if (e.target.textContent.trim() === '删除') { if (type === 'domain') { disabledList.domains = disabledList.domains.filter(d => d !== value); } else if (type === 'url') { disabledList.urls = disabledList.urls.filter(u => u !== value); } saveDisabledList(); renderDisabledPanel(); } else if (e.target.textContent.trim() === '启用') { if (type === 'domain') { enableDomain(value); } else if (type === 'url') { enableUrl(value); } renderDisabledPanel(); } }); }); // 添加域名按钮 const addDomainBtn = document.getElementById('add-domain-btn'); if (addDomainBtn) { addDomainBtn.addEventListener('click', () => { const input = document.getElementById('add-domain-input'); const domain = input.value.trim(); if (domain) { if (!disabledList.domains.includes(domain)) { disabledList.domains.push(domain); saveDisabledList(); input.value = ''; renderDisabledPanel(); } else { alert('该域名已在禁用列表中'); } } }); } // 添加URL按钮 const addUrlBtn = document.getElementById('add-url-btn'); if (addUrlBtn) { addUrlBtn.addEventListener('click', () => { const input = document.getElementById('add-url-input'); const url = input.value.trim(); if (url) { if (!disabledList.urls.includes(url)) { disabledList.urls.push(url); saveDisabledList(); input.value = ''; renderDisabledPanel(); } else { alert('该网址已在禁用列表中'); } } }); } // 输入框回车事件 const domainInput = document.getElementById('add-domain-input'); if (domainInput) { domainInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { document.getElementById('add-domain-btn').click(); } }); } const urlInput = document.getElementById('add-url-input'); if (urlInput) { urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { document.getElementById('add-url-btn').click(); } }); } } // 初始渲染禁用管理面板 renderDisabledPanel(); } function init() { loadHighlights(); registerEvents(); if (document.readyState === 'complete') { setTimeout(() => { applyHighlights(); observeDomChanges(); }, 500); } else { window.addEventListener('load', () => { setTimeout(() => { applyHighlights(); observeDomChanges(); }, 500); }); } // 注册油猴菜单命令 GM_registerMenuCommand('打开侧边栏', () => { toggleSidebar(true); }); GM_registerMenuCommand('切换浮动按钮显示/隐藏', toggleFloatingButton); } init(); createFloatingButtonAndSidebar(); })();