// ==UserScript== // @name 网页划词高亮工具 // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description 提供网页划词高亮功能,支持WebDAV云端备份 // @author sunny43 // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_listValues // @run-at document-idle // @downloadURL none // ==/UserScript== (function () { 'use strict'; const STYLE_PREFIX = 'sunny43-'; // 全局变量 let highlights = []; const rawPageUrl = window.location.href; const rawDomain = window.location.hostname; const activationContext = resolveActivationContext(rawDomain, rawPageUrl); const activationDomain = activationContext.domain; const activationPageUrl = activationContext.url; let currentPageUrl = rawPageUrl; let settings = GM_getValue('highlight_settings', { colors: ['#ff909c', '#b89fff', '#74b4ff', '#70d382', '#ffcb7e'], activeColor: '#ff909c', minTextLength: 1, sidebarDescription: '高亮工具', sidebarWidth: 320, showFloatingButton: true }); let savedRange = null; // 保存选区范围 let ignoreNextClick = false; // 忽略下一次点击的标志 let menuDisplayTimer = null; // 菜单显示定时器 let menuOperationInProgress = false; // 添加菜单操作锁定 function resolveActivationContext(defaultDomain, defaultUrl) { let domain = defaultDomain; let url = defaultUrl; let isTopWindow = true; try { isTopWindow = window.top === window.self; } catch (e) { isTopWindow = false; } if (!isTopWindow) { let resolvedFromTop = false; try { const topLocation = window.top.location; if (topLocation && topLocation.hostname) { domain = topLocation.hostname; resolvedFromTop = true; } if (topLocation && topLocation.href) { url = topLocation.href; } } catch (e) { // 跨域访问顶层窗口会抛出异常,忽略 } if (!resolvedFromTop && document.referrer) { try { const refUrl = new URL(document.referrer); if (refUrl.hostname) { domain = refUrl.hostname; } if (refUrl.href) { url = refUrl.href; } } catch (e) { // 忽略解析错误,继续使用默认值 } } } return { domain, url }; } // 启用列表 let enabledList = GM_getValue('enabled_list', { domains: [], urls: [] }); // 判断当前页面是否启用:当启用列表为空时,默认启用所有页面 const isEnabledForCurrentPage = (list) => { const emptyList = (!list || (Array.isArray(list.domains) && list.domains.length === 0) && (Array.isArray(list.urls) && list.urls.length === 0)); if (emptyList) return true; // 默认开启 return (list.domains || []).includes(activationDomain) || (list.urls || []).includes(activationPageUrl); }; // 检查当前页面是否启用高亮功能 let isHighlightEnabled = isEnabledForCurrentPage(enabledList); let updateSidebarHighlights = null; GM_addStyle(` /* 高亮菜单样式 */ .${STYLE_PREFIX}highlight-menu, .${STYLE_PREFIX}highlight-menu * { all: initial; box-sizing: border-box; } .${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: 8px; height: 38px !important; z-index: 9999; display: flex; flex-direction: row; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1; color: #fff; opacity: 0; transition: opacity 0.2s ease-in; pointer-events: none; box-sizing: border-box; } .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}show { opacity: 1; pointer-events: auto; /* 显示时响应事件 */ } /* 菜单箭头样式 */ .${STYLE_PREFIX}highlight-menu::after { content: ''; position: absolute; bottom: -5px; left: var(--arrow-left, 50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #333336; margin-left: -6px; box-sizing: border-box; } .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}arrow-top::after { top: -5px; bottom: auto; border-top: none; border-bottom: 6px solid #333336; } /* 颜色选择区域 */ .${STYLE_PREFIX}highlight-menu-colors { display: flex; flex-direction: row; align-items: center; margin: 0 2px; height: 22px !important; flex-wrap: nowrap; flex: 0 0 auto; gap: 6px; } /* 颜色选择按钮 */ .${STYLE_PREFIX}highlight-menu-color { width: 22px !important; height: 22px !important; min-width: 22px !important; min-height: 22px !important; max-width: 22px !important; max-height: 22px !important; border-radius: 50% !important; margin: 0 !important; padding: 0 !important; cursor: pointer; position: relative; display: flex !important; align-items: center; justify-content: center; transition: transform 0.15s ease; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12) !important; flex-shrink: 0 !important; box-sizing: border-box !important; border: none !important; outline: none !important; } .${STYLE_PREFIX}highlight-menu-color:hover { transform: scale(1.12); } .${STYLE_PREFIX}highlight-menu-color.${STYLE_PREFIX}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; } /* 菜单按钮样式已移除(当前未使用) */ /* 闪烁效果用于高亮跳转 */ @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; } `); // 保存启用列表 function saveEnabledList() { GM_setValue('enabled_list', enabledList); // 刷新当前状态(空列表表示默认启用) isHighlightEnabled = isEnabledForCurrentPage(enabledList); // 更新浮动按钮显示状态 const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`); if (floatingButton) { floatingButton.style.display = (settings.showFloatingButton && isHighlightEnabled) ? 'flex' : 'none'; } } // 启用域名 function enableDomain(domain) { if (!enabledList.domains.includes(domain)) { enabledList.domains.push(domain); saveEnabledList(); } } // 启用域名 function disableDomain(domain) { enabledList.domains = enabledList.domains.filter(d => d !== domain); saveEnabledList(); } // 启用URL function enableUrl(url) { if (!enabledList.urls.includes(url)) { enabledList.urls.push(url); saveEnabledList(); } } // 启用URL function disableUrl(url) { enabledList.urls = enabledList.urls.filter(u => u !== url); saveEnabledList(); } function generateUrlCandidates(url) { const candidates = []; if (!url) { return candidates; } candidates.push(url); try { const parsed = new URL(url); const noHash = new URL(parsed.href); if (noHash.hash) { noHash.hash = ''; const candidate = noHash.href.endsWith('#') ? noHash.href.slice(0, -1) : noHash.href; if (!candidates.includes(candidate)) { candidates.push(candidate); } } const noSearch = new URL(noHash.href); if (noSearch.search) { noSearch.search = ''; const candidate = noSearch.href; if (!candidates.includes(candidate)) { candidates.push(candidate); } } const trimmed = noSearch.href.endsWith('/') ? noSearch.href.slice(0, -1) : noSearch.href; if (trimmed && !candidates.includes(trimmed)) { candidates.push(trimmed); } } catch (e) { // 忽略无法解析的 URL } return candidates; } // 加载当前页面的高亮 function loadHighlights() { const allHighlights = GM_getValue('highlights', {}); const candidates = [ currentPageUrl, ...generateUrlCandidates(currentPageUrl), activationPageUrl, ...generateUrlCandidates(activationPageUrl) ]; const visited = new Set(); for (const candidate of candidates) { if (!candidate || visited.has(candidate)) { continue; } visited.add(candidate); const stored = allHighlights[candidate]; if (Array.isArray(stored)) { currentPageUrl = candidate; highlights = stored; return highlights; } } highlights = []; return highlights; } // 加载整个域名下的所有高亮数据 function loadDomainHighlights() { const allHighlights = GM_getValue('highlights', {}); const domainHighlights = {}; const currentDomain = activationDomain; // 遍历所有URL,找出属于当前域名的高亮 for (const [url, highlightArray] of Object.entries(allHighlights)) { try { const urlObj = new URL(url); if (urlObj.hostname === currentDomain && Array.isArray(highlightArray) && highlightArray.length > 0) { domainHighlights[url] = highlightArray; } } catch (e) { // 忽略无效的URL } } return domainHighlights; } // 保存高亮到存储 function saveHighlights() { const allHighlights = GM_getValue('highlights', {}); allHighlights[currentPageUrl] = highlights; GM_setValue('highlights', allHighlights); } // 保存设置 function saveSettings() { GM_setValue('highlight_settings', settings); } // 移除高亮菜单 function removeHighlightMenu() { 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 attachOutsideClose(menu) { document.addEventListener('click', function closeMenu(e) { if (ignoreNextClick) { ignoreNextClick = false; return; } if (!menu.contains(e.target)) { removeHighlightMenu(); } }, { once: true }); } // 高亮选中文本 function highlightSelection(color) { if (!isHighlightEnabled) { return null; } const selection = window.getSelection(); if (!selection.rangeCount) return null; const range = selection.getRangeAt(0); const rawSelectedText = selection.toString(); const trimmedText = rawSelectedText.trim(); if (!trimmedText || trimmedText.length < settings.minTextLength) { return null; } const selectedText = rawSelectedText; const highlightId = 'highlight-' + Date.now() + '-' + Math.floor(Math.random() * 10000); // ★ 先从未修改前的文本中提取上下文 let prefix = '', suffix = ''; let xpath = '', textOffset = 0, parentElement = null; let container = null, containerXPath = '', containerFingerprint = null; let containerOffset = 0; // 容器内的绝对偏移 const globalContext = collectRangeContext(range, 20); if (globalContext) { prefix = globalContext.prefix || ''; suffix = globalContext.suffix || ''; } if (range.startContainer.nodeType === Node.TEXT_NODE) { const originalText = range.startContainer.textContent; const startOffset = range.startOffset; const endOffset = Math.min(originalText.length, startOffset + selectedText.length); if (!prefix) { prefix = extractValidContext(originalText, startOffset, 20, "backward"); } if (!suffix) { suffix = extractValidContext(originalText, endOffset, 20, "forward"); } // 获取父元素用于生成XPath parentElement = range.startContainer.parentElement; textOffset = startOffset; // ★ 新增:查找最小容器 container = findMinimalContainer(range.startContainer); if (container) { try { containerXPath = generateXPath(container); containerFingerprint = generateContainerFingerprint(container); // 计算容器内的绝对偏移 containerOffset = calculateContainerOffset(container, range.startContainer, startOffset); } catch (e) { console.warn('容器信息生成失败:', e); } } // 生成XPath try { xpath = generateXPath(parentElement); } catch (e) { console.warn('XPath生成失败:', e); } } try { // 检查是否需要分片段处理 const fragments = collectHighlightFragments(range, highlightId, color); // 包装高亮(这会处理DOM) wrapRangeWithHighlight(range, highlightId, color); if (fragments && fragments.length > 1) { // 多片段情况:创建主记录和片段记录 const mainHighlight = { id: highlightId, text: selectedText, // 完整文本 color: color, timestamp: Date.now(), url: currentPageUrl, isMultiFragment: true, // 标记为多片段 fragmentCount: fragments.length, // 保留第一个片段的信息用于兼容 xpath: fragments[0].xpath, textOffset: fragments[0].textOffset, textLength: selectedText.length, contextHash: generateContextHash(fragments[0].prefix, fragments[fragments.length - 1].suffix, selectedText), prefix: fragments[0].prefix, suffix: fragments[fragments.length - 1].suffix }; highlights.push(mainHighlight); // 添加所有片段记录 fragments.forEach(fragment => { highlights.push(fragment); }); } else { // 单片段或传统情况 const highlight = { id: highlightId, text: selectedText, color: color, timestamp: Date.now(), url: currentPageUrl, // 优化存储结构 xpath: xpath, textOffset: textOffset, textLength: selectedText.length, contextHash: generateContextHash(prefix, suffix, selectedText), // ★ 新增:容器相关信息 containerXPath: containerXPath, containerOffset: containerOffset, containerFingerprint: containerFingerprint, // 兼容性:保留原有字段 prefix: prefix, // 前置上下文 suffix: suffix // 后置上下文 }; highlights.push(highlight); } saveHighlights(); // 点击事件在包装函数中已处理 // 检查侧边栏是否打开,如果打开则刷新高亮列表 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; } } } // 收集高亮片段信息(新增辅助函数) function collectHighlightFragments(range, highlightId, color) { const fragments = []; const commonAncestor = range.commonAncestorContainer; const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE']; // 判断是否跨块级元素 let containsBlockElement = false; if (commonAncestor.nodeType === Node.ELEMENT_NODE) { containsBlockElement = blockTags.includes(commonAncestor.tagName) || !!commonAncestor.querySelector(blockTags.map(tag => tag.toLowerCase()).join(',')); } if (!containsBlockElement) { // 单片段情况 return null; // 返回null表示使用传统方式 } // 多片段情况:遍历所有文本节点 const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_TEXT, null // 不使用过滤器,遍历所有文本节点 ); let node; let fragmentIndex = 0; let isInRange = false; let foundStart = false; let foundEnd = false; while (node = walker.nextNode()) { // 跳过已高亮的节点 if (node.parentElement && node.parentElement.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { continue; } let nodeStartOffset = 0; let nodeEndOffset = node.textContent.length; let shouldInclude = false; // 检查是否是起始节点 if (node === range.startContainer) { nodeStartOffset = range.startOffset; shouldInclude = true; isInRange = true; foundStart = true; } // 检查是否是结束节点 if (node === range.endContainer) { nodeEndOffset = range.endOffset; shouldInclude = true; foundEnd = true; } // 如果已经开始但还未结束,包含整个节点 if (isInRange && !foundEnd && node !== range.startContainer) { shouldInclude = true; } // 收集片段信息 if (shouldInclude && nodeEndOffset > nodeStartOffset) { const fragmentText = node.textContent.substring(nodeStartOffset, nodeEndOffset); const parentElement = node.parentElement; // 生成该片段的上下文 const fragmentPrefix = extractValidContext(node.textContent, nodeStartOffset, 20, "backward"); const fragmentSuffix = extractValidContext(node.textContent, nodeEndOffset, 20, "forward"); let xpath = ''; try { // 如果父元素只包含一个文本节点,使用父元素的XPath // 否则,为文本节点生成特定的XPath const textNodes = Array.from(parentElement.childNodes).filter(n => n.nodeType === Node.TEXT_NODE); if (textNodes.length === 1) { xpath = generateXPath(parentElement); } else { // 找出当前文本节点在父元素中的索引 const textIndex = textNodes.indexOf(node); xpath = generateXPath(parentElement) + `/text()[${textIndex + 1}]`; } } catch (e) { console.warn('片段XPath生成失败:', e); } fragments.push({ id: `${highlightId}-fragment-${fragmentIndex}`, parentId: highlightId, text: fragmentText, color: color, isFragment: true, fragmentIndex: fragmentIndex, xpath: xpath, textOffset: nodeStartOffset, textLength: fragmentText.length, contextHash: generateContextHash(fragmentPrefix, fragmentSuffix, fragmentText), prefix: fragmentPrefix, suffix: fragmentSuffix, timestamp: Date.now(), url: currentPageUrl }); fragmentIndex++; } // 如果找到了结束节点,停止遍历 if (foundEnd) { break; } } return fragments.length > 1 ? fragments : null; } // 根据ID删除高亮 function removeHighlightById(highlightId) { // 查找主高亮记录 const mainHighlight = highlights.find(h => h.id === highlightId); if (mainHighlight && mainHighlight.isMultiFragment) { // 多片段高亮:需要删除所有片段的DOM元素 // 先删除主ID的元素(如果有) const mainElements = document.querySelectorAll(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); mainElements.forEach(elem => { const textNode = document.createTextNode(elem.textContent); const parent = elem.parentNode; if (parent) { parent.replaceChild(textNode, elem); // 合并相邻的文本节点 parent.normalize(); } }); // 再删除所有片段ID的元素 const fragments = highlights.filter(h => h.parentId === highlightId); fragments.forEach(fragment => { const fragmentElements = document.querySelectorAll(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${fragment.id}"]`); fragmentElements.forEach(elem => { const textNode = document.createTextNode(elem.textContent); const parent = elem.parentNode; if (parent) { parent.replaceChild(textNode, elem); // 合并相邻的文本节点 parent.normalize(); } }); }); // 删除主记录和所有片段记录 highlights = highlights.filter(h => h.id !== highlightId && h.parentId !== highlightId); } else { // 单片段高亮:使用原逻辑 const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); if (highlightElement) { const textNode = document.createTextNode(highlightElement.textContent); const parent = highlightElement.parentNode; if (parent) { parent.replaceChild(textNode, highlightElement); // 合并相邻的文本节点 parent.normalize(); } } 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() { // 检查 document.body 是否存在 if (!document.body) { console.warn('document.body 不存在,无法启动 DOM 监听'); return; } let debounceTimer; // 新增变量用于防抖 let isApplyingHighlights = false; // 防止循环触发 const observer = new MutationObserver((mutations) => { // 如果正在应用高亮,忽略此次变化 if (isApplyingHighlights) { return; } // 过滤掉由脚本自身创建的高亮元素导致的变化 const hasRelevantMutation = mutations.some(mutation => { // 忽略脚本自己创建的高亮标签变化 if (mutation.target && mutation.target.classList && mutation.target.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { return false; } // 忽略父元素是高亮标签的变化 if (mutation.target && mutation.target.parentElement && mutation.target.parentElement.classList && mutation.target.parentElement.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { return false; } // 忽略祖先元素是高亮标签的变化(解决嵌套高亮导致的问题) let parent = mutation.target; while (parent && parent !== document.body) { if (parent.classList && parent.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { return false; } parent = parent.parentElement; } // 忽略添加的节点是脚本UI元素或高亮元素 if (mutation.addedNodes.length > 0) { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const el = node; if (el.classList && ( el.classList.contains(`${STYLE_PREFIX}highlight-marked`) || el.classList.contains(`${STYLE_PREFIX}highlight-menu`) || el.classList.contains(`${STYLE_PREFIX}floating-button`) || el.classList.contains(`${STYLE_PREFIX}sidebar`) )) { return false; } // 检查是否包含高亮元素(避免批量DOM操作触发重新应用) if (el.querySelector && el.querySelector(`.${STYLE_PREFIX}highlight-marked`)) { return false; } } } } // 忽略删除的节点是高亮元素 if (mutation.removedNodes.length > 0) { for (let node of mutation.removedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const el = node; if (el.classList && el.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { return false; } } } } return true; }); if (!hasRelevantMutation) { return; } clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { // 只对没有高亮的元素重新应用高亮,避免影响已有高亮 isApplyingHighlights = true; applyHighlights(); // 延迟重置标志,确保所有由 applyHighlights 引起的 DOM 变化都被忽略 setTimeout(() => { isApplyingHighlights = false; }, 100); }, 300); }); try { observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } catch (e) { console.warn('启动 DOM 监听失败:', e); } } // 更改高亮颜色 function changeHighlightColor(highlightId, newColor) { // 查找主高亮记录 const mainHighlight = highlights.find(h => h.id === highlightId); if (mainHighlight && mainHighlight.isMultiFragment) { // 多片段高亮:更新所有片段的颜色 const fragments = highlights.filter(h => h.parentId === highlightId); // 更新主记录颜色 mainHighlight.color = newColor; // 更新所有片段记录的颜色 fragments.forEach(fragment => { fragment.color = newColor; // 更新DOM元素 const fragmentElements = document.querySelectorAll(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${fragment.id}"]`); fragmentElements.forEach(elem => { elem.style.backgroundColor = newColor; }); }); } else { // 单片段高亮:原逻辑 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 && isHighlightEnabled) ? '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 && isHighlightEnabled) ? 'flex' : 'none'; saveSettings(); } // 显示高亮编辑菜单 function showHighlightEditMenu(event, highlightId) { if (!isHighlightEnabled) { return; } removeHighlightMenu(); if (menuOperationInProgress) return; menuOperationInProgress = true; event.preventDefault(); event.stopPropagation(); ignoreNextClick = true; // 查找高亮记录,如果是片段ID,找到其主记录 let highlight = highlights.find(h => h.id === highlightId); if (highlight && highlight.isFragment && highlight.parentId) { // 如果点击的是片段,使用主记录的ID highlightId = highlight.parentId; 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(`${STYLE_PREFIX}active`); }); const activeColorButton = menu.querySelector(`.${STYLE_PREFIX}highlight-menu-color[data-color="${highlight.color}"]`); if (activeColorButton) { activeColorButton.classList.add(`${STYLE_PREFIX}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 来自动清理事件监听 attachOutsideClose(menu); 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); try { wrapRangeWithHighlight(range, highlightId, color); // 新高亮直接返回 true return true; } catch (e) { console.warn('应用高亮失败:', e); } } } return false; } // 应用页面上的所有高亮 function applyHighlights() { // 过滤出主高亮记录(排除片段记录) const mainHighlights = highlights.filter(h => !h.isFragment); // 分离单片段和多片段高亮 const singleFragmentHighlights = mainHighlights.filter(h => !h.isMultiFragment); const multiFragmentHighlights = mainHighlights.filter(h => h.isMultiFragment); // ★ 优化:按容器分组处理单片段高亮 applySingleFragmentsByContainer(singleFragmentHighlights); // 然后按时间戳升序恢复多片段高亮(从早到晚) multiFragmentHighlights.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); multiFragmentHighlights.forEach(highlight => { const fragments = highlights.filter(h => h.parentId === highlight.id); // 按片段索引倒序排序,从后往前恢复,避免DOM变化影响XPath fragments.sort((a, b) => (b.fragmentIndex || 0) - (a.fragmentIndex || 0)); let allFragmentsRestored = true; let anyFragmentRestored = false; fragments.forEach(fragment => { const restored = applyHighlightOptimized(fragment); if (!restored) { allFragmentsRestored = false; console.warn('片段恢复失败:', fragment.text); } else { anyFragmentRestored = true; } }); // 如果没有任何片段成功恢复,标记主记录为失败 if (!anyFragmentRestored) { markHighlightAsFailed(highlight); console.warn('多片段高亮完全失败:', highlight.text); } else if (!allFragmentsRestored) { // 部分恢复也算失败 markHighlightAsFailed(highlight); console.warn('多片段高亮部分恢复失败:', highlight.text); } else { // 全部成功,重置失败计数 highlight.failedCount = 0; } }); } // ★ 新增:按容器分组处理单片段高亮 function applySingleFragmentsByContainer(singleFragments) { // 按容器 XPath 分组 const containerGroups = new Map(); singleFragments.forEach(highlight => { const containerKey = highlight.containerXPath || highlight.xpath || 'unknown'; if (!containerGroups.has(containerKey)) { containerGroups.set(containerKey, []); } containerGroups.get(containerKey).push(highlight); }); // 遍历每个容器组 containerGroups.forEach((highlightsInContainer, containerXPath) => { // 同一容器内,按时间戳升序排序(早的先恢复) // 但按容器内偏移量降序排序(从后往前恢复,避免偏移变化) highlightsInContainer.sort((a, b) => { // 优先使用容器内偏移,如果没有则使用文本偏移 const offsetA = a.containerOffset !== undefined ? a.containerOffset : a.textOffset || 0; const offsetB = b.containerOffset !== undefined ? b.containerOffset : b.textOffset || 0; // 从后往前排序(偏移大的先处理) return offsetB - offsetA; }); // 逐个恢复该容器内的高亮 highlightsInContainer.forEach(highlight => { const restored = applyHighlightOptimized(highlight); if (!restored) { console.warn('优化恢复失败:', highlight.text); } }); }); } // 创建高亮菜单 function createHighlightMenu(isNewHighlight = true) { removeHighlightMenu(); ignoreNextClick = true; const menu = document.createElement('div'); menu.className = `${STYLE_PREFIX}highlight-menu`; menu.innerHTML = `
`; // 无论如何先置空操作ID menu.dataset.currentHighlightId = ''; document.body.appendChild(menu); // 如果是新建高亮,确保所有颜色块没有激活状态 if (isNewHighlight) { menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => { el.classList.remove(`${STYLE_PREFIX}active`); }); } menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => { el.addEventListener('click', (e) => { const color = el.dataset.color; const isActive = el.classList.contains(`${STYLE_PREFIX}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(`${STYLE_PREFIX}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(`${STYLE_PREFIX}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(`${STYLE_PREFIX}active`, colorEl.dataset.color === color)); } e.stopPropagation(); }); }); return menu; } // 显示高亮菜单 function showHighlightMenu() { if (!isHighlightEnabled) { return; } if (menuOperationInProgress) return; menuOperationInProgress = true; const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText === '') { menuOperationInProgress = false; return; } // 检查选中的文本是否在侧边栏内 const range = selection.getRangeAt(0); let container = range.commonAncestorContainer; if (container.nodeType === Node.TEXT_NODE) { container = container.parentElement; } // 向上遍历DOM树,检查是否在侧边栏内 let element = container; while (element && element !== document.body) { if (element.id === `${STYLE_PREFIX}sidebar`) { // 选中的文本在侧边栏内,不显示颜色选择器 menuOperationInProgress = false; return; } element = element.parentElement; } const menu = createHighlightMenu(true); 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); attachOutsideClose(menu); setTimeout(() => { ignoreNextClick = false; menuOperationInProgress = false; }, 100); } // 注册事件 function registerEvents() { document.addEventListener('mouseup', function (e) { if (!isHighlightEnabled) { 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 applyHighlightOptimized(highlight) { // 检查是否已经有相同ID的高亮存在 const existingHighlight = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlight.id}"]`); if (existingHighlight) { return true; } // 第一层:XPath + 偏移量快速定位(90%情况适用) if (tryXPathMatch(highlight)) { highlight.failedCount = 0; // 重置失败计数 return true; } // 第二层:上下文哈希快速匹配(8%情况适用) if (tryContextHashMatch(highlight)) { highlight.failedCount = 0; return true; } // 失败处理:标记为失效,不进行复杂匹配 markHighlightAsFailed(highlight); return false; } // 第一层匹配:XPath + 偏移量(增强:使用容器指纹验证) function tryXPathMatch(highlight) { try { // ★ 优先尝试使用容器 XPath(如果存在) if (highlight.containerXPath && highlight.containerOffset !== undefined) { const restored = tryContainerXPathMatch(highlight); if (restored) { return true; } } // 降级:使用传统 XPath 方法 if (!highlight.xpath || highlight.textOffset === undefined) { return false; } // 使用XPath查找元素 const result = document.evaluate( highlight.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); const targetNode = result.singleNodeValue; if (!targetNode) { return false; } // 如果XPath直接指向文本节点,直接使用它 if (targetNode.nodeType === Node.TEXT_NODE) { const text = targetNode.textContent; if (text.substring(highlight.textOffset, highlight.textOffset + highlight.textLength) === highlight.text) { return createHighlightAt(targetNode, highlight.textOffset, highlight); } return false; } // 否则,收集元素下的所有文本节点 const targetElement = targetNode; const textNodes = []; const walker = document.createTreeWalker( targetElement, NodeFilter.SHOW_TEXT, null ); let node; while (node = walker.nextNode()) { textNodes.push(node); } // 构建完整文本和节点位置映射 let fullText = ''; const nodeMap = []; // 记录每个字符对应的节点和节点内偏移 for (let textNode of textNodes) { const nodeText = textNode.textContent; const startPos = fullText.length; for (let i = 0; i < nodeText.length; i++) { nodeMap.push({ node: textNode, offset: i }); } fullText += nodeText; } // 检查文本是否匹配 const extractedText = fullText.substring( highlight.textOffset, highlight.textOffset + highlight.textLength ); if (extractedText === highlight.text) { // 确定起始和结束节点 const startInfo = nodeMap[highlight.textOffset]; const endInfo = nodeMap[highlight.textOffset + highlight.textLength - 1]; if (!startInfo || !endInfo) { return false; } // 创建跨节点的高亮范围 return createHighlightAtRange( startInfo.node, startInfo.offset, endInfo.node, endInfo.offset + 1, highlight ); } return false; } catch (e) { console.warn('XPath匹配失败:', e); return false; } } // ★ 新增:使用容器 XPath 和容器指纹进行匹配 function tryContainerXPathMatch(highlight) { try { // 使用容器 XPath 查找容器 const result = document.evaluate( highlight.containerXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); const container = result.singleNodeValue; if (!container) { return false; } // ★ 验证容器指纹(如果存在) if (highlight.containerFingerprint) { const currentFingerprint = generateContainerFingerprint(container); // 验证标签名 if (currentFingerprint.tagName !== highlight.containerFingerprint.tagName) { console.warn('容器标签不匹配:', currentFingerprint.tagName, '!=', highlight.containerFingerprint.tagName); return false; } // 验证长度(允许10%的差异) const lengthDiff = Math.abs(currentFingerprint.textLength - highlight.containerFingerprint.textLength); const lengthThreshold = highlight.containerFingerprint.textLength * 0.1; if (lengthDiff > lengthThreshold) { console.warn('容器长度差异过大:', lengthDiff, '>', lengthThreshold); return false; } // 验证前缀后缀(至少有50%相似) const prefixMatch = highlight.containerFingerprint.prefix.substring(0, 50); const suffixMatch = highlight.containerFingerprint.suffix.substring(0, 50); if (!currentFingerprint.prefix.includes(prefixMatch) && !currentFingerprint.suffix.includes(suffixMatch)) { console.warn('容器指纹不匹配'); return false; } } // 提取容器的完整文本 const containerText = container.textContent || ''; // 使用容器内偏移提取目标文本 const extractedText = containerText.substring( highlight.containerOffset, highlight.containerOffset + highlight.textLength ); // 验证文本是否匹配 if (extractedText !== highlight.text) { return false; } // 找到容器内对应的文本节点和偏移 const walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, null ); let totalOffset = 0; let startNode = null, startOffset = 0; let endNode = null, endOffset = 0; const targetEnd = highlight.containerOffset + highlight.textLength; let node; while (node = walker.nextNode()) { const nodeLength = node.textContent.length; const nodeStart = totalOffset; const nodeEnd = totalOffset + nodeLength; // 找起始节点:高亮起始位置在当前节点范围内 if (startNode === null && nodeStart <= highlight.containerOffset && highlight.containerOffset < nodeEnd) { startNode = node; startOffset = highlight.containerOffset - nodeStart; } // 找结束节点:高亮结束位置在当前节点范围内或之前 // ★ 修复:只有当 targetEnd 真正在当前节点内时才设置 endNode if (endNode === null && nodeStart < targetEnd && targetEnd <= nodeEnd) { endNode = node; endOffset = targetEnd - nodeStart; } totalOffset = nodeEnd; // ★ 修复:只有确认找到正确的结束节点后才停止 // 如果 targetEnd 超出当前节点,继续遍历 if (startNode && endNode) { break; } } // ★ 修复:如果遍历完都没找到 endNode,可能 targetEnd 超出了容器范围 // 此时使用最后一个节点作为 endNode if (startNode && !endNode && totalOffset > 0) { // 重新遍历找到最后一个节点 const lastWalker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null); let lastNode = null; while (lastNode = lastWalker.nextNode()) { endNode = lastNode; } if (endNode) { endOffset = endNode.textContent.length; } } if (!startNode || !endNode) { console.warn('容器内未找到起始或结束节点'); return false; } // 创建高亮 return createHighlightAtRange(startNode, startOffset, endNode, endOffset, highlight); } catch (e) { console.warn('容器XPath匹配失败:', e); return false; } } // 第二层匹配:上下文哈希快速匹配 function tryContextHashMatch(highlight) { try { if (!document.body || !highlight || !highlight.text) { return false; } const contextWindow = 50; // 上下文窗口大小 const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null ); const bufferNodes = []; let bufferText = ''; const resolveOffsetInBuffer = (targetIndex) => { let accumulated = 0; for (let i = 0; i < bufferNodes.length; i++) { const entry = bufferNodes[i]; const next = accumulated + entry.text.length; if (targetIndex < next) { return { node: entry.node, offset: targetIndex - accumulated }; } accumulated = next; } if (bufferNodes.length) { const lastEntry = bufferNodes[bufferNodes.length - 1]; if (targetIndex === accumulated) { return { node: lastEntry.node, offset: lastEntry.text.length }; } } return null; }; let node; while (node = walker.nextNode()) { if (!node || !node.textContent) { continue; } // 跳过已经高亮的节点 if (node.parentElement && node.parentElement.closest(`.${STYLE_PREFIX}highlight-marked`)) { continue; } const textContent = node.textContent; if (!textContent.length) { continue; } bufferNodes.push({ node, text: textContent }); bufferText += textContent; const maxLength = highlight.text.length + contextWindow * 2; while (bufferNodes.length && bufferText.length > maxLength) { const removed = bufferNodes.shift(); bufferText = bufferText.slice(removed.text.length); } let searchFrom = 0; while (true) { const idx = bufferText.indexOf(highlight.text, searchFrom); if (idx === -1) { break; } const prefixStart = Math.max(0, idx - contextWindow); const suffixEnd = Math.min(bufferText.length, idx + highlight.text.length + contextWindow); const prefix = bufferText.substring(prefixStart, idx); const suffix = bufferText.substring(idx + highlight.text.length, suffixEnd); const currentHash = generateContextHash(prefix, suffix, highlight.text); if (!highlight.contextHash || currentHash === highlight.contextHash) { const startInfo = resolveOffsetInBuffer(idx); const endInfo = resolveOffsetInBuffer(idx + highlight.text.length); if (startInfo && endInfo && startInfo.node && endInfo.node) { try { const range = document.createRange(); range.setStart(startInfo.node, startInfo.offset); range.setEnd(endInfo.node, endInfo.offset); wrapRangeWithHighlight(range, highlight.id, highlight.color); return true; } catch (rangeError) { console.warn('范围创建失败:', rangeError); } } } searchFrom = idx + 1; } } return false; } catch (e) { console.warn('上下文哈希匹配失败:', e); return false; } } // 将选区包装为高亮元素的通用方法 function wrapRangeWithHighlight(range, highlightId, color) { // 检查是否需要分段处理 const commonAncestor = range.commonAncestorContainer; const startContainer = range.startContainer; const endContainer = range.endContainer; const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE']; // 判断是否包含块级元素 let containsBlockElement = false; if (commonAncestor.nodeType === Node.ELEMENT_NODE) { containsBlockElement = blockTags.includes(commonAncestor.tagName) || !!commonAncestor.querySelector(blockTags.map(tag => tag.toLowerCase()).join(',')); } // ★ 修复:检查是否跨越多个文本节点(即使不跨块级元素) const spanMultipleNodes = startContainer !== endContainer; // 如果是简单情况(单个文本节点内,不跨块级元素),使用简单逻辑 if (!containsBlockElement && !spanMultipleNodes) { // 简单情况:单个文本节点内的高亮 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); } catch (e) { console.warn('简单高亮创建失败:', e); return null; } highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); removeHighlightMenu(); setTimeout(() => { showHighlightEditMenu(e, highlightId); }, 10); }); return highlightElement; } else { // 复杂情况:跨越多个节点或跨块级元素,需要分段处理 const highlightElements = []; try { // 获取选区内的所有节点 const startOffset = range.startOffset; const endOffset = range.endOffset; // 创建树遍历器 const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_TEXT, null // 简化处理,遍历所有文本节点 ); let node; let isInRange = false; let foundEnd = false; while (node = walker.nextNode()) { // 跳过已高亮的节点 if (node.parentElement && node.parentElement.classList.contains(`${STYLE_PREFIX}highlight-marked`)) { continue; } let nodeStartOffset = 0; let nodeEndOffset = node.textContent.length; let shouldHighlight = false; // 如果是起始节点,调整偏移 if (node === startContainer) { nodeStartOffset = startOffset; isInRange = true; shouldHighlight = true; } else if (node === endContainer) { // 如果是结束节点,调整偏移 nodeEndOffset = endOffset; shouldHighlight = true; foundEnd = true; } else if (isInRange && !foundEnd) { // 在范围内的中间节点 shouldHighlight = true; } // 创建单个文本节点的高亮 if (shouldHighlight && nodeEndOffset > nodeStartOffset) { const nodeRange = document.createRange(); nodeRange.setStart(node, nodeStartOffset); nodeRange.setEnd(node, nodeEndOffset); const span = document.createElement('span'); span.className = `${STYLE_PREFIX}highlight-marked`; span.dataset.highlightId = highlightId; span.style.backgroundColor = color; try { nodeRange.surroundContents(span); span.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); removeHighlightMenu(); setTimeout(() => { showHighlightEditMenu(e, highlightId); }, 10); }); highlightElements.push(span); } catch (e) { // 如果 surroundContents 失败,尝试其他方法 const extracted = nodeRange.extractContents(); span.appendChild(extracted); nodeRange.insertNode(span); span.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); removeHighlightMenu(); setTimeout(() => { showHighlightEditMenu(e, highlightId); }, 10); }); highlightElements.push(span); } } // 如果已经处理了结束节点,停止遍历 if (foundEnd) { break; } } return highlightElements[0]; // 返回第一个高亮元素 } catch (e) { console.error('分段高亮失败:', e); // 降级处理:使用原始方法 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); } catch (extractError) { console.error('降级处理也失败:', extractError); return null; } highlightElement.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); removeHighlightMenu(); setTimeout(() => { showHighlightEditMenu(e, highlightId); }, 10); }); return highlightElement; } } } // 在指定位置创建高亮 function createHighlightAt(textNode, offset, highlight) { try { const range = document.createRange(); range.setStart(textNode, offset); range.setEnd(textNode, offset + highlight.text.length); wrapRangeWithHighlight(range, highlight.id, highlight.color); return true; } catch (e) { console.warn('创建高亮失败:', e); return false; } } // 在跨节点范围创建高亮(支持包含等内嵌元素的文本) function createHighlightAtRange(startNode, startOffset, endNode, endOffset, highlight) { try { const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); wrapRangeWithHighlight(range, highlight.id, highlight.color); return true; } catch (e) { console.warn('创建跨节点高亮失败:', e); return false; } } // 标记高亮为失效 function markHighlightAsFailed(highlight) { highlight.failedCount = (highlight.failedCount || 0) + 1; highlight.lastFailedTime = Date.now(); console.warn('高亮失效:', highlight.text, '失败次数:', highlight.failedCount); saveHighlights(); } // 创建失效高亮管理控件 function createFailedHighlightControls() { const container = document.createElement('div'); container.className = `${STYLE_PREFIX}failed-controls`; Object.assign(container.style, { padding: '8px', borderBottom: '1px solid rgba(255,255,255,0.1)', marginBottom: '8px', flex: '0 0 auto' }); // 统计失效高亮数量(只统计主记录,不统计片段) const mainHighlights = highlights.filter(h => !h.isFragment); const failedCount = mainHighlights.filter(h => h.failedCount && h.failedCount >= 3).length; if (failedCount === 0) { container.style.display = 'none'; return container; } container.innerHTML = `暂无高亮内容
选中文本并点击颜色进行高亮
该域名下暂无高亮内容
`; listContainer.appendChild(emptyState); } else { // 按URL分组显示 urlEntries.forEach(([url, highlightArray]) => { const urlGroup = createUrlGroup(url, highlightArray); listContainer.appendChild(urlGroup); }); } domainHighlightsListContainer.appendChild(listContainer); } // 更新函数:同时更新本页和全站列表 updateSidebarHighlights = function() { renderCurrentPageHighlights(); renderDomainHighlights(); }; // 创建URL分组 function createUrlGroup(url, highlightArray) { const isCurrentPage = url === currentPageUrl; const mainHighlights = highlightArray.filter(h => !h.isFragment); const sortedHighlights = [...mainHighlights].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // 获取URL的路径部分用于显示 let displayPath = ''; try { const urlObj = new URL(url); displayPath = urlObj.pathname + urlObj.search + urlObj.hash; if (displayPath.length > 50) { displayPath = displayPath.substring(0, 47) + '...'; } } catch (e) { displayPath = url; } // 创建分组容器 const groupContainer = document.createElement('div'); groupContainer.className = `${STYLE_PREFIX}url-group`; Object.assign(groupContainer.style, { marginBottom: '12px', border: '1px solid rgba(255, 255, 255, 0.06)', borderRadius: '8px', overflow: 'hidden', background: 'rgba(38, 42, 51, 0.3)' }); // 创建分组头部 const groupHeader = document.createElement('div'); groupHeader.className = `${STYLE_PREFIX}url-group-header`; Object.assign(groupHeader.style, { padding: '10px 12px', background: isCurrentPage ? 'rgba(59, 165, 216, 0.1)' : 'rgba(46, 48, 58, 0.5)', borderBottom: '1px solid rgba(255, 255, 255, 0.06)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', transition: 'background 0.2s' }); // URL图标 const urlIcon = document.createElement('span'); urlIcon.innerHTML = isCurrentPage ? '📄' : '🔗'; urlIcon.style.fontSize = '14px'; // URL路径文本 const urlText = document.createElement('div'); Object.assign(urlText.style, { flex: '1', fontSize: '12px', color: isCurrentPage ? '#3BA5D8' : '#E8E9EB', fontWeight: isCurrentPage ? '600' : '400', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }); urlText.textContent = displayPath; urlText.title = url; // 完整URL作为tooltip // 当前页面标签 if (isCurrentPage) { const currentTag = document.createElement('span'); Object.assign(currentTag.style, { padding: '2px 8px', background: 'rgba(59, 165, 216, 0.2)', border: '1px solid rgba(59, 165, 216, 0.3)', borderRadius: '4px', fontSize: '11px', color: '#3BA5D8', fontWeight: '600' }); currentTag.textContent = '当前'; groupHeader.appendChild(currentTag); } // 高亮数量 const countBadge = document.createElement('span'); Object.assign(countBadge.style, { padding: '2px 8px', background: 'rgba(112, 211, 130, 0.15)', border: '1px solid rgba(112, 211, 130, 0.3)', borderRadius: '4px', fontSize: '11px', color: '#70d382', fontWeight: '600' }); countBadge.textContent = mainHighlights.length; // 展开/折叠图标 const toggleIcon = document.createElement('span'); toggleIcon.innerHTML = '▼'; Object.assign(toggleIcon.style, { fontSize: '10px', color: '#999', transition: 'transform 0.2s' }); groupHeader.appendChild(urlIcon); groupHeader.appendChild(urlText); groupHeader.appendChild(countBadge); groupHeader.appendChild(toggleIcon); // 创建高亮列表容器 const highlightsContainer = document.createElement('div'); highlightsContainer.className = `${STYLE_PREFIX}url-group-highlights`; Object.assign(highlightsContainer.style, { display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px', maxHeight: '400px', overflowY: 'auto' }); // 渲染该URL的所有高亮 sortedHighlights.forEach((highlight, index) => { const highlightItem = createHighlightItem(highlight, index, !isCurrentPage, url); highlightsContainer.appendChild(highlightItem); }); // 展开/折叠功能 let isExpanded = isCurrentPage; // 当前页面默认展开 if (!isExpanded) { highlightsContainer.style.display = 'none'; toggleIcon.style.transform = 'rotate(-90deg)'; } groupHeader.addEventListener('click', () => { isExpanded = !isExpanded; highlightsContainer.style.display = isExpanded ? 'flex' : 'none'; toggleIcon.style.transform = isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)'; }); // 悬停效果 groupHeader.addEventListener('mouseenter', () => { groupHeader.style.background = isCurrentPage ? 'rgba(59, 165, 216, 0.15)' : 'rgba(46, 48, 58, 0.7)'; }); groupHeader.addEventListener('mouseleave', () => { groupHeader.style.background = isCurrentPage ? 'rgba(59, 165, 216, 0.1)' : 'rgba(46, 48, 58, 0.5)'; }); groupContainer.appendChild(groupHeader); groupContainer.appendChild(highlightsContainer); return groupContainer; } // 创建单个高亮项目 function createHighlightItem(highlight, index, isOtherPage = false, pageUrl = '') { const item = document.createElement('div'); item.className = `${STYLE_PREFIX}highlight-item`; item.dataset.highlightId = highlight.id; Object.assign(item.style, { background: isOtherPage ? 'linear-gradient(135deg, rgba(78, 89, 108, 0.15) 0%, rgba(46, 48, 58, 0.12) 100%)' : 'linear-gradient(135deg, rgba(78, 89, 108, 0.22) 0%, rgba(46, 48, 58, 0.18) 100%)', border: isOtherPage ? '1px solid rgba(255, 255, 255, 0.05)' : '1px solid rgba(255, 255, 255, 0.08)', borderRadius: '10px', padding: '14px', margin: '0', position: 'relative', transition: 'all 0.18s cubic-bezier(0.4, 0, 0.2, 1)', cursor: 'pointer', overflow: 'hidden', flex: '0 0 auto', backdropFilter: 'blur(8px)', boxShadow: '0 10px 24px rgba(7, 12, 24, 0.28)', opacity: isOtherPage ? '0.85' : '1' }); // 高亮内容 const content = document.createElement('div'); Object.assign(content.style, { color: '#E8E9EB', fontSize: '13.5px', lineHeight: '1.6', marginBottom: '10px', wordBreak: 'break-word', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: '3', overflow: 'hidden', fontWeight: '400', }); // 处理高亮文本,避免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', }); // 时间信息区域(含发光色点) const timeContainer = document.createElement('div'); Object.assign(timeContainer.style, { display: 'flex', alignItems: 'center', gap: '6px', }); // 发光色点 const colorDot = document.createElement('div'); Object.assign(colorDot.style, { width: '6px', height: '6px', borderRadius: '50%', background: highlight.color, boxShadow: `0 0 8px ${highlight.color}80`, }); // 时间信息 const timeInfo = document.createElement('div'); Object.assign(timeInfo.style, { color: '#6B7280', fontSize: '11.5px', fontWeight: '500', }); // 格式化时间 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; timeContainer.appendChild(colorDot); timeContainer.appendChild(timeInfo); // 失效标识 if (highlight.failedCount && highlight.failedCount >= 3) { const failedBadge = document.createElement('span'); Object.assign(failedBadge.style, { background: 'linear-gradient(135deg, rgba(255, 165, 0, 0.15) 0%, rgba(255, 140, 0, 0.08) 100%)', border: '1px solid rgba(255, 165, 0, 0.25)', borderRadius: '4px', padding: '2px 6px', color: '#FFA500', fontSize: '10.5px', fontWeight: '600', marginLeft: '8px', display: 'inline-flex', alignItems: 'center', gap: '3px', backdropFilter: 'blur(4px)', boxShadow: '0 0 8px rgba(255, 165, 0, 0.15)', letterSpacing: '0.3px', }); // 使用SVG图标替代emoji failedBadge.innerHTML = ` 失效 `; failedBadge.title = `恢复失败 ${highlight.failedCount} 次`; timeContainer.appendChild(failedBadge); } // 操作按钮容器(默认隐藏,悬停时显示) const actionButtons = document.createElement('div'); actionButtons.className = `${STYLE_PREFIX}card-actions`; Object.assign(actionButtons.style, { display: 'flex', gap: '6px', opacity: '0', transition: 'opacity 0.2s', }); // 跳转按钮 const jumpButton = document.createElement('button'); const jumpDefaultBackground = 'transparent'; const jumpDefaultBorder = 'transparent'; const jumpHoverBackground = 'rgba(118, 196, 255, 0.22)'; const jumpHoverBorder = 'rgba(118, 196, 255, 0.45)'; Object.assign(jumpButton.style, { width: '25px', height: '25px', background: 'transparent', border: '1px solid transparent', borderRadius: '50%', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#8ed0ff', transition: 'all 0.18s ease', boxShadow: 'none', backdropFilter: 'blur(0)' }); jumpButton.setAttribute('aria-label', '定位到原文'); jumpButton.innerHTML = ` `; const activateJumpVisual = () => { jumpButton.style.background = jumpHoverBackground; jumpButton.style.borderColor = jumpHoverBorder; jumpButton.style.boxShadow = '0 10px 20px rgba(12, 23, 42, 0.35)'; jumpButton.style.transform = 'translateY(-1px) scale(1.08)'; jumpButton.style.color = '#e5f5ff'; }; const resetJumpVisual = () => { jumpButton.style.background = jumpDefaultBackground; jumpButton.style.borderColor = jumpDefaultBorder; jumpButton.style.boxShadow = 'none'; jumpButton.style.transform = 'none'; jumpButton.style.color = '#8ed0ff'; }; jumpButton.addEventListener('mouseenter', activateJumpVisual); jumpButton.addEventListener('focus', activateJumpVisual); jumpButton.addEventListener('mouseleave', resetJumpVisual); jumpButton.addEventListener('blur', resetJumpVisual); jumpButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (isOtherPage && pageUrl) { // 其他页面的高亮,提示跳转 if (confirm(`该高亮位于其他页面,是否跳转?\n\n${pageUrl}`)) { window.open(pageUrl, '_blank'); } } else { // 当前页面的高亮,直接滚动 scrollToHighlight(highlight.id); } }); // 删除按钮 const deleteButton = document.createElement('button'); const deleteDefaultBackground = 'transparent'; const deleteDefaultBorder = 'transparent'; const deleteHoverBackground = 'rgba(255, 126, 126, 0.24)'; const deleteHoverBorder = 'rgba(255, 172, 172, 0.48)'; Object.assign(deleteButton.style, { width: '25px', height: '25px', background: deleteDefaultBackground, border: `1px solid ${deleteDefaultBorder}`, borderRadius: '50%', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff9f9f', transition: 'all 0.18s ease', boxShadow: 'none', backdropFilter: 'blur(0)' }); deleteButton.setAttribute('aria-label', '删除高亮'); deleteButton.innerHTML = ` `; const activateDeleteVisual = () => { deleteButton.style.background = deleteHoverBackground; deleteButton.style.borderColor = deleteHoverBorder; deleteButton.style.boxShadow = '0 10px 20px rgba(32, 12, 18, 0.4)'; deleteButton.style.transform = 'translateY(-1px) scale(1.08)'; deleteButton.style.color = '#ffe3e3'; }; const resetDeleteVisual = () => { deleteButton.style.background = deleteDefaultBackground; deleteButton.style.borderColor = deleteDefaultBorder; deleteButton.style.boxShadow = 'none'; deleteButton.style.transform = 'none'; deleteButton.style.color = '#ff9f9f'; }; deleteButton.addEventListener('mouseenter', activateDeleteVisual); deleteButton.addEventListener('focus', activateDeleteVisual); deleteButton.addEventListener('mouseleave', resetDeleteVisual); deleteButton.addEventListener('blur', resetDeleteVisual); deleteButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (confirm('确定要删除这条高亮吗?')) { if (isOtherPage && pageUrl) { // 删除其他页面的高亮 const allHighlights = GM_getValue('highlights', {}); if (allHighlights[pageUrl]) { // 从该URL的高亮数组中删除 allHighlights[pageUrl] = allHighlights[pageUrl].filter(h => h.id !== highlight.id && h.parentId !== highlight.id ); // 如果数组为空,删除该URL的键 if (allHighlights[pageUrl].length === 0) { delete allHighlights[pageUrl]; } GM_setValue('highlights', allHighlights); } } else { // 删除当前页面的高亮 removeHighlightById(highlight.id); } updateSidebarHighlights(); } }); actionButtons.appendChild(jumpButton); actionButtons.appendChild(deleteButton); infoBar.appendChild(timeContainer); infoBar.appendChild(actionButtons); // 添加卡片悬停效果 const defaultBackground = item.style.background; const defaultBorder = item.style.borderColor; const defaultShadow = item.style.boxShadow; item.addEventListener('mouseenter', () => { item.style.background = 'linear-gradient(135deg, rgba(94, 139, 194, 0.28) 0%, rgba(63, 83, 120, 0.24) 100%)'; item.style.borderColor = 'rgba(124, 189, 255, 0.45)'; item.style.transform = 'translateY(-2px)'; item.style.boxShadow = '0 16px 32px rgba(7, 12, 24, 0.38)'; actionButtons.style.opacity = '1'; }); item.addEventListener('mouseleave', () => { item.style.background = defaultBackground; item.style.borderColor = defaultBorder; item.style.transform = 'none'; item.style.boxShadow = defaultShadow; actionButtons.style.opacity = '0'; }); item.appendChild(content); item.appendChild(infoBar); return item; } // 滚动到指定高亮 function scrollToHighlight(highlightId) { // 先尝试查找主ID的元素 let highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`); // 如果没找到主ID元素,检查是否为多片段高亮 if (!highlightElement) { const mainHighlight = highlights.find(h => h.id === highlightId); if (mainHighlight && mainHighlight.isMultiFragment) { // 查找第一个片段的元素 const firstFragment = highlights.find(h => h.parentId === highlightId); if (firstFragment) { highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${firstFragment.id}"]`); } } } if (highlightElement) { // 平滑滚动到元素 highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 获取所有相关的高亮元素(包括多片段) const mainHighlight = highlights.find(h => h.id === highlightId); let allElements = [highlightElement]; // 如果是多片段高亮,获取所有片段元素 if (mainHighlight && mainHighlight.isMultiFragment) { const fragments = highlights.filter(h => h.parentId === highlightId); fragments.forEach(fragment => { const fragmentElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${fragment.id}"]`); if (fragmentElement && !allElements.includes(fragmentElement)) { allElements.push(fragmentElement); } }); } // 为所有相关元素添加闪烁效果 allElements.forEach(element => { element.classList.add(`${STYLE_PREFIX}highlight-flash`); const originalTransition = element.style.transition; element.style.transition = 'all 0.3s ease'; setTimeout(() => { element.classList.remove(`${STYLE_PREFIX}highlight-flash`); element.style.transition = originalTransition; }, 2500); }); } } // 初始渲染高亮列表 renderCurrentPageHighlights(); renderDomainHighlights(); // 渲染启用管理面板内容 function renderEnabledPanel() { const container = sidebar.querySelector(`.${STYLE_PREFIX}disabled-container`); if (!container) return; // 清空容器 container.innerHTML = ''; const theme = { cardBackground: 'linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)', cardBorder: '1px solid rgba(255, 255, 255, 0.08)' }; Object.assign(container.style, { color: '#E8E9EB', display: 'flex', flexDirection: 'column', gap: '20px', padding: '18px', flex: '1', overflowY: 'auto', boxSizing: 'border-box' }); // 添加当前页面管理区域 const currentPageSection = document.createElement('div'); currentPageSection.className = `${STYLE_PREFIX}disabled-section`; Object.assign(currentPageSection.style, { margin: '0', padding: '16px', background: theme.cardBackground, borderRadius: '10px', border: theme.cardBorder, boxShadow: '0 12px 32px rgba(0, 0, 0, 0.22)', display: 'flex', flexDirection: 'column', gap: '12px' }); const currentPageTitle = document.createElement('div'); currentPageTitle.className = `${STYLE_PREFIX}disabled-title`; currentPageTitle.innerHTML = `当前页面`; Object.assign(currentPageTitle.style, { fontSize: '13px', fontWeight: '600', color: '#E8E9EB', marginBottom: '0', 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, { margin: '0', padding: '16px', background: theme.cardBackground, borderRadius: '10px', border: theme.cardBorder, boxShadow: '0 10px 28px rgba(0, 0, 0, 0.2)', display: 'flex', flexDirection: 'column', gap: '12px' }); const domainsTitle = document.createElement('div'); domainsTitle.className = `${STYLE_PREFIX}disabled-title`; domainsTitle.innerHTML = `启用域名列表`; Object.assign(domainsTitle.style, { fontSize: '13px', fontWeight: '600', color: '#E8E9EB', marginBottom: '0', display: 'flex', alignItems: 'center', gap: '8px' }); const domainsList = document.createElement('div'); domainsList.className = `${STYLE_PREFIX}domains-list`; domainsList.innerHTML = renderEnabledDomains(); Object.assign(domainsList.style, { display: 'flex', flexDirection: 'column', gap: '10px' }); // 添加域名表单 const addDomainForm = document.createElement('div'); addDomainForm.className = `${STYLE_PREFIX}add-disabled-form`; Object.assign(addDomainForm.style, { display: 'flex', marginTop: '0', gap: '0', borderRadius: '10px', overflow: 'hidden', border: theme.cardBorder, background: 'rgba(12, 14, 18, 0.8)', boxShadow: '0 10px 28px rgba(0, 0, 0, 0.18)' }); 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', background: 'transparent', border: 'none', borderRight: '1px solid rgba(255, 255, 255, 0.08)', padding: '12px 14px', fontSize: '13px', color: '#E8E9EB', outline: 'none', transition: 'background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease' }); const addDomainBtn = document.createElement('button'); addDomainBtn.className = `${STYLE_PREFIX}add-disabled-button`; addDomainBtn.id = 'add-domain-btn'; addDomainBtn.textContent = '添加'; Object.assign(addDomainBtn.style, { background: 'linear-gradient(135deg, rgba(78, 168, 222, 0.2) 0%, rgba(78, 168, 222, 0.12) 100%)', color: '#E4F3FF', border: 'none', borderRadius: '0', padding: '12px 18px', fontSize: '13px', fontWeight: '600', cursor: 'pointer', transition: 'transform 0.2s ease, background 0.2s ease, box-shadow 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`; Object.assign(urlsSection.style, { margin: '0', padding: '16px', background: theme.cardBackground, borderRadius: '10px', border: theme.cardBorder, boxShadow: '0 10px 28px rgba(0, 0, 0, 0.2)', display: 'flex', flexDirection: 'column', gap: '12px' }); const urlsTitle = document.createElement('div'); urlsTitle.className = `${STYLE_PREFIX}disabled-title`; urlsTitle.innerHTML = `启用网址列表`; Object.assign(urlsTitle.style, { fontSize: '13px', fontWeight: '600', color: '#E8E9EB', marginBottom: '0', display: 'flex', alignItems: 'center', gap: '8px' }); const urlsList = document.createElement('div'); urlsList.className = `${STYLE_PREFIX}urls-list`; urlsList.innerHTML = renderEnabledUrls(); Object.assign(urlsList.style, { display: 'flex', flexDirection: 'column', gap: '10px' }); // 添加URL表单 const addUrlForm = document.createElement('div'); addUrlForm.className = `${STYLE_PREFIX}add-disabled-form`; Object.assign(addUrlForm.style, { display: 'flex', marginTop: '0', gap: '0', borderRadius: '10px', overflow: 'hidden', border: theme.cardBorder, background: 'rgba(12, 14, 18, 0.8)', boxShadow: '0 10px 28px rgba(0, 0, 0, 0.18)' }); 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', background: 'transparent', border: 'none', borderRight: '1px solid rgba(255, 255, 255, 0.08)', padding: '12px 14px', fontSize: '13px', color: '#E8E9EB', outline: 'none', transition: 'background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease' }); const addUrlBtn = document.createElement('button'); addUrlBtn.className = `${STYLE_PREFIX}add-disabled-button`; addUrlBtn.id = 'add-url-btn'; addUrlBtn.textContent = '添加'; Object.assign(addUrlBtn.style, { background: 'linear-gradient(135deg, rgba(78, 168, 222, 0.2) 0%, rgba(78, 168, 222, 0.12) 100%)', color: '#E4F3FF', border: 'none', borderRadius: '0', padding: '12px 18px', fontSize: '13px', fontWeight: '600', cursor: 'pointer', transition: 'transform 0.2s ease, background 0.2s ease, box-shadow 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 isDomainEnabled = enabledList.domains.includes(activationDomain); const isUrlEnabled = enabledList.urls.includes(activationPageUrl); if (isDomainEnabled || isUrlEnabled) { return `