网页划词高亮工具

提供网页划词高亮功能

  1. // ==UserScript==
  2. // @name 网页划词高亮工具
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.2
  5. // @description 提供网页划词高亮功能
  6. // @author sunny43
  7. // @license MIT
  8. // @match *://*/*
  9. // @grant GM_addStyle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const STYLE_PREFIX = 'sunny43-';
  19.  
  20. // 全局变量
  21. let highlights = [];
  22. let currentPageUrl = window.location.href;
  23. let currentDomain = window.location.hostname;
  24. let settings = GM_getValue('highlight_settings', {
  25. colors: ['#ff909c', '#b89fff', '#74b4ff', '#70d382', '#ffcb7e'],
  26. activeColor: '#ff909c',
  27. minTextLength: 1,
  28. enableFuzzyMatch: true,
  29. maxContextDistance: 50,
  30. sidebarDescription: '高亮工具',
  31. sidebarWidth: 320,
  32. showFloatingButton: true
  33. });
  34. let savedRange = null; // 保存选区范围
  35. let ignoreNextClick = false; // 忽略下一次点击的标志
  36. let menuDisplayTimer = null; // 菜单显示定时器
  37. let menuOperationInProgress = false; // 添加菜单操作锁定
  38. // 启用列表
  39. let enabledList = GM_getValue('enabled_list', {
  40. domains: [],
  41. urls: []
  42. });
  43. // 检查当前页面是否启用高亮功能
  44. let isHighlightEnabled = enabledList.domains.includes(currentDomain) ||
  45. enabledList.urls.includes(currentPageUrl);
  46. let updateSidebarHighlights = null;
  47.  
  48. GM_addStyle(`
  49. /* 高亮菜单样式 */
  50. .${STYLE_PREFIX}highlight-menu {
  51. position: absolute;
  52. background: #333336;
  53. border: none;
  54. border-radius: 24px;
  55. box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  56. padding: 10px 8px;
  57. z-index: 9999;
  58. display: flex;
  59. flex-direction: row;
  60. align-items: center;
  61. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  62. color: #fff;
  63. opacity: 0;
  64. transition: opacity 0.2s ease-in;
  65. pointer-events: none;
  66. max-height: 40px; /* 添加最大高度限制 */
  67. height: auto; /* 确保高度自适应内容但不超过最大高度 */
  68. box-sizing: border-box; /* 确保padding不影响总高度 */
  69. }
  70. .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}show {
  71. opacity: 1;
  72. pointer-events: auto; /* 显示时响应事件 */
  73. }
  74.  
  75. /* 菜单箭头样式 */
  76. .${STYLE_PREFIX}highlight-menu::after {
  77. content: '';
  78. position: absolute;
  79. bottom: -6px;
  80. left: var(--arrow-left, 50%);
  81. width: 12px;
  82. height: 6px;
  83. background-color: #333336;
  84. clip-path: polygon(0 0, 100% 0, 50% 100%);
  85. margin-left: -6px;
  86. }
  87. .${STYLE_PREFIX}highlight-menu.${STYLE_PREFIX}arrow-top::after {
  88. top: -6px;
  89. bottom: auto;
  90. clip-path: polygon(0 100%, 100% 100%, 50% 0);
  91. }
  92.  
  93. /* 颜色选择区域 */
  94. .${STYLE_PREFIX}highlight-menu-colors {
  95. display: flex;
  96. flex-direction: row;
  97. align-items: center;
  98. margin: 0 2px;
  99. flex-wrap: nowrap;
  100. flex: 0 0 auto;
  101. }
  102.  
  103. /* 颜色选择按钮 */
  104. .${STYLE_PREFIX}highlight-menu-color {
  105. width: 22px;
  106. height: 22px;
  107. border-radius: 50%;
  108. margin: 0 3px;
  109. cursor: pointer;
  110. position: relative;
  111. display: flex;
  112. align-items: center;
  113. justify-content: center;
  114. transition: transform 0.15s ease;
  115. box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12);
  116. flex-shrink: 0;
  117. }
  118. .${STYLE_PREFIX}highlight-menu-color:hover {
  119. transform: scale(1.12);
  120. }
  121. .${STYLE_PREFIX}highlight-menu-color.${STYLE_PREFIX}active::after {
  122. content: "";
  123. width: 12px;
  124. height: 12px;
  125. 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");
  126. background-repeat: no-repeat;
  127. background-position: center;
  128. background-size: contain;
  129. }
  130.  
  131. /* 菜单按钮通用样式 */
  132. .${STYLE_PREFIX}highlight-menu-action {
  133. height: 22px;
  134. margin: 0 2px;
  135. cursor: pointer;
  136. padding: 0 10px;
  137. border-radius: 12px;
  138. color: #fff;
  139. font-size: 13px;
  140. background: rgba(255,255,255,0.1);
  141. border: none;
  142. transition: all 0.15s ease;
  143. white-space: nowrap;
  144. display: flex;
  145. align-items: center;
  146. justify-content: center;
  147. flex-shrink: 0;
  148. }
  149. .${STYLE_PREFIX}highlight-menu-action:hover {
  150. background: rgba(255,255,255,0.2);
  151. }
  152.  
  153. /* 删除按钮样式 */
  154. .${STYLE_PREFIX}highlight-action-delete {
  155. color: #f0f0f0;
  156. font-weight: 500;
  157. position: relative;
  158. overflow: hidden;
  159. transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
  160. margin-left: 3px;
  161. }
  162. .${STYLE_PREFIX}highlight-action-delete:hover {
  163. background: rgba(255,82,82,0.12);
  164. color: #ff6b6b;
  165. transform: translateY(-1px);
  166. box-shadow: 0 2px 8px rgba(255,82,82,0.25);
  167. }
  168. .${STYLE_PREFIX}highlight-action-delete:active {
  169. transform: translateY(0px);
  170. background: rgba(255,82,82,0.2);
  171. }
  172.  
  173. /* 闪烁效果用于高亮跳转 */
  174. @keyframes ${STYLE_PREFIX}highlightFlash {
  175. 0%, 100% { opacity: 1; }
  176. 50% { opacity: 0.3; }
  177. }
  178. .${STYLE_PREFIX}highlight-flash {
  179. animation: ${STYLE_PREFIX}highlightFlash 0.5s ease 4;
  180. box-shadow: 0 0 0 3px rgba(255, 255, 0, 0.7) !important;
  181. position: relative;
  182. z-index: 10;
  183. }
  184.  
  185. /* 浮动按钮样式 */
  186. #${STYLE_PREFIX}floating-button {
  187. position: fixed;
  188. bottom: 20px;
  189. right: 20px;
  190. z-index: 10000;
  191. width: 38px;
  192. height: 38px;
  193. border: none;
  194. border-radius: 6px;
  195. cursor: pointer;
  196. background-color: #262A33;
  197. color: #E8E9EB;
  198. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  199. transition: all 0.2s ease;
  200. display: flex;
  201. align-items: center;
  202. justify-content: center;
  203. }
  204. #${STYLE_PREFIX}floating-button:hover {
  205. background-color: #3BA5D8;
  206. box-shadow: 0 4px 12px rgba(59, 165, 216, 0.25);
  207. }
  208. #${STYLE_PREFIX}floating-button:active {
  209. transform: translateY(0px);
  210. }
  211.  
  212. /* 侧边栏样式 */
  213. #${STYLE_PREFIX}sidebar {
  214. position: fixed;
  215. top: 0;
  216. right: -280px;
  217. width: 280px;
  218. height: 100%;
  219. background: linear-gradient(135deg, #262A33 0%, #1A1D24 100%);
  220. box-shadow: -1px 0 5px rgba(0, 0, 0, 0.15);
  221. transition: right 0.3s ease;
  222. z-index: 9999;
  223. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  224. }
  225. `);
  226.  
  227. // 保存启用列表
  228. function saveEnabledList() {
  229. GM_setValue('enabled_list', enabledList);
  230. // 刷新当前状态
  231. isHighlightEnabled = enabledList.domains.includes(currentDomain) ||
  232. enabledList.urls.includes(currentPageUrl);
  233.  
  234. // 更新浮动按钮显示状态
  235. const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`);
  236. if (floatingButton) {
  237. floatingButton.style.display = (settings.showFloatingButton && isHighlightEnabled) ? 'flex' : 'none';
  238. }
  239. }
  240.  
  241. // 启用域名
  242. function enableDomain(domain) {
  243. if (!enabledList.domains.includes(domain)) {
  244. enabledList.domains.push(domain);
  245. saveEnabledList();
  246. }
  247. }
  248.  
  249. // 启用域名
  250. function disableDomain(domain) {
  251. enabledList.domains = enabledList.domains.filter(d => d !== domain);
  252. saveEnabledList();
  253. }
  254.  
  255. // 启用URL
  256. function enableUrl(url) {
  257. if (!enabledList.urls.includes(url)) {
  258. enabledList.urls.push(url);
  259. saveEnabledList();
  260. }
  261. }
  262.  
  263. // 启用URL
  264. function disableUrl(url) {
  265. enabledList.urls = enabledList.urls.filter(u => u !== url);
  266. saveEnabledList();
  267. }
  268.  
  269. // 加载当前页面的高亮
  270. function loadHighlights() {
  271. const allHighlights = GM_getValue('highlights', {});
  272. highlights = allHighlights[currentPageUrl] || [];
  273. return highlights;
  274. }
  275.  
  276. // 保存高亮到存储
  277. function saveHighlights() {
  278. const allHighlights = GM_getValue('highlights', {});
  279. allHighlights[currentPageUrl] = highlights;
  280. GM_setValue('highlights', allHighlights);
  281. }
  282.  
  283. // 保存设置
  284. function saveSettings() {
  285. GM_setValue('highlight_settings', settings);
  286. }
  287.  
  288. // 移除高亮菜单
  289. function removeHighlightMenu() {
  290. if (window.currentMenuCloseHandler) {
  291. document.removeEventListener('click', window.currentMenuCloseHandler);
  292. window.currentMenuCloseHandler = null;
  293. }
  294. const existingMenus = document.querySelectorAll(`.${STYLE_PREFIX}highlight-menu`);
  295. if (existingMenus.length) {
  296. existingMenus.forEach(menu => {
  297. menu.classList.remove(`${STYLE_PREFIX}show`);
  298. setTimeout(() => {
  299. if (menu && menu.parentNode) {
  300. menu.parentNode.removeChild(menu);
  301. }
  302. }, 200);
  303. });
  304. }
  305. clearTimeout(menuDisplayTimer);
  306. ignoreNextClick = false;
  307. menuOperationInProgress = false;
  308. }
  309.  
  310. // 高亮选中文本
  311. function highlightSelection(color) {
  312. if (!isHighlightEnabled) {
  313. return null;
  314. }
  315. const selection = window.getSelection();
  316. if (!selection.rangeCount) return null;
  317. const range = selection.getRangeAt(0);
  318. const selectedText = selection.toString().trim();
  319. if (!selectedText || selectedText.length < settings.minTextLength) {
  320. return null;
  321. }
  322. const highlightId = 'highlight-' + Date.now() + '-' + Math.floor(Math.random() * 10000);
  323. const highlightElement = document.createElement('span');
  324. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  325. highlightElement.dataset.highlightId = highlightId;
  326. highlightElement.style.backgroundColor = color;
  327.  
  328. // ★ 先从未修改前的文本中提取上下文
  329. let prefix = '', suffix = '';
  330. if (range.startContainer.nodeType === Node.TEXT_NODE) {
  331. const originalText = range.startContainer.textContent;
  332. const startOffset = range.startOffset;
  333. const endOffset = startOffset + selectedText.length;
  334. prefix = extractValidContext(originalText, startOffset, 20, "backward");
  335. suffix = extractValidContext(originalText, endOffset, 20, "forward");
  336. }
  337.  
  338. try {
  339. // 再进行DOM操作前,提取上下文后才调用 extractContents
  340. const fragment = range.extractContents();
  341. highlightElement.appendChild(fragment);
  342. range.insertNode(highlightElement);
  343.  
  344. const highlight = {
  345. id: highlightId,
  346. text: selectedText,
  347. color: color,
  348. timestamp: Date.now(),
  349. url: currentPageUrl,
  350. prefix: prefix, // 前置上下文
  351. suffix: suffix // 后置上下文
  352. };
  353.  
  354. highlights.push(highlight);
  355. saveHighlights();
  356.  
  357. highlightElement.addEventListener('click', (e) => {
  358. e.preventDefault();
  359. e.stopPropagation();
  360. removeHighlightMenu();
  361. setTimeout(() => {
  362. showHighlightEditMenu(e, highlightId);
  363. }, 10);
  364. });
  365.  
  366. // 检查侧边栏是否打开,如果打开则刷新高亮列表
  367. const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`);
  368. if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) {
  369. updateSidebarHighlights();
  370. }
  371.  
  372. selection.removeAllRanges();
  373. return highlightId;
  374. } catch (e) {
  375. console.warn('高亮失败:', e);
  376. try {
  377. findAndHighlight(selectedText, color, highlightId);
  378.  
  379. // 检查侧边栏是否打开,如果打开则刷新高亮列表
  380. const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`);
  381. if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) {
  382. updateSidebarHighlights();
  383. }
  384.  
  385. return highlightId;
  386. } catch (error) {
  387. console.error('替代高亮方法也失败:', error);
  388. return null;
  389. }
  390. }
  391. }
  392.  
  393. // 根据ID删除高亮
  394. function removeHighlightById(highlightId) {
  395. const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`);
  396. if (highlightElement) {
  397. const textNode = document.createTextNode(highlightElement.textContent);
  398. highlightElement.parentNode.replaceChild(textNode, highlightElement);
  399. }
  400. highlights = highlights.filter(h => h.id !== highlightId);
  401. saveHighlights();
  402.  
  403. // 检查侧边栏是否打开,如果打开则刷新高亮列表
  404. const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`);
  405. if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) {
  406. updateSidebarHighlights();
  407. }
  408. }
  409.  
  410. // 使用 MutationObserver 监听 DOM 变化,动态恢复高亮
  411. function observeDomChanges() {
  412. let debounceTimer; // 新增变量用于防抖
  413. const observer = new MutationObserver((mutations) => {
  414. clearTimeout(debounceTimer);
  415. debounceTimer = setTimeout(() => {
  416. applyHighlights();
  417. }, 300);
  418. });
  419. observer.observe(document.body, {
  420. childList: true,
  421. subtree: true,
  422. characterData: true
  423. });
  424. }
  425.  
  426. // 更改高亮颜色
  427. function changeHighlightColor(highlightId, newColor) {
  428. const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`);
  429. if (highlightElement) {
  430. highlightElement.style.backgroundColor = newColor;
  431. }
  432. const index = highlights.findIndex(h => h.id === highlightId);
  433. if (index !== -1) {
  434. highlights[index].color = newColor;
  435. saveHighlights();
  436. }
  437.  
  438. // 检查侧边栏是否打开,如果打开则刷新高亮列表
  439. const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`);
  440. if (sidebar && sidebar.style.right === '0px' && updateSidebarHighlights) {
  441. updateSidebarHighlights();
  442. }
  443. }
  444.  
  445. // 显示/隐藏侧边栏
  446. function toggleSidebar(forceShow = true) {
  447. const sidebar = document.getElementById(`${STYLE_PREFIX}sidebar`);
  448. const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`);
  449. if (!sidebar) return;
  450.  
  451. if (forceShow) {
  452. sidebar.style.right = '0px';
  453. // 显示侧边栏时隐藏浮动按钮
  454. if (floatingButton) {
  455. floatingButton.style.display = 'none';
  456. }
  457. if (updateSidebarHighlights) {
  458. updateSidebarHighlights();
  459. }
  460. } else {
  461. const width = sidebar.style.width || '300px';
  462. const wasVisible = sidebar.style.right === '0px';
  463. sidebar.style.right = wasVisible ? `-${width}` : '0px';
  464.  
  465. // 更新浮动按钮显示状态
  466. if (floatingButton) {
  467. if (wasVisible) {
  468. // 关闭侧边栏时,根据设置和启用状态决定是否显示浮动按钮
  469. floatingButton.style.display = (settings.showFloatingButton && isHighlightEnabled) ? 'flex' : 'none';
  470. } else {
  471. // 打开侧边栏时,隐藏浮动按钮
  472. floatingButton.style.display = 'none';
  473. }
  474. }
  475.  
  476. if (sidebar.style.right === '0px' && updateSidebarHighlights) {
  477. updateSidebarHighlights();
  478. }
  479. }
  480. }
  481.  
  482. // 切换浮动按钮显示/隐藏
  483. function toggleFloatingButton() {
  484. const floatingButton = document.getElementById(`${STYLE_PREFIX}floating-button`);
  485. if (!floatingButton) return;
  486.  
  487. settings.showFloatingButton = !settings.showFloatingButton;
  488. // 即使设置为显示,在启用页面也不显示按钮
  489. floatingButton.style.display = (settings.showFloatingButton && isHighlightEnabled) ? 'flex' : 'none'; saveSettings();
  490. }
  491.  
  492. // 显示高亮编辑菜单
  493. function showHighlightEditMenu(event, highlightId) {
  494. if (!isHighlightEnabled) {
  495. return;
  496. }
  497. removeHighlightMenu();
  498. if (menuOperationInProgress) return;
  499. menuOperationInProgress = true;
  500. event.preventDefault();
  501. event.stopPropagation();
  502. ignoreNextClick = true;
  503. const highlight = highlights.find(h => h.id === highlightId);
  504. if (!highlight) {
  505. menuOperationInProgress = false;
  506. return;
  507. }
  508. const menu = createHighlightMenu(false);
  509. menu.dataset.currentHighlightId = highlightId;
  510. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(colorBtn => {
  511. colorBtn.classList.remove(`${STYLE_PREFIX}active`);
  512. });
  513. const activeColorButton = menu.querySelector(`.${STYLE_PREFIX}highlight-menu-color[data-color="${highlight.color}"]`);
  514. if (activeColorButton) {
  515. activeColorButton.classList.add(`${STYLE_PREFIX}active`);
  516. }
  517. const menuHeight = 50;
  518. let menuTop = event.clientY + window.scrollY - menuHeight - 10;
  519. let showAbove = true;
  520. if (event.clientY < menuHeight + 10) {
  521. menuTop = event.clientY + window.scrollY + 10;
  522. showAbove = false;
  523. }
  524. menu.style.top = `${menuTop}px`;
  525. const menuWidth = menu.offsetWidth || 200;
  526. let menuLeft;
  527. if (event.clientX - (menuWidth / 2) < 5) {
  528. menuLeft = 5;
  529. } else if (event.clientX + (menuWidth / 2) > window.innerWidth - 5) {
  530. menuLeft = window.innerWidth - menuWidth - 5;
  531. } else {
  532. menuLeft = event.clientX - (menuWidth / 2);
  533. }
  534. menu.style.left = `${menuLeft}px`;
  535. const arrowLeft = event.clientX - menuLeft;
  536. const minArrowLeft = 12;
  537. const maxArrowLeft = menuWidth - 12;
  538. const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft));
  539. menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`);
  540. if (!showAbove) {
  541. menu.classList.add(`${STYLE_PREFIX}arrow-top`);
  542. } else {
  543. menu.classList.remove(`${STYLE_PREFIX}arrow-top`);
  544. }
  545. requestAnimationFrame(() => {
  546. menu.classList.add(`${STYLE_PREFIX}show`);
  547. // 使用 once:true 来自动清理事件监听
  548. document.addEventListener('click', function closeMenu(e) {
  549. if (ignoreNextClick) {
  550. ignoreNextClick = false;
  551. return;
  552. }
  553. if (!menu.contains(e.target)) {
  554. removeHighlightMenu();
  555. }
  556. }, { once: true });
  557. setTimeout(() => {
  558. ignoreNextClick = false;
  559. menuOperationInProgress = false;
  560. }, 50);
  561. });
  562. }
  563.  
  564. // 查找并高亮文本
  565. function findAndHighlight(searchText, color, highlightId) {
  566. // 遍历所有文本节点查找匹配内容
  567. const treeWalker = document.createTreeWalker(
  568. document.body,
  569. NodeFilter.SHOW_TEXT,
  570. null
  571. );
  572. while (treeWalker.nextNode()) {
  573. const node = treeWalker.currentNode;
  574. const textContent = node.textContent;
  575. if (!textContent || textContent.trim().length === 0) continue;
  576. const idx = textContent.indexOf(searchText);
  577. if (idx !== -1) {
  578. const range = document.createRange();
  579. range.setStart(node, idx);
  580. range.setEnd(node, idx + searchText.length);
  581. const highlightElement = document.createElement('span');
  582. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  583. highlightElement.dataset.highlightId = highlightId;
  584. highlightElement.style.backgroundColor = color;
  585. try {
  586. const fragment = range.extractContents();
  587. highlightElement.appendChild(fragment);
  588. range.insertNode(highlightElement);
  589. highlightElement.addEventListener('click', (e) => {
  590. e.preventDefault();
  591. e.stopPropagation();
  592. showHighlightEditMenu(e, highlightId);
  593. });
  594. // 新高亮直接返回 true
  595. return true;
  596. } catch (e) {
  597. console.warn('应用高亮失败:', e);
  598. }
  599. }
  600. }
  601. return false;
  602. }
  603.  
  604. // 应用页面上的所有高亮
  605. function applyHighlights() {
  606. // 按 timestamp 降序排序(从后向前恢复)
  607. highlights.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
  608. highlights.forEach(highlight => {
  609. const restored = advancedRestoreHighlight(highlight);
  610. if (!restored) {
  611. console.warn('多步恢复失败:', highlight.id);
  612. }
  613. });
  614. }
  615.  
  616. // 创建高亮菜单
  617. function createHighlightMenu(isNewHighlight = true) {
  618. removeHighlightMenu();
  619. ignoreNextClick = true;
  620. const menu = document.createElement('div');
  621. menu.className = `${STYLE_PREFIX}highlight-menu`;
  622. menu.innerHTML = `
  623. <div class="${STYLE_PREFIX}highlight-menu-colors">
  624. ${settings.colors.map(color => `
  625. <div class="${STYLE_PREFIX}highlight-menu-color"
  626. style="background-color: ${color};"
  627. data-color="${color}">
  628. </div>
  629. `).join('')}
  630. </div>
  631. `;
  632. // 无论如何先置空操作ID
  633. menu.dataset.currentHighlightId = '';
  634. document.body.appendChild(menu);
  635.  
  636. // 如果是新建高亮,确保所有颜色块没有激活状态
  637. if (isNewHighlight) {
  638. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => {
  639. el.classList.remove(`${STYLE_PREFIX}active`);
  640. });
  641. }
  642.  
  643. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`).forEach(el => {
  644. el.addEventListener('click', (e) => {
  645. const color = el.dataset.color;
  646. const isActive = el.classList.contains(`${STYLE_PREFIX}active`);
  647. const currentHighlightId = menu.dataset.currentHighlightId;
  648. if (isActive) {
  649. if (currentHighlightId) {
  650. removeHighlightById(currentHighlightId);
  651. menu.dataset.currentHighlightId = '';
  652. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`)
  653. .forEach(colorEl => colorEl.classList.remove(`${STYLE_PREFIX}active`));
  654. const sel = window.getSelection();
  655. sel.removeAllRanges();
  656. if (savedRange) {
  657. sel.addRange(savedRange.cloneRange());
  658. }
  659. } else {
  660. window.getSelection().removeAllRanges();
  661. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`)
  662. .forEach(colorEl => colorEl.classList.remove(`${STYLE_PREFIX}active`));
  663. }
  664. removeHighlightMenu();
  665. } else {
  666. settings.activeColor = color;
  667. saveSettings();
  668. if (currentHighlightId) {
  669. changeHighlightColor(currentHighlightId, color);
  670. } else {
  671. const selection = window.getSelection();
  672. if (selection.toString().trim() === '' && savedRange) {
  673. selection.removeAllRanges();
  674. selection.addRange(savedRange.cloneRange());
  675. }
  676. const newHighlightId = highlightSelection(color);
  677. if (newHighlightId) {
  678. menu.dataset.currentHighlightId = newHighlightId;
  679. }
  680. }
  681. menu.querySelectorAll(`.${STYLE_PREFIX}highlight-menu-color`)
  682. .forEach(colorEl => colorEl.classList.toggle(`${STYLE_PREFIX}active`, colorEl.dataset.color === color));
  683. }
  684. e.stopPropagation();
  685. });
  686. });
  687. return menu;
  688. }
  689.  
  690. // 显示高亮菜单
  691. function showHighlightMenu() {
  692. if (!isHighlightEnabled) {
  693. return;
  694. }
  695. if (menuOperationInProgress) return;
  696. menuOperationInProgress = true;
  697. const selection = window.getSelection();
  698. const selectedText = selection.toString().trim();
  699. if (selectedText === '') {
  700. menuOperationInProgress = false;
  701. return;
  702. }
  703. const menu = createHighlightMenu(true);
  704. const range = selection.getRangeAt(0);
  705. const rects = range.getClientRects();
  706. if (rects.length === 0) {
  707. menuOperationInProgress = false;
  708. return;
  709. }
  710. const targetRect = rects[0];
  711. const menuHeight = 50;
  712. let initialTop = window.scrollY + targetRect.top - menuHeight - 8;
  713. let showAbove = true;
  714. if (targetRect.top < menuHeight + 10) {
  715. initialTop = window.scrollY + targetRect.bottom + 8;
  716. showAbove = false;
  717. }
  718. menu.style.top = `${initialTop}px`;
  719. setTimeout(() => {
  720. const menuWidth = menu.offsetWidth;
  721. const textCenterX = targetRect.left + (targetRect.width / 2);
  722. let menuLeft;
  723. if (textCenterX - (menuWidth / 2) < 5) {
  724. menuLeft = 5;
  725. } else if (textCenterX + (menuWidth / 2) > window.innerWidth - 5) {
  726. menuLeft = window.innerWidth - menuWidth - 5;
  727. } else {
  728. menuLeft = textCenterX - (menuWidth / 2);
  729. }
  730. menu.style.left = `${menuLeft}px`;
  731. menu.style.transform = 'none';
  732. const arrowLeft = textCenterX - menuLeft;
  733. const minArrowLeft = 12;
  734. const maxArrowLeft = menuWidth - 12;
  735. const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft));
  736. menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`);
  737. if (!showAbove) {
  738. menu.classList.add(`${STYLE_PREFIX}arrow-top`);
  739. } else {
  740. menu.classList.remove(`${STYLE_PREFIX}arrow-top`);
  741. }
  742. requestAnimationFrame(() => {
  743. menu.classList.add(`${STYLE_PREFIX}show`);
  744. });
  745. }, 0);
  746. document.addEventListener('click', function closeMenu(e) {
  747. if (ignoreNextClick) {
  748. ignoreNextClick = false;
  749. return;
  750. }
  751. if (!menu.contains(e.target)) {
  752. removeHighlightMenu();
  753. }
  754. }, { once: true });
  755. setTimeout(() => {
  756. ignoreNextClick = false;
  757. menuOperationInProgress = false;
  758. }, 100);
  759. }
  760.  
  761. // 注册(不可用)事件
  762. function registerEvents() {
  763. document.addEventListener('mouseup', function (e) {
  764. if (!isHighlightEnabled) {
  765. return;
  766. }
  767. if (e.target.closest(`.${STYLE_PREFIX}highlight-menu`)) {
  768. return;
  769. }
  770. const selection = window.getSelection();
  771. const selectedText = selection.toString().trim();
  772. if (selectedText.length < (settings.minTextLength || 1)) {
  773. return;
  774. }
  775. if (selection.rangeCount > 0) {
  776. savedRange = selection.getRangeAt(0).cloneRange();
  777. }
  778. removeHighlightMenu();
  779. clearTimeout(menuDisplayTimer);
  780. ignoreNextClick = true;
  781. menuDisplayTimer = setTimeout(() => {
  782. showHighlightMenu();
  783. }, 10);
  784. });
  785. }
  786.  
  787. function fuzzyContextMatch(highlight) {
  788. // 如果未启用模糊匹配,则直接返回 false
  789. if (!settings.enableFuzzyMatch) return false;
  790.  
  791. if (!highlight.text) return false;
  792. const textNodes = document.createTreeWalker(
  793. document.body,
  794. NodeFilter.SHOW_TEXT,
  795. null
  796. );
  797. const pattern = highlight.text.trim();
  798.  
  799. // 预处理前缀和后缀
  800. const storedPrefix = highlight.prefix ? highlight.prefix.trim() : '';
  801. const storedSuffix = highlight.suffix ? highlight.suffix.trim() : '';
  802.  
  803. // 存储所有匹配项及其上下文评分
  804. const matches = [];
  805.  
  806. // 记录匹配的总数,以便特殊处理单一匹配的情况
  807. let matchCount = 0;
  808.  
  809. while (textNodes.nextNode()) {
  810. const node = textNodes.currentNode;
  811. const textContent = node.textContent;
  812. if (!textContent || textContent.trim().length === 0) continue;
  813.  
  814. // 查找所有可能的匹配位置
  815. let startIdx = 0;
  816. while (startIdx < textContent.length) {
  817. const idx = textContent.indexOf(pattern, startIdx);
  818. if (idx === -1) break;
  819.  
  820. matchCount++;
  821.  
  822. // 获取上下文
  823. const actualPrefix = textContent.substring(Math.max(0, idx - 40), idx).trim();
  824. const actualSuffix = textContent.substring(idx + pattern.length,
  825. idx + pattern.length + 40).trim();
  826.  
  827. // 计算上下文匹配分数
  828. let score = 0;
  829.  
  830. // 前缀匹配分数计算 (更宽松版)
  831. if (storedPrefix && actualPrefix) {
  832. if (actualPrefix.includes(storedPrefix)) {
  833. score += 10; // 前缀完全包含加10分
  834. } else {
  835. // 尝试寻找部分匹配
  836. for (let i = 1; i <= Math.min(storedPrefix.length, 12); i++) {
  837. const prefixEnd = storedPrefix.slice(-i);
  838. if (actualPrefix.endsWith(prefixEnd)) {
  839. score += i / 2; // 匹配前缀尾部,加分
  840. break;
  841. }
  842. }
  843.  
  844. // 尝试在前缀中查找关键片段
  845. if (storedPrefix.length > 6) {
  846. for (let i = 0; i < storedPrefix.length - 5; i++) {
  847. const fragment = storedPrefix.substring(i, i + 5);
  848. if (actualPrefix.includes(fragment)) {
  849. score += 2.5; // 找到显著片段加2.5分
  850. break;
  851. }
  852. }
  853. }
  854. }
  855. }
  856.  
  857. // 后缀匹配分数计算 (更宽松版)
  858. if (storedSuffix && actualSuffix) {
  859. if (actualSuffix.includes(storedSuffix)) {
  860. score += 10; // 后缀完全包含加10分
  861. } else {
  862. // 尝试寻找部分匹配
  863. for (let i = 1; i <= Math.min(storedSuffix.length, 12); i++) {
  864. const suffixStart = storedSuffix.slice(0, i);
  865. if (actualSuffix.startsWith(suffixStart)) {
  866. score += i / 2; // 匹配后缀头部,加分
  867. break;
  868. }
  869. }
  870.  
  871. // 尝试在后缀中查找关键片段
  872. if (storedSuffix.length > 6) {
  873. for (let i = 0; i < storedSuffix.length - 5; i++) {
  874. const fragment = storedSuffix.substring(i, i + 5);
  875. if (actualSuffix.includes(fragment)) {
  876. score += 2.5; // 找到显著片段加2.5分
  877. break;
  878. }
  879. }
  880. }
  881. }
  882. }
  883.  
  884. // 如果是单个字符的高亮,给予额外分数,避免完全相同的短文本被错过
  885. if (pattern.length <= 3 && (actualPrefix.includes(storedPrefix) || actualSuffix.includes(storedSuffix))) {
  886. score += 5;
  887. }
  888.  
  889. // 记录匹配项
  890. matches.push({
  891. node,
  892. idx,
  893. score,
  894. actualPrefix,
  895. actualSuffix
  896. });
  897.  
  898. startIdx = idx + 1; // 继续搜索下一个匹配
  899. }
  900. }
  901.  
  902. // 特殊情况:如果页面上只有一个匹配,直接使用它
  903. if (matchCount === 1 && matches.length === 1) {
  904. const match = matches[0];
  905. try {
  906. const range = document.createRange();
  907. range.setStart(match.node, match.idx);
  908. range.setEnd(match.node, match.idx + pattern.length);
  909.  
  910. const highlightElement = document.createElement('span');
  911. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  912. highlightElement.dataset.highlightId = highlight.id;
  913. highlightElement.style.backgroundColor = highlight.color;
  914.  
  915. const fragment = range.extractContents();
  916. highlightElement.appendChild(fragment);
  917. range.insertNode(highlightElement);
  918.  
  919. highlightElement.addEventListener('click', (e) => {
  920. e.preventDefault();
  921. e.stopPropagation();
  922. showHighlightEditMenu(e, highlight.id);
  923. });
  924.  
  925. return true;
  926. } catch (e) {
  927. console.warn('唯一模糊匹配高亮失败:', e);
  928. }
  929. }
  930.  
  931. // 如果有多个匹配项,选择分数最高的
  932. if (matches.length > 0) {
  933. // 按分数降序排序
  934. matches.sort((a, b) => b.score - a.score);
  935. const bestMatch = matches[0];
  936.  
  937. // 降低得分阈值到2,使更多匹配可以被接受
  938. const threshold = matchCount > 1 ? 2 : 0.5;
  939.  
  940. if (bestMatch.score >= threshold) {
  941. // 构造高亮
  942. try {
  943. const range = document.createRange();
  944. range.setStart(bestMatch.node, bestMatch.idx);
  945. range.setEnd(bestMatch.node, bestMatch.idx + pattern.length);
  946.  
  947. const highlightElement = document.createElement('span');
  948. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  949. highlightElement.dataset.highlightId = highlight.id;
  950. highlightElement.style.backgroundColor = highlight.color;
  951.  
  952. const fragment = range.extractContents();
  953. highlightElement.appendChild(fragment);
  954. range.insertNode(highlightElement);
  955.  
  956. highlightElement.addEventListener('click', (e) => {
  957. e.preventDefault();
  958. e.stopPropagation();
  959. showHighlightEditMenu(e, highlight.id);
  960. });
  961.  
  962. return true;
  963. } catch (e) {
  964. console.warn('模糊匹配插入高亮失败:', e);
  965. }
  966. } else {
  967. console.log('模糊匹配分数过低,尝试进一步降低要求:', {
  968. text: pattern,
  969. bestScore: bestMatch.score
  970. });
  971.  
  972. // 如果分数太低但至少有一个匹配,可以考虑用最后的回退机制
  973. if (matches.length > 0 && pattern.length <= 5) {
  974. // 对于短文本,如果我们有任何匹配并且上下文也有一些匹配,就使用它
  975. const bestMatch = matches[0];
  976. try {
  977. const range = document.createRange();
  978. range.setStart(bestMatch.node, bestMatch.idx);
  979. range.setEnd(bestMatch.node, bestMatch.idx + pattern.length);
  980.  
  981. const highlightElement = document.createElement('span');
  982. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  983. highlightElement.dataset.highlightId = highlight.id;
  984. highlightElement.style.backgroundColor = highlight.color;
  985.  
  986. console.log('使用回退机制恢复短文本高亮:', pattern);
  987.  
  988. const fragment = range.extractContents();
  989. highlightElement.appendChild(fragment);
  990. range.insertNode(highlightElement);
  991.  
  992. highlightElement.addEventListener('click', (e) => {
  993. e.preventDefault();
  994. e.stopPropagation();
  995. showHighlightEditMenu(e, highlight.id);
  996. });
  997.  
  998. return true;
  999. } catch (e) {
  1000. console.warn('回退机制高亮失败:', e);
  1001. }
  1002. }
  1003. }
  1004. }
  1005.  
  1006. return false;
  1007. }
  1008.  
  1009. function advancedRestoreHighlight(highlight) {
  1010. // 检查是否已经有相同ID的高亮存在
  1011. const existingHighlight = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlight.id}"]`);
  1012. if (existingHighlight) {
  1013. // 高亮已存在,不需要重复恢复
  1014. return true;
  1015. }
  1016.  
  1017. // 使用文本上下文匹配恢复高亮
  1018. const contextRestored = restoreHighlightUsingContext(highlight);
  1019. if (contextRestored) {
  1020. // 只保存最新一次恢复记录
  1021. highlight.recoveryHistory = {
  1022. timestamp: Date.now(),
  1023. method: 'contextMatch',
  1024. success: true
  1025. };
  1026. saveHighlights(); // 保存更新后的恢复历史
  1027. return true;
  1028. }
  1029.  
  1030. // 尝试模糊匹配
  1031. if (fuzzyContextMatch(highlight)) {
  1032. highlight.recoveryHistory = {
  1033. timestamp: Date.now(),
  1034. method: 'fuzzyContext',
  1035. success: true
  1036. };
  1037. saveHighlights();
  1038. return true;
  1039. }
  1040.  
  1041. // 记录失败状态
  1042. highlight.recoveryHistory = {
  1043. timestamp: Date.now(),
  1044. method: 'fallback',
  1045. success: false
  1046. };
  1047. saveHighlights();
  1048. return false;
  1049. }
  1050.  
  1051. function restoreHighlightUsingContext(highlight) {
  1052. // 遍历页面中所有文本节点
  1053. const treeWalker = document.createTreeWalker(
  1054. document.body,
  1055. NodeFilter.SHOW_TEXT,
  1056. null
  1057. );
  1058.  
  1059. // 如果存在前缀和后缀信息,预处理它们以便于比较
  1060. const storedPrefix = highlight.prefix ? highlight.prefix.trim() : '';
  1061. const storedSuffix = highlight.suffix ? highlight.suffix.trim() : '';
  1062.  
  1063. // 存储所有可能的匹配及其得分
  1064. const matches = [];
  1065.  
  1066. // 记录找到多少个完全相同的文本
  1067. let exactTextMatches = 0;
  1068.  
  1069. while (treeWalker.nextNode()) {
  1070. const node = treeWalker.currentNode;
  1071. const textContent = node.textContent;
  1072. if (!textContent || textContent.trim().length === 0) continue;
  1073.  
  1074. const idx = textContent.indexOf(highlight.text);
  1075. if (idx !== -1) {
  1076. // 记录找到了一个文本匹配
  1077. exactTextMatches++;
  1078.  
  1079. // 获取当前节点中匹配区域前后的上下文
  1080. const actualPrefix = textContent.substring(Math.max(0, idx - 30), idx).trim();
  1081. const actualSuffix = textContent.substring(idx + highlight.text.length,
  1082. idx + highlight.text.length + 30).trim();
  1083.  
  1084. // 计算上下文匹配得分
  1085. let score = 1; // 基础分:找到了文本
  1086.  
  1087. // 前缀匹配得分计算 (改进版)
  1088. if (storedPrefix && actualPrefix) {
  1089. if (actualPrefix.includes(storedPrefix)) {
  1090. score += 10; // 前缀完全包含加10分
  1091. } else if (storedPrefix.length > 3) {
  1092. // 尝试匹配前缀的尾部
  1093. for (let i = Math.min(storedPrefix.length, actualPrefix.length); i >= 3; i--) {
  1094. if (storedPrefix.slice(-i) === actualPrefix.slice(-i)) {
  1095. score += i / 2; // 匹配长度越长分数越高
  1096. break;
  1097. }
  1098. }
  1099.  
  1100. // 另外尝试寻找前缀中的部分匹配
  1101. if (storedPrefix.length >= 8) {
  1102. for (let i = 0; i < storedPrefix.length - 6; i++) {
  1103. const fragment = storedPrefix.substring(i, i + 6);
  1104. if (actualPrefix.includes(fragment)) {
  1105. score += 3; // 找到部分匹配加3分
  1106. break;
  1107. }
  1108. }
  1109. }
  1110. }
  1111. }
  1112.  
  1113. // 后缀匹配得分计算 (改进版)
  1114. if (storedSuffix && actualSuffix) {
  1115. if (actualSuffix.includes(storedSuffix)) {
  1116. score += 10; // 后缀完全包含加10分
  1117. } else if (storedSuffix.length > 3) {
  1118. // 尝试匹配后缀的开头
  1119. for (let i = Math.min(storedSuffix.length, actualSuffix.length); i >= 3; i--) {
  1120. if (storedSuffix.slice(0, i) === actualSuffix.slice(0, i)) {
  1121. score += i / 2; // 匹配长度越长分数越高
  1122. break;
  1123. }
  1124. }
  1125.  
  1126. // 另外尝试寻找后缀中的部分匹配
  1127. if (storedSuffix.length >= 8) {
  1128. for (let i = 0; i < storedSuffix.length - 6; i++) {
  1129. const fragment = storedSuffix.substring(i, i + 6);
  1130. if (actualSuffix.includes(fragment)) {
  1131. score += 3; // 找到部分匹配加3分
  1132. break;
  1133. }
  1134. }
  1135. }
  1136. }
  1137. }
  1138.  
  1139. matches.push({
  1140. node,
  1141. idx,
  1142. score,
  1143. actualPrefix,
  1144. actualSuffix
  1145. });
  1146. }
  1147. }
  1148.  
  1149. // 特殊情况处理:如果页面上只有一个文本匹配,直接使用它
  1150. if (exactTextMatches === 1 && matches.length === 1) {
  1151. const match = matches[0];
  1152. try {
  1153. const range = document.createRange();
  1154. range.setStart(match.node, match.idx);
  1155. range.setEnd(match.node, match.idx + highlight.text.length);
  1156.  
  1157. const highlightElement = document.createElement('span');
  1158. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  1159. highlightElement.dataset.highlightId = highlight.id;
  1160. highlightElement.style.backgroundColor = highlight.color;
  1161.  
  1162. const fragment = range.extractContents();
  1163. highlightElement.appendChild(fragment);
  1164. range.insertNode(highlightElement);
  1165.  
  1166. highlightElement.addEventListener('click', (e) => {
  1167. e.preventDefault();
  1168. e.stopPropagation();
  1169. showHighlightEditMenu(e, highlight.id);
  1170. });
  1171.  
  1172. return true;
  1173. } catch (e) {
  1174. console.warn('唯一文本匹配恢复高亮失败:', e);
  1175. }
  1176. }
  1177.  
  1178. // 如果找到多个匹配项,选择得分最高的
  1179. if (matches.length > 0) {
  1180. // 按得分降序排序
  1181. matches.sort((a, b) => b.score - a.score);
  1182. const bestMatch = matches[0];
  1183.  
  1184. // 降低匹配阈值,从5降到3,使得更多的匹配可以被接受
  1185. const minScore = exactTextMatches > 1 ? 3 : 1;
  1186.  
  1187. if (bestMatch.score >= minScore) {
  1188. try {
  1189. const range = document.createRange();
  1190. range.setStart(bestMatch.node, bestMatch.idx);
  1191. range.setEnd(bestMatch.node, bestMatch.idx + highlight.text.length);
  1192.  
  1193. const highlightElement = document.createElement('span');
  1194. highlightElement.className = `${STYLE_PREFIX}highlight-marked`;
  1195. highlightElement.dataset.highlightId = highlight.id;
  1196. highlightElement.style.backgroundColor = highlight.color;
  1197.  
  1198. const fragment = range.extractContents();
  1199. highlightElement.appendChild(fragment);
  1200. range.insertNode(highlightElement);
  1201.  
  1202. highlightElement.addEventListener('click', (e) => {
  1203. e.preventDefault();
  1204. e.stopPropagation();
  1205. showHighlightEditMenu(e, highlight.id);
  1206. });
  1207.  
  1208. return true;
  1209. } catch (e) {
  1210. console.warn('上下文匹配恢复高亮失败:', e);
  1211. }
  1212. } else {
  1213. console.log('匹配分数过低,尝试模糊匹配:', {
  1214. text: highlight.text,
  1215. bestScore: bestMatch.score,
  1216. matchCount: matches.length
  1217. });
  1218. }
  1219. }
  1220.  
  1221. return false;
  1222. }
  1223.  
  1224. function extractValidContext(text, start, count, direction) {
  1225. // direction: "backward" 从 start 往前提取, "forward" 从 start 往后提取
  1226. let result = "";
  1227. let processedChars = 0;
  1228.  
  1229. // 对于短文本或单个字符,我们提取更多上下文
  1230. const adjustedCount = count * (text.length <= 3 ? 2 : 1);
  1231.  
  1232. if (direction === "backward") {
  1233. for (let i = start - 1; i >= 0 && processedChars < adjustedCount * 2; i--) {
  1234. const ch = text.charAt(i);
  1235. // 只计算有效字符(中文、英文、数字)
  1236. if (/[\u4e00-\u9fffA-Za-z0-9]/.test(ch)) {
  1237. result = ch + result;
  1238. processedChars++;
  1239. if (processedChars >= adjustedCount) break;
  1240. } else {
  1241. // 空格和标点也记录,但不计入有效字符数
  1242. result = ch + result;
  1243. }
  1244. }
  1245. } else { // forward
  1246. for (let i = start; i < text.length && processedChars < adjustedCount * 2; i++) {
  1247. const ch = text.charAt(i);
  1248. // 只计算有效字符(中文、英文、数字)
  1249. if (/[\u4e00-\u9fffA-Za-z0-9]/.test(ch)) {
  1250. result += ch;
  1251. processedChars++;
  1252. if (processedChars >= adjustedCount) break;
  1253. } else {
  1254. // 空格和标点也记录,但不计入有效字符数
  1255. result += ch;
  1256. }
  1257. }
  1258. }
  1259. return result;
  1260. }
  1261.  
  1262. // 添加浮动按钮和侧边栏功能
  1263. function createFloatingButtonAndSidebar() {
  1264. const tooltipStyle = document.createElement('style');
  1265. tooltipStyle.textContent = `
  1266. .${STYLE_PREFIX}tooltip {
  1267. position: absolute;
  1268. background: #1A1D24;
  1269. color: #E8E9EB;
  1270. padding: 4px 10px;
  1271. border-radius: 4px;
  1272. font-size: 12px;
  1273. pointer-events: none;
  1274. white-space: nowrap;
  1275. z-index: 10000;
  1276. opacity: 0;
  1277. transition: opacity 0.2s;
  1278. bottom: 130%;
  1279. left: 50%;
  1280. transform: translateX(-50%);
  1281. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  1282. border: 1px solid rgba(59, 165, 216, 0.2);
  1283. }
  1284.  
  1285. .${STYLE_PREFIX}tooltip::after {
  1286. content: "";
  1287. position: absolute;
  1288. top: 100%;
  1289. left: 50%;
  1290. margin-left: -5px;
  1291. border-width: 5px;
  1292. border-style: solid;
  1293. border-color: #1A1D24 transparent transparent transparent;
  1294. }
  1295.  
  1296. .${STYLE_PREFIX}tooltip-container {
  1297. position: relative;
  1298. }
  1299. `;
  1300. document.head.appendChild(tooltipStyle);
  1301. // 创建浮动按钮
  1302. const floatingButton = document.createElement('button');
  1303. floatingButton.id = `${STYLE_PREFIX}floating-button`;
  1304. // 使用 SVG 图标,代表"汉堡菜单"
  1305. floatingButton.innerHTML = `
  1306. <svg viewBox="0 0 100 80" width="16" height="16" fill="#ccc" xmlns="http://www.w3.org/2000/svg">
  1307. <rect width="100" height="10"></rect>
  1308. <rect y="30" width="100" height="10"></rect>
  1309. <rect y="60" width="100" height="10"></rect>
  1310. </svg>
  1311. `;
  1312. // 根据设置和启用状态决定是否显示
  1313. floatingButton.style.display = (settings.showFloatingButton && isHighlightEnabled) ? 'flex' : 'none';
  1314. document.body.appendChild(floatingButton);
  1315. // 创建侧边栏(初始隐藏)
  1316. const sidebar = document.createElement('div');
  1317. sidebar.id = `${STYLE_PREFIX}sidebar`;
  1318. Object.assign(sidebar.style, {
  1319. position: 'fixed',
  1320. top: '0',
  1321. right: '-280px',
  1322. width: '280px',
  1323. height: '100%',
  1324. boxShadow: '-1px 0 8px rgba(0, 0, 0, 0.2)',
  1325. transition: 'none',
  1326. zIndex: '9999',
  1327. overflow: 'hidden',
  1328. display: 'flex',
  1329. flexDirection: 'column',
  1330. color: '#E8E9EB',
  1331. fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
  1332. background: 'linear-gradient(135deg, #262A33 0%, #1A1D24 100%)',
  1333. borderLeft: '1px solid rgba(255, 255, 255, 0.03)',
  1334. });
  1335.  
  1336. // 构建侧边栏内部结构
  1337. sidebar.innerHTML = `
  1338. <div class="${STYLE_PREFIX}sidebar-header">
  1339. <div class="${STYLE_PREFIX}sidebar-title" title="双击修改标题">
  1340. ${settings.sidebarDescription || '网页划词高亮工具'}
  1341. </div>
  1342. <div class="${STYLE_PREFIX}sidebar-controls">
  1343. <button class="${STYLE_PREFIX}sidebar-close" title="关闭侧边栏">
  1344. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  1345. <path d="M18 6L6 18M6 6l12 12"></path>
  1346. </svg>
  1347. </button>
  1348. </div>
  1349. </div>
  1350.  
  1351. <div class="${STYLE_PREFIX}sidebar-tabs">
  1352. <button class="${STYLE_PREFIX}sidebar-tab ${STYLE_PREFIX}active" data-tab="highlights">高亮列表</button>
  1353. <button class="${STYLE_PREFIX}sidebar-tab" data-tab="disabled">启用管理</button>
  1354. </div>
  1355.  
  1356. <div class="${STYLE_PREFIX}sidebar-content">
  1357. <div class="${STYLE_PREFIX}tab-panel ${STYLE_PREFIX}active" data-panel="highlights">
  1358. <div class="${STYLE_PREFIX}highlights-list"></div>
  1359. </div>
  1360.  
  1361. <div class="${STYLE_PREFIX}tab-panel" data-panel="disabled">
  1362. <div class="${STYLE_PREFIX}disabled-container"></div>
  1363. </div>
  1364. </div>
  1365. `;
  1366.  
  1367. document.body.appendChild(sidebar);
  1368. setTimeout(() => {
  1369. sidebar.style.transition = 'right 0.3s ease';
  1370. }, 10);
  1371.  
  1372. // 设置侧边栏内部元素样式
  1373. const header = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-header`);
  1374. Object.assign(header.style, {
  1375. display: 'flex',
  1376. justifyContent: 'space-between',
  1377. alignItems: 'center',
  1378. boxSizing: 'border-box',
  1379. borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
  1380. height: '42px',
  1381. background: 'rgba(26, 29, 36, 0.8)',
  1382. padding: '0 16px',
  1383. });
  1384.  
  1385. const title = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-title`);
  1386. Object.assign(title.style, {
  1387. fontSize: '13px',
  1388. fontWeight: '600',
  1389. flex: '1',
  1390. overflow: 'hidden',
  1391. textOverflow: 'ellipsis',
  1392. whiteSpace: 'nowrap',
  1393. cursor: 'default',
  1394. letterSpacing: '0.3px',
  1395. opacity: '0.9',
  1396. });
  1397.  
  1398. const controls = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-controls`);
  1399. Object.assign(controls.style, {
  1400. display: 'flex',
  1401. gap: '8px'
  1402. });
  1403.  
  1404. // 设置按钮样式
  1405. sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-controls button`).forEach(btn => {
  1406. Object.assign(btn.style, {
  1407. background: 'none',
  1408. border: 'none',
  1409. cursor: 'pointer',
  1410. padding: '3px', // 从5px减小到3px
  1411. display: 'flex',
  1412. alignItems: 'center',
  1413. justifyContent: 'center',
  1414. color: '#ccc',
  1415. borderRadius: '3px',
  1416. transition: 'background-color 0.2s'
  1417. });
  1418.  
  1419. // 添加按钮悬停效果
  1420. btn.addEventListener('mouseenter', () => {
  1421. btn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
  1422. });
  1423.  
  1424. btn.addEventListener('mouseleave', () => {
  1425. btn.style.backgroundColor = 'transparent';
  1426. });
  1427. });
  1428.  
  1429. // 设置标签页样式
  1430. const tabs = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-tabs`);
  1431. Object.assign(tabs.style, {
  1432. display: 'flex',
  1433. borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
  1434. padding: '0',
  1435. justifyContent: 'center',
  1436. backgroundColor: 'rgba(26, 29, 36, 0.6)',
  1437. height: '36px'
  1438. });
  1439.  
  1440. sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(tab => {
  1441. Object.assign(tab.style, {
  1442. height: '100%',
  1443. cursor: 'pointer',
  1444. fontWeight: '500',
  1445. background: 'none',
  1446. border: 'none',
  1447. color: '#ADADB8',
  1448. borderBottom: '2px solid transparent',
  1449. margin: '0',
  1450. transition: 'all 0.2s ease',
  1451. flex: '1',
  1452. display: 'flex',
  1453. alignItems: 'center',
  1454. justifyContent: 'center',
  1455. letterSpacing: '0.3px',
  1456. fontSize: '13px',
  1457. padding: '0 12px',
  1458. opacity: '0.75',
  1459. });
  1460. });
  1461.  
  1462. // 激活的标签页样式
  1463. const activeTab = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-tab.${STYLE_PREFIX}active`);
  1464. if (activeTab) {
  1465. Object.assign(activeTab.style, {
  1466. color: '#3BA5D8',
  1467. borderBottom: '2px solid #3BA5D8', // 改为湖蓝色
  1468. backgroundColor: 'rgba(59, 165, 216, 0.05)',
  1469. opacity: '1',
  1470. });
  1471. }
  1472.  
  1473. // 内容区域样式
  1474. const contentArea = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-content`);
  1475. Object.assign(contentArea.style, {
  1476. flex: '1',
  1477. overflow: 'hidden',
  1478. position: 'relative'
  1479. });
  1480.  
  1481. // 设置面板样式
  1482. sidebar.querySelectorAll(`.${STYLE_PREFIX}tab-panel`).forEach(panel => {
  1483. Object.assign(panel.style, {
  1484. height: '100%',
  1485. width: '100%',
  1486. position: 'absolute',
  1487. top: '0',
  1488. left: '0',
  1489. padding: '14px 14px 14px 14px', // 修改为:上右下左,底部padding设为0
  1490. boxSizing: 'border-box',
  1491. overflow: 'auto',
  1492. display: 'none'
  1493. });
  1494. });
  1495.  
  1496. // 显示当前活动面板
  1497. const activePanel = sidebar.querySelector(`.${STYLE_PREFIX}tab-panel.${STYLE_PREFIX}active`);
  1498. if (activePanel) {
  1499. activePanel.style.display = 'block';
  1500. }
  1501.  
  1502. // 添加侧边栏拖拽调整区域(位于侧边栏的最左侧)
  1503. const resizer = document.createElement('div');
  1504. Object.assign(resizer.style, {
  1505. position: 'absolute',
  1506. left: '0',
  1507. top: '0',
  1508. width: '5px',
  1509. height: '100%',
  1510. cursor: 'ew-resize',
  1511. backgroundColor: 'transparent'
  1512. });
  1513. sidebar.appendChild(resizer);
  1514.  
  1515. // 拖拽事件逻辑
  1516. resizer.addEventListener('mousedown', initResize);
  1517.  
  1518. function initResize(e) {
  1519. e.preventDefault();
  1520. window.addEventListener('mousemove', resizeSidebar);
  1521. window.addEventListener('mouseup', stopResize);
  1522. }
  1523.  
  1524. function resizeSidebar(e) {
  1525. // 计算出新的宽度:侧边栏右对齐,宽度 = 窗口宽度 - 鼠标水平位置
  1526. const newWidth = window.innerWidth - e.clientX;
  1527. // 限制最小宽度为 150px,最大宽度为窗口 80%
  1528. if (newWidth >= 150 && newWidth <= window.innerWidth * 0.8) {
  1529. sidebar.style.width = newWidth + 'px';
  1530. // 更新设置中的宽度
  1531. settings.sidebarWidth = newWidth;
  1532. saveSettings();
  1533. }
  1534. }
  1535.  
  1536. function stopResize(e) {
  1537. window.removeEventListener('mousemove', resizeSidebar);
  1538. window.removeEventListener('mouseup', stopResize);
  1539. }
  1540.  
  1541. // 标签页切换事件
  1542. sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(tab => {
  1543. tab.addEventListener('click', () => {
  1544. // 移除所有标签页和面板的活动状态
  1545. sidebar.querySelectorAll(`.${STYLE_PREFIX}sidebar-tab`).forEach(t => {
  1546. t.classList.remove(`${STYLE_PREFIX}active`);
  1547. t.style.color = '#ccc';
  1548. t.style.borderBottom = '2px solid transparent';
  1549. t.style.backgroundColor = 'transparent';
  1550. });
  1551.  
  1552. sidebar.querySelectorAll(`.${STYLE_PREFIX}tab-panel`).forEach(p => {
  1553. p.classList.remove(`${STYLE_PREFIX}active`);
  1554. p.style.display = 'none';
  1555. });
  1556.  
  1557. // 激活当前标签和面板
  1558. tab.classList.add(`${STYLE_PREFIX}active`);
  1559. tab.style.color = '#fff';
  1560. // 这里需要修改,将红色改为湖蓝色
  1561. tab.style.borderBottom = '2px solid #3BA5D8'; // 修改为湖蓝色,与初始样式一致
  1562. tab.style.backgroundColor = 'rgba(59, 165, 216, 0.05)'; // 添加微妙的背景色
  1563.  
  1564. const panelId = tab.getAttribute('data-tab');
  1565. const panel = sidebar.querySelector(`.${STYLE_PREFIX}tab-panel[data-panel="${panelId}"]`);
  1566. if (panel) {
  1567. panel.classList.add(`${STYLE_PREFIX}active`);
  1568. panel.style.display = 'block';
  1569. }
  1570. });
  1571.  
  1572. // 悬停效果也应修改为一致的颜色
  1573. tab.addEventListener('mouseenter', () => {
  1574. if (!tab.classList.contains(`${STYLE_PREFIX}active`)) {
  1575. tab.style.backgroundColor = 'rgba(59, 165, 216, 0.05)'; // 改为湖蓝色背景
  1576. tab.style.borderBottom = '2px solid rgba(59, 165, 216, 0.3)'; // 淡化的湖蓝色边框
  1577. }
  1578. });
  1579. });
  1580.  
  1581. // 标题双击编辑功能
  1582. const titleElement = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-title`);
  1583. titleElement.addEventListener('dblclick', () => {
  1584. const currentTitle = titleElement.textContent.trim() || '高亮工具';
  1585. const input = document.createElement('input');
  1586. input.type = 'text';
  1587. input.value = currentTitle;
  1588.  
  1589. // 美化输入框样式
  1590. Object.assign(input.style, {
  1591. width: '100%',
  1592. fontSize: '14px',
  1593. fontWeight: '500',
  1594. padding: '2px 6px',
  1595. boxSizing: 'border-box',
  1596. border: '1px solid rgba(116, 180, 255, 0.5)',
  1597. borderRadius: '3px',
  1598. outline: 'none',
  1599. background: 'rgba(0, 0, 0, 0.2)',
  1600. color: '#fff',
  1601. transition: 'all 0.15s ease',
  1602. boxShadow: '0 0 0 1px rgba(116, 180, 255, 0.3)'
  1603. });
  1604.  
  1605. // 替换标题内容为输入框
  1606. titleElement.innerHTML = '';
  1607. titleElement.appendChild(input);
  1608. input.focus();
  1609. input.select();
  1610.  
  1611. // 添加输入框聚焦样式
  1612. input.addEventListener('focus', () => {
  1613. input.style.background = 'rgba(20, 20, 20, 0.4)';
  1614. input.style.boxShadow = '0 0 0 2px rgba(116, 180, 255, 0.4)';
  1615. });
  1616.  
  1617. // 确认修改:输入框失焦或按下 Enter 键时更新标题
  1618. const confirmChange = () => {
  1619. const newTitle = input.value.trim() || '高亮工具';
  1620. settings.sidebarDescription = newTitle;
  1621. titleElement.textContent = newTitle;
  1622. saveSettings();
  1623. };
  1624.  
  1625. input.addEventListener('blur', confirmChange);
  1626. input.addEventListener('keydown', (event) => {
  1627. if (event.key === 'Enter') {
  1628. input.blur();
  1629. } else if (event.key === 'Escape') {
  1630. // 按ESC键取消编辑
  1631. titleElement.textContent = currentTitle;
  1632. input.blur();
  1633. }
  1634. });
  1635. });
  1636.  
  1637. // 关闭按钮事件
  1638. const closeButton = sidebar.querySelector(`.${STYLE_PREFIX}sidebar-close`);
  1639. closeButton.addEventListener('click', () => {
  1640. sidebar.style.right = `-${parseInt(sidebar.style.width)}px`;
  1641.  
  1642. // 侧边栏关闭时,如果设置允许显示浮动按钮且当前页面未启用,则恢复显示浮动按钮
  1643. if (settings.showFloatingButton && isHighlightEnabled) {
  1644. floatingButton.style.display = 'flex';
  1645. }
  1646. });
  1647.  
  1648. // 浮动按钮点击后切换侧边栏的显示和隐藏
  1649. floatingButton.addEventListener('click', () => {
  1650. if (sidebar.style.right === '0px') {
  1651. sidebar.style.right = `-${parseInt(sidebar.style.width)}px`;
  1652. // 如果设置允许显示浮动按钮且当前页面已启用,则显示浮动按钮
  1653. if (settings.showFloatingButton && isHighlightEnabled) { // 正确的变量
  1654. floatingButton.style.display = 'flex';
  1655. }
  1656. } else {
  1657. sidebar.style.right = '0px';
  1658. // 当侧边栏显示时,隐藏浮动按钮
  1659. floatingButton.style.display = 'none';
  1660. // 刷新高亮列表
  1661. if (updateSidebarHighlights) {
  1662. updateSidebarHighlights();
  1663. }
  1664. }
  1665. });
  1666.  
  1667. // 初始设置宽度
  1668. if (settings.sidebarWidth) {
  1669. sidebar.style.width = `${settings.sidebarWidth}px`;
  1670. sidebar.style.right = `-${settings.sidebarWidth}px`; // 确保初始位置与实际宽度匹配
  1671. } else {
  1672. sidebar.style.right = '-300px'; // 默认宽度的对应位置
  1673. }
  1674.  
  1675. // 渲染高亮列表面板
  1676. function renderHighlightsList() {
  1677. const highlightsListContainer = sidebar.querySelector(`.${STYLE_PREFIX}highlights-list`);
  1678. if (!highlightsListContainer) return;
  1679.  
  1680. // 清空容器
  1681. highlightsListContainer.innerHTML = '';
  1682. Object.assign(highlightsListContainer.style, {
  1683. height: 'calc(100vh - 120px)',
  1684. overflow: 'hidden',
  1685. display: 'flex',
  1686. flexDirection: 'column',
  1687. paddingBottom: '0',
  1688. width: '100%',
  1689. position: 'relative',
  1690. boxSizing: 'border-box',
  1691. });
  1692.  
  1693. // 创建高亮列表
  1694. const listContainer = document.createElement('div');
  1695. listContainer.className = `${STYLE_PREFIX}highlights-items`;
  1696. Object.assign(listContainer.style, {
  1697. display: 'flex',
  1698. flexDirection: 'column',
  1699. gap: '8px',
  1700. overflow: 'auto',
  1701. paddingRight: '8px',
  1702. paddingBottom: '44px',
  1703. width: '100%',
  1704. boxSizing: 'border-box',
  1705. overflowX: 'hidden' // 添加这行来禁止横向滚动
  1706. });
  1707.  
  1708. // 自定义滚动条样式
  1709. const styleEl = document.createElement('style');
  1710. styleEl.textContent = `
  1711. .${STYLE_PREFIX}highlights-items::-webkit-scrollbar {
  1712. width: 5px; /* 更细的滚动条 */
  1713. }
  1714. .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-track {
  1715. background: rgba(255, 255, 255, 0.03); /* 更微妙的轨道 */
  1716. border-radius: 3px;
  1717. }
  1718. .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-thumb {
  1719. background: rgba(255, 255, 255, 0.15); /* 更微妙的滑块 */
  1720. border-radius: 3px;
  1721. }
  1722. .${STYLE_PREFIX}highlights-items::-webkit-scrollbar-thumb:hover {
  1723. background: rgba(255, 255, 255, 0.25);
  1724. }
  1725. `;
  1726. document.head.appendChild(styleEl);
  1727.  
  1728. // 排序高亮,按时间倒序
  1729. const sortedHighlights = [...highlights].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
  1730.  
  1731. if (sortedHighlights.length === 0) {
  1732. // 显示空状态
  1733. const emptyState = document.createElement('div');
  1734. Object.assign(emptyState.style, {
  1735. display: 'flex',
  1736. flexDirection: 'column',
  1737. alignItems: 'center',
  1738. justifyContent: 'center',
  1739. padding: '60px 20px',
  1740. textAlign: 'center',
  1741. color: '#999',
  1742. fontSize: '13px'
  1743. });
  1744.  
  1745. // 使用SVG图标作为空状态图标
  1746. emptyState.innerHTML = `
  1747. <svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.15)"
  1748. stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  1749. <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
  1750. </svg>
  1751. <p style="margin-top:16px; font-size:13px !important;">暂无高亮内容<br>选中文本并点击颜色进行高亮</p>
  1752. `;
  1753. listContainer.appendChild(emptyState);
  1754. } else {
  1755. // 渲染所有高亮项目
  1756. sortedHighlights.forEach((highlight, index) => {
  1757. const highlightItem = createHighlightItem(highlight, index);
  1758. listContainer.appendChild(highlightItem);
  1759. });
  1760. }
  1761.  
  1762. highlightsListContainer.appendChild(listContainer);
  1763.  
  1764. // 创建底部固定按钮栏
  1765. const bottomActionBar = document.createElement('div');
  1766. bottomActionBar.className = `${STYLE_PREFIX}highlights-bottom-actions`;
  1767. Object.assign(bottomActionBar.style, {
  1768. display: 'flex',
  1769. position: 'absolute',
  1770. bottom: '0',
  1771. left: '0',
  1772. right: '0',
  1773. padding: '0 8px 8px 8px', // 增加左右padding以对齐列表内容
  1774. zIndex: '1',
  1775. gap: '10px',
  1776. boxSizing: 'border-box',
  1777. background: 'transparent',
  1778. width: '100%' // 确保宽度100%
  1779. });
  1780. // 创建刷新按钮
  1781. const refreshBtn = document.createElement('button');
  1782. refreshBtn.textContent = '刷新列表';
  1783. Object.assign(refreshBtn.style, {
  1784. flex: '1',
  1785. background: 'rgba(59, 165, 216, 0.12)',
  1786. border: '1px solid rgba(59, 165, 216, 0.08)',
  1787. borderRadius: '4px',
  1788. padding: '8px 12px',
  1789. color: '#3BA5D8',
  1790. fontSize: '13px',
  1791. fontWeight: '500',
  1792. cursor: 'pointer',
  1793. transition: 'all 0.2s ease'
  1794. });
  1795.  
  1796. // 添加悬停效果
  1797. refreshBtn.addEventListener('mouseenter', () => {
  1798. refreshBtn.style.background = 'rgba(59, 165, 216, 0.2)';
  1799. });
  1800. refreshBtn.addEventListener('mouseleave', () => {
  1801. refreshBtn.style.background = 'rgba(59, 165, 216, 0.12)';
  1802. });
  1803.  
  1804. refreshBtn.addEventListener('click', () => {
  1805. // 刷新高亮列表
  1806. loadHighlights();
  1807. applyHighlights();
  1808. renderHighlightsList();
  1809. });
  1810.  
  1811. // 创建清除按钮
  1812. const clearBtn = document.createElement('button');
  1813. clearBtn.textContent = '清除全部';
  1814. Object.assign(clearBtn.style, {
  1815. flex: '1',
  1816. background: 'rgba(255, 82, 82, 0.12)',
  1817. border: '1px solid rgba(255, 82, 82, 0.08)',
  1818. color: '#ff6b6b',
  1819. borderRadius: '4px',
  1820. padding: '8px 12px',
  1821. fontSize: '13px',
  1822. fontWeight: '500',
  1823. cursor: 'pointer',
  1824. transition: 'all 0.2s ease'
  1825. });
  1826.  
  1827. // 添加悬停效果
  1828. clearBtn.addEventListener('mouseenter', () => {
  1829. clearBtn.style.background = 'rgba(255, 82, 82, 0.2)';
  1830. });
  1831. clearBtn.addEventListener('mouseleave', () => {
  1832. clearBtn.style.background = 'rgba(255, 82, 82, 0.12)';
  1833. });
  1834.  
  1835. clearBtn.addEventListener('click', () => {
  1836. if (highlights.length === 0) return;
  1837.  
  1838. // 确认删除
  1839. if (confirm('确定要删除所有高亮吗?此操作不可撤销。')) {
  1840. // 移除DOM中的高亮元素
  1841. document.querySelectorAll(`.${STYLE_PREFIX}highlight-marked`).forEach(el => {
  1842. const textNode = document.createTextNode(el.textContent);
  1843. el.parentNode.replaceChild(textNode, el);
  1844. });
  1845.  
  1846. // 清空高亮数组
  1847. highlights = [];
  1848. saveHighlights();
  1849. renderHighlightsList();
  1850. }
  1851. });
  1852.  
  1853. bottomActionBar.appendChild(refreshBtn);
  1854. bottomActionBar.appendChild(clearBtn);
  1855. highlightsListContainer.appendChild(bottomActionBar);
  1856. }
  1857.  
  1858. updateSidebarHighlights = renderHighlightsList;
  1859.  
  1860. // 创建单个高亮项目
  1861. function createHighlightItem(highlight, index) {
  1862. const item = document.createElement('div');
  1863. item.className = `${STYLE_PREFIX}highlight-item`;
  1864. item.dataset.highlightId = highlight.id;
  1865.  
  1866. Object.assign(item.style, {
  1867. backgroundColor: 'rgba(42, 46, 54, 0.4)',
  1868. borderRadius: '4px',
  1869. padding: '12px 14px',
  1870. position: 'relative',
  1871. transition: 'all 0.15s ease',
  1872. border: '1px solid rgba(255, 255, 255, 0.03)',
  1873. margin: '0 0 8px 0',
  1874. });
  1875.  
  1876. // 颜色指示器
  1877. const colorIndicator = document.createElement('div');
  1878. Object.assign(colorIndicator.style, {
  1879. position: 'absolute',
  1880. top: '0',
  1881. left: '0',
  1882. width: '3px',
  1883. height: '100%',
  1884. backgroundColor: highlight.color,
  1885. borderTopLeftRadius: '12px',
  1886. borderBottomLeftRadius: '12px'
  1887. });
  1888.  
  1889. // 高亮内容
  1890. const content = document.createElement('div');
  1891. Object.assign(content.style, {
  1892. paddingLeft: '4px',
  1893. color: '#E8E9EB',
  1894. fontSize: '14px',
  1895. lineHeight: '1.5',
  1896. marginBottom: '8px',
  1897. wordBreak: 'break-word',
  1898. display: '-webkit-box',
  1899. WebkitBoxOrient: 'vertical',
  1900. WebkitLineClamp: '2',
  1901. overflow: 'hidden',
  1902. textOverflow: 'ellipsis'
  1903. });
  1904.  
  1905. // 处理高亮文本,避免XSS
  1906. const textNode = document.createTextNode(highlight.text);
  1907. content.appendChild(textNode);
  1908.  
  1909. // 底部信息栏
  1910. const infoBar = document.createElement('div');
  1911. Object.assign(infoBar.style, {
  1912. display: 'flex',
  1913. justifyContent: 'space-between',
  1914. alignItems: 'center',
  1915. fontSize: '12px',
  1916. marginTop: '6px',
  1917. paddingLeft: '3px' // 与内容区域左边距统一
  1918. });
  1919.  
  1920. // 时间信息
  1921. const timeInfo = document.createElement('div');
  1922. Object.assign(timeInfo.style, {
  1923. color: '#999',
  1924. fontSize: '12px'
  1925. });
  1926.  
  1927. // 格式化时间
  1928. const date = new Date(highlight.timestamp);
  1929. const formattedDate = `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  1930. timeInfo.textContent = formattedDate;
  1931.  
  1932. // 操作按钮容器
  1933. const actionButtons = document.createElement('div');
  1934. Object.assign(actionButtons.style, {
  1935. display: 'flex',
  1936. gap: '10px'
  1937. });
  1938.  
  1939. // 跳转按钮
  1940. const jumpButton = document.createElement('div');
  1941. Object.assign(jumpButton.style, {
  1942. position: 'relative',
  1943. background: 'none',
  1944. border: 'none',
  1945. padding: '3px',
  1946. cursor: 'pointer',
  1947. color: '#74b4ff',
  1948. display: 'flex',
  1949. alignItems: 'center',
  1950. fontSize: '12px',
  1951. transition: 'color 0.15s ease'
  1952. });
  1953. jumpButton.className = `${STYLE_PREFIX}tooltip-container`;
  1954. jumpButton.innerHTML = `
  1955. <div class="${STYLE_PREFIX}tooltip">跳转到此高亮</div>
  1956. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  1957. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1958. <path d="M15 3h6v6M14 10l6.16-6.16M9 21H3v-6M10 14l-6.16 6.16"></path>
  1959. </svg>
  1960. `;
  1961.  
  1962. jumpButton.addEventListener('mouseenter', () => {
  1963. jumpButton.style.color = '#a0cfff';
  1964. jumpButton.querySelector(`.${STYLE_PREFIX}tooltip`).style.opacity = '1';
  1965. });
  1966.  
  1967. jumpButton.addEventListener('mouseleave', () => {
  1968. jumpButton.style.color = '#74b4ff';
  1969. jumpButton.querySelector(`.${STYLE_PREFIX}tooltip`).style.opacity = '0';
  1970. });
  1971. jumpButton.addEventListener('click', () => {
  1972. scrollToHighlight(highlight.id);
  1973. });
  1974.  
  1975. // 删除按钮
  1976. const deleteButton = document.createElement('div');
  1977. Object.assign(deleteButton.style, {
  1978. position: 'relative',
  1979. background: 'none',
  1980. border: 'none',
  1981. padding: '3px',
  1982. cursor: 'pointer',
  1983. color: 'rgba(190, 60, 60, 0.8)',
  1984. display: 'flex',
  1985. alignItems: 'center',
  1986. fontSize: '12px',
  1987. transition: 'color 0.15s ease'
  1988. });
  1989. deleteButton.className = `${STYLE_PREFIX}tooltip-container`;
  1990. deleteButton.innerHTML = `
  1991. <div class="${STYLE_PREFIX}tooltip">删除此高亮</div>
  1992. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  1993. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1994. <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
  1995. </svg>
  1996. `;
  1997.  
  1998. deleteButton.addEventListener('mouseenter', () => {
  1999. deleteButton.style.color = 'rgba(255, 80, 80, 0.95)';
  2000. deleteButton.querySelector(`.${STYLE_PREFIX}tooltip`).style.opacity = '1';
  2001. });
  2002.  
  2003. deleteButton.addEventListener('mouseleave', () => {
  2004. deleteButton.style.color = 'rgba(190, 60, 60, 0.8)';
  2005. deleteButton.querySelector(`.${STYLE_PREFIX}tooltip`).style.opacity = '0';
  2006. });
  2007. deleteButton.addEventListener('click', () => {
  2008. if(confirm('确定要删除这条高亮吗?')) {
  2009. removeHighlightById(highlight.id);
  2010. renderHighlightsList(); // 重新渲染列表
  2011. }
  2012. });
  2013.  
  2014. actionButtons.appendChild(jumpButton);
  2015. actionButtons.appendChild(deleteButton);
  2016.  
  2017. infoBar.appendChild(timeInfo);
  2018. infoBar.appendChild(actionButtons);
  2019.  
  2020. // 添加项目悬停效果
  2021. item.addEventListener('mouseenter', () => {
  2022. item.style.backgroundColor = 'rgba(59, 165, 216, 0.1)';
  2023. item.style.borderColor = 'rgba(59, 165, 216, 0.15)';
  2024. });
  2025. item.addEventListener('mouseleave', () => {
  2026. item.style.backgroundColor = 'rgba(42, 46, 54, 0.4)';
  2027. item.style.borderColor = 'rgba(255, 255, 255, 0.03)';
  2028. });
  2029.  
  2030.  
  2031. item.appendChild(colorIndicator);
  2032. item.appendChild(content);
  2033. item.appendChild(infoBar);
  2034.  
  2035. return item;
  2036. }
  2037.  
  2038. // 滚动到指定高亮
  2039. function scrollToHighlight(highlightId) {
  2040. const highlightElement = document.querySelector(`.${STYLE_PREFIX}highlight-marked[data-highlight-id="${highlightId}"]`);
  2041. if (highlightElement) {
  2042. // 平滑滚动到元素
  2043. highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  2044.  
  2045. // 添加闪烁效果,并设置临时样式使其更加明显
  2046. highlightElement.classList.add(`${STYLE_PREFIX}highlight-flash`);
  2047.  
  2048. // 保存原有的样式以便恢复
  2049. const originalTransition = highlightElement.style.transition;
  2050.  
  2051. // 添加过渡效果使视觉反馈更平滑
  2052. highlightElement.style.transition = 'all 0.3s ease';
  2053.  
  2054. setTimeout(() => {
  2055. highlightElement.classList.remove(`${STYLE_PREFIX}highlight-flash`);
  2056. highlightElement.style.transition = originalTransition;
  2057. }, 2500);
  2058. }
  2059. }
  2060.  
  2061. // 初始渲染高亮列表
  2062. renderHighlightsList();
  2063.  
  2064. // 渲染启用管理面板内容
  2065. function renderEnabledPanel() {
  2066. const container = sidebar.querySelector(`.${STYLE_PREFIX}disabled-container`);
  2067. if (!container) return;
  2068.  
  2069. // 清空容器
  2070. container.innerHTML = '';
  2071.  
  2072. // 给容器本身设置明确的颜色
  2073. Object.assign(container.style, {
  2074. color: '#f0f0f0' // 确保所有文本默认为浅色
  2075. });
  2076.  
  2077. // 添加当前页面管理区域
  2078. const currentPageSection = document.createElement('div');
  2079. currentPageSection.className = `${STYLE_PREFIX}disabled-section`;
  2080. Object.assign(currentPageSection.style, {
  2081. marginBottom: '20px',
  2082. color: '#f0f0f0' // 明确设置文本颜色
  2083. });
  2084.  
  2085. const currentPageTitle = document.createElement('div');
  2086. currentPageTitle.className = `${STYLE_PREFIX}disabled-title`;
  2087. currentPageTitle.innerHTML = `<span>当前页面</span>`;
  2088. Object.assign(currentPageTitle.style, {
  2089. fontSize: '13px',
  2090. fontWeight: '600',
  2091. color: '#eee', // 确保标题颜色为浅色
  2092. marginBottom: '10px',
  2093. display: 'flex',
  2094. alignItems: 'center',
  2095. gap: '8px'
  2096. });
  2097.  
  2098. // 当前页面状态
  2099. const currentStatus = document.createElement('div');
  2100. currentStatus.className = `${STYLE_PREFIX}current-status`;
  2101. currentStatus.innerHTML = renderCurrentPageStatus();
  2102.  
  2103. currentPageSection.appendChild(currentPageTitle);
  2104. currentPageSection.appendChild(currentStatus);
  2105. container.appendChild(currentPageSection);
  2106.  
  2107. // 启用域名列表区域
  2108. const domainsSection = document.createElement('div');
  2109. domainsSection.className = `${STYLE_PREFIX}disabled-section`;
  2110. Object.assign(domainsSection.style, {
  2111. marginBottom: '20px'
  2112. });
  2113.  
  2114. const domainsTitle = document.createElement('div');
  2115. domainsTitle.className = `${STYLE_PREFIX}disabled-title`;
  2116. domainsTitle.innerHTML = `<span>启用域名列表</span>`;
  2117. Object.assign(domainsTitle.style, {
  2118. fontSize: '13px',
  2119. fontWeight: '600',
  2120. color: '#eee',
  2121. marginBottom: '10px',
  2122. display: 'flex',
  2123. alignItems: 'center',
  2124. gap: '8px'
  2125. });
  2126.  
  2127. const domainsList = document.createElement('div');
  2128. domainsList.className = `${STYLE_PREFIX}domains-list`;
  2129. domainsList.innerHTML = renderEnabledDomains();
  2130.  
  2131. // 添加域名表单
  2132. const addDomainForm = document.createElement('div');
  2133. addDomainForm.className = `${STYLE_PREFIX}add-disabled-form`;
  2134. Object.assign(addDomainForm.style, {
  2135. display: 'flex',
  2136. marginTop: '12px',
  2137. gap: '0'
  2138. });
  2139.  
  2140. const domainInput = document.createElement('input');
  2141. domainInput.className = `${STYLE_PREFIX}add-disabled-input`;
  2142. domainInput.id = 'add-domain-input';
  2143. domainInput.placeholder = '输入域名...';
  2144. Object.assign(domainInput.style, {
  2145. flex: '1',
  2146. backgroundColor: 'rgba(255, 255, 255, 0.07)',
  2147. border: '1px solid rgba(255, 255, 255, 0.1)',
  2148. borderRadius: '4px 0 0 4px',
  2149. padding: '8px 12px',
  2150. fontSize: '13px',
  2151. color: '#fff',
  2152. outline: 'none'
  2153. });
  2154.  
  2155. const addDomainBtn = document.createElement('button');
  2156. addDomainBtn.className = `${STYLE_PREFIX}add-disabled-button`;
  2157. addDomainBtn.id = 'add-domain-btn';
  2158. addDomainBtn.textContent = '添加';
  2159. Object.assign(addDomainBtn.style, {
  2160. backgroundColor: 'rgba(190, 60, 60, 0.8)',
  2161. color: '#f5e0e0',
  2162. border: 'none',
  2163. borderRadius: '0 4px 4px 0',
  2164. padding: '8px 16px',
  2165. fontSize: '13px',
  2166. fontWeight: '500',
  2167. cursor: 'pointer',
  2168. transition: 'all 0.2s ease'
  2169. });
  2170.  
  2171. addDomainForm.appendChild(domainInput);
  2172. addDomainForm.appendChild(addDomainBtn);
  2173.  
  2174. domainsSection.appendChild(domainsTitle);
  2175. domainsSection.appendChild(domainsList);
  2176. domainsSection.appendChild(addDomainForm);
  2177. container.appendChild(domainsSection);
  2178.  
  2179. // 启用URL列表区域
  2180. const urlsSection = document.createElement('div');
  2181. urlsSection.className = `${STYLE_PREFIX}disabled-section`;
  2182.  
  2183. const urlsTitle = document.createElement('div');
  2184. urlsTitle.className = `${STYLE_PREFIX}disabled-title`;
  2185. urlsTitle.innerHTML = `<span>启用网址列表</span>`;
  2186. Object.assign(urlsTitle.style, {
  2187. fontSize: '13px',
  2188. fontWeight: '600',
  2189. color: '#eee',
  2190. marginBottom: '10px',
  2191. display: 'flex',
  2192. alignItems: 'center',
  2193. gap: '8px'
  2194. });
  2195.  
  2196. const urlsList = document.createElement('div');
  2197. urlsList.className = `${STYLE_PREFIX}urls-list`;
  2198. urlsList.innerHTML = renderEnabledUrls();
  2199.  
  2200. // 添加URL表单
  2201. const addUrlForm = document.createElement('div');
  2202. addUrlForm.className = `${STYLE_PREFIX}add-disabled-form`;
  2203. Object.assign(addUrlForm.style, {
  2204. display: 'flex',
  2205. marginTop: '12px',
  2206. gap: '0'
  2207. });
  2208.  
  2209. const urlInput = document.createElement('input');
  2210. urlInput.className = `${STYLE_PREFIX}add-disabled-input`;
  2211. urlInput.id = 'add-url-input';
  2212. urlInput.placeholder = '输入网址...';
  2213. Object.assign(urlInput.style, {
  2214. flex: '1',
  2215. backgroundColor: 'rgba(255, 255, 255, 0.07)',
  2216. border: '1px solid rgba(255, 255, 255, 0.1)',
  2217. borderRadius: '4px 0 0 4px',
  2218. padding: '8px 12px',
  2219. fontSize: '13px',
  2220. color: '#fff',
  2221. outline: 'none'
  2222. });
  2223.  
  2224. const addUrlBtn = document.createElement('button');
  2225. addUrlBtn.className = `${STYLE_PREFIX}add-disabled-button`;
  2226. addUrlBtn.id = 'add-url-btn';
  2227. addUrlBtn.textContent = '添加';
  2228. Object.assign(addUrlBtn.style, {
  2229. backgroundColor: 'rgba(190, 60, 60, 0.8)',
  2230. color: '#f5e0e0',
  2231. border: 'none',
  2232. borderRadius: '0 4px 4px 0',
  2233. padding: '8px 16px',
  2234. fontSize: '13px',
  2235. fontWeight: '500',
  2236. cursor: 'pointer',
  2237. transition: 'all 0.2s ease'
  2238. });
  2239.  
  2240. addUrlForm.appendChild(urlInput);
  2241. addUrlForm.appendChild(addUrlBtn);
  2242.  
  2243. urlsSection.appendChild(urlsTitle);
  2244. urlsSection.appendChild(urlsList);
  2245. urlsSection.appendChild(addUrlForm);
  2246. container.appendChild(urlsSection);
  2247.  
  2248. // 绑定事件
  2249. bindDisabledPanelEvents();
  2250. }
  2251.  
  2252. // 渲染当前页面状态
  2253. function renderCurrentPageStatus() {
  2254. const isDomainEnabled = enabledList.domains.includes(currentDomain);
  2255. const isUrlEnabled = enabledList.urls.includes(currentPageUrl);
  2256.  
  2257. if (isDomainEnabled || isUrlEnabled) {
  2258. return `
  2259. <div class="${STYLE_PREFIX}disabled-item">
  2260. <div class="${STYLE_PREFIX}disabled-info">
  2261. <span>${isDomainEnabled ? `此域名 (${currentDomain}) 已启用高亮` : '此网址已启用高亮'}</span>
  2262. </div>
  2263. <span class="${STYLE_PREFIX}disabled-action" data-type="${isDomainEnabled ? 'domain' : 'url'}" data-value="${isDomainEnabled ? currentDomain : currentPageUrl}">
  2264. 禁用
  2265. </span>
  2266. </div>
  2267. `;
  2268. } else {
  2269. return `
  2270. <div class="${STYLE_PREFIX}current-page-actions">
  2271. <button class="${STYLE_PREFIX}disable-btn" id="enable-domain-btn">
  2272. 启用此域名
  2273. </button>
  2274. <button class="${STYLE_PREFIX}disable-btn" id="enable-url-btn">
  2275. 启用此网址
  2276. </button>
  2277. </div>
  2278. `;
  2279. }
  2280. }
  2281.  
  2282. // 渲染启用域名列表
  2283. function renderEnabledDomains() {
  2284. if (enabledList.domains.length === 0) {
  2285. return `<div class="${STYLE_PREFIX}empty-list">没有启用的域名</div>`;
  2286. }
  2287.  
  2288. return enabledList.domains.map(domain => `
  2289. <div class="${STYLE_PREFIX}disabled-item">
  2290. <div class="${STYLE_PREFIX}disabled-info">
  2291. <span>${domain}</span>
  2292. </div>
  2293. <span class="${STYLE_PREFIX}disabled-action" data-type="domain" data-value="${domain}">
  2294. 删除
  2295. </span>
  2296. </div>
  2297. `).join('');
  2298. }
  2299.  
  2300. // 渲染启用URL列表
  2301. function renderEnabledUrls() {
  2302. if (enabledList.urls.length === 0) {
  2303. return `<div class="${STYLE_PREFIX}empty-list">没有启用的网址</div>`;
  2304. }
  2305.  
  2306. return enabledList.urls.map(url => {
  2307. // 为了美观,截断过长的URL
  2308. const displayUrl = url.length > 40 ? url.substring(0, 37) + '...' : url;
  2309.  
  2310. return `
  2311. <div class="${STYLE_PREFIX}disabled-item" title="${url}">
  2312. <div class="${STYLE_PREFIX}disabled-info">
  2313. <span>${displayUrl}</span>
  2314. </div>
  2315. <span class="${STYLE_PREFIX}disabled-action" data-type="url" data-value="${url}">
  2316. 删除
  2317. </span>
  2318. </div>
  2319. `;
  2320. }).join('');
  2321. }
  2322.  
  2323. // 绑定启用管理面板事件
  2324. function bindDisabledPanelEvents() {
  2325. // 启用当前域名按钮
  2326. const enableDomainBtn = document.getElementById('enable-domain-btn');
  2327. if (enableDomainBtn) {
  2328. enableDomainBtn.addEventListener('click', () => {
  2329. if (confirm('确定要启用域名 "' + currentDomain + '" 的高亮功能吗?')) {
  2330. enableDomain(currentDomain);
  2331. renderEnabledPanel();
  2332. }
  2333. });
  2334. }
  2335.  
  2336. // 启用当前网址按钮
  2337. const enableUrlBtn = document.getElementById('enable-url-btn');
  2338. if (enableUrlBtn) {
  2339. enableUrlBtn.addEventListener('click', () => {
  2340. if (confirm('确定要启用当前网址的高亮功能吗?')) {
  2341. enableUrl(currentPageUrl);
  2342. renderEnabledPanel();
  2343. }
  2344. });
  2345. }
  2346.  
  2347. // 添加样式
  2348. const styleSheet = document.createElement('style');
  2349. styleSheet.textContent = `
  2350. #${STYLE_PREFIX}sidebar,
  2351. #${STYLE_PREFIX}sidebar *,
  2352. .${STYLE_PREFIX}disabled-section,
  2353. .${STYLE_PREFIX}disabled-title,
  2354. .${STYLE_PREFIX}disabled-title span,
  2355. .${STYLE_PREFIX}disabled-item,
  2356. .${STYLE_PREFIX}disabled-info,
  2357. .${STYLE_PREFIX}disabled-info span,
  2358. .${STYLE_PREFIX}empty-list,
  2359. .${STYLE_PREFIX}sidebar-tab,
  2360. .${STYLE_PREFIX}highlight-item {
  2361. color: #E8E9EB !important;
  2362. }
  2363. .${STYLE_PREFIX}disabled-item {
  2364. display: flex;
  2365. justify-content: space-between;
  2366. align-items: center;
  2367. padding: 8px 12px;
  2368. background-color: rgba(42, 46, 54, 0.4);
  2369. border-radius: 4px;
  2370. margin-bottom: 6px;
  2371. transition: all 0.2s ease;
  2372. border: 1px solid rgba(255, 255, 255, 0.03);
  2373. }
  2374.  
  2375. .${STYLE_PREFIX}disabled-item:hover {
  2376. background-color: rgba(59, 165, 216, 0.1);
  2377. border-color: rgba(59, 165, 216, 0.15);
  2378. }
  2379.  
  2380. .${STYLE_PREFIX}disabled-info {
  2381. display: flex;
  2382. align-items: center;
  2383. gap: 8px;
  2384. font-size: 13px;
  2385. color: #E8E9EB;
  2386. flex: 1;
  2387. overflow: hidden;
  2388. text-overflow: ellipsis;
  2389. white-space: nowrap;
  2390. }
  2391.  
  2392. .${STYLE_PREFIX}disabled-action {
  2393. color: #3BA5D8 !important;
  2394. font-size: 12px;
  2395. cursor: pointer;
  2396. padding: 2px 6px;
  2397. border-radius: 3px;
  2398. transition: all 0.2s;
  2399. opacity: 0.85;
  2400. }
  2401.  
  2402. .${STYLE_PREFIX}disabled-action:hover {
  2403. background-color: rgba(59, 165, 216, 0.15);
  2404. opacity: 1;
  2405. }
  2406.  
  2407. .${STYLE_PREFIX}empty-list {
  2408. padding: 10px;
  2409. color: #ADADB8 !important;
  2410. font-style: italic;
  2411. font-size: 13px;
  2412. text-align: center;
  2413. background-color: rgba(42, 46, 54, 0.2);
  2414. border-radius: 4px;
  2415. }
  2416.  
  2417. .${STYLE_PREFIX}current-page-actions {
  2418. display: flex;
  2419. gap: 10px;
  2420. }
  2421.  
  2422. .${STYLE_PREFIX}disable-btn {
  2423. flex: 1;
  2424. background: rgba(59, 165, 216, 0.12);
  2425. border: none;
  2426. border-radius: 4px;
  2427. padding: 8px 12px;
  2428. color: #3BA5D8;
  2429. font-size: 13px;
  2430. font-weight: 500;
  2431. cursor: pointer;
  2432. transition: all 0.2s ease;
  2433. }
  2434.  
  2435. .${STYLE_PREFIX}disable-btn:hover {
  2436. background-color: rgba(59, 165, 216, 0.2);
  2437. }
  2438.  
  2439. .${STYLE_PREFIX}add-disabled-input:focus {
  2440. border-color: #3BA5D8;
  2441. background-color: rgba(255, 255, 255, 0.1);
  2442. }
  2443.  
  2444. .${STYLE_PREFIX}add-disabled-button:hover {
  2445. background-color: #3BA5D8;
  2446. }
  2447. `;
  2448. document.head.appendChild(styleSheet);
  2449.  
  2450. // 删除按钮事件
  2451. document.querySelectorAll(`.${STYLE_PREFIX}disabled-action`).forEach(btn => {
  2452. btn.addEventListener('click', (e) => {
  2453. const type = e.target.dataset.type;
  2454. const value = e.target.dataset.value;
  2455.  
  2456. if (e.target.textContent.trim() === '删除') {
  2457. if (type === 'domain') {
  2458. enabledList.domains = enabledList.domains.filter(d => d !== value);
  2459. } else if (type === 'url') {
  2460. enabledList.urls = enabledList.urls.filter(u => u !== value);
  2461. }
  2462. saveEnabledList();
  2463. renderEnabledPanel();
  2464. } else if (e.target.textContent.trim() === '启用') {
  2465. if (type === 'domain') {
  2466. enableDomain(value);
  2467. } else if (type === 'url') {
  2468. enableUrl(value);
  2469. }
  2470. renderEnabledPanel();
  2471. } else if (e.target.textContent.trim() === '禁用') {
  2472. // 添加对禁用按钮的处理
  2473. if (type === 'domain') {
  2474. disableDomain(value);
  2475. } else if (type === 'url') {
  2476. disableUrl(value);
  2477. }
  2478. saveEnabledList();
  2479. renderEnabledPanel();
  2480. }
  2481. });
  2482. });
  2483.  
  2484. // 添加域名按钮
  2485. const addDomainBtn = document.getElementById('add-domain-btn');
  2486. if (addDomainBtn) {
  2487. Object.assign(addDomainBtn.style, {
  2488. backgroundColor: 'rgba(59, 165, 216, 0.8)',
  2489. color: '#f0f8ff',
  2490. border: 'none',
  2491. borderRadius: '0 4px 4px 0',
  2492. padding: '8px 16px',
  2493. fontSize: '13px',
  2494. fontWeight: '500',
  2495. cursor: 'pointer',
  2496. transition: 'all 0.2s ease'
  2497. });
  2498. addDomainBtn.addEventListener('click', () => {
  2499. const input = document.getElementById('add-domain-input');
  2500. const domain = input.value.trim();
  2501.  
  2502. if (domain) {
  2503. if (!enabledList.domains.includes(domain)) {
  2504. enabledList.domains.push(domain);
  2505. saveEnabledList();
  2506. input.value = '';
  2507. renderEnabledPanel();
  2508. } else {
  2509. alert('该域名已在启用列表中');
  2510. }
  2511. }
  2512. });
  2513. }
  2514.  
  2515. // 添加URL按钮
  2516. const addUrlBtn = document.getElementById('add-url-btn');
  2517. if (addUrlBtn) {
  2518. Object.assign(addUrlBtn.style, {
  2519. backgroundColor: 'rgba(59, 165, 216, 0.8)',
  2520. color: '#f0f8ff',
  2521. });
  2522. addUrlBtn.addEventListener('click', () => {
  2523. const input = document.getElementById('add-url-input');
  2524. const url = input.value.trim();
  2525.  
  2526. if (url) {
  2527. if (!enabledList.urls.includes(url)) {
  2528. enabledList.urls.push(url);
  2529. saveEnabledList();
  2530. input.value = '';
  2531. renderEnabledPanel();
  2532. } else {
  2533. alert('该网址已在启用列表中');
  2534. }
  2535. }
  2536. });
  2537. }
  2538.  
  2539. // 输入框回车事件
  2540. const domainInput = document.getElementById('add-domain-input');
  2541. if (domainInput) {
  2542. domainInput.addEventListener('keydown', (e) => {
  2543. if (e.key === 'Enter') {
  2544. document.getElementById('add-domain-btn').click();
  2545. }
  2546. });
  2547. }
  2548.  
  2549. const urlInput = document.getElementById('add-url-input');
  2550. if (urlInput) {
  2551. urlInput.addEventListener('keydown', (e) => {
  2552. if (e.key === 'Enter') {
  2553. document.getElementById('add-url-btn').click();
  2554. }
  2555. });
  2556. }
  2557. }
  2558.  
  2559. // 初始渲染启用管理面板
  2560. renderEnabledPanel();
  2561. }
  2562.  
  2563. function init() {
  2564. loadHighlights();
  2565. registerEvents();
  2566. if (document.readyState === 'complete') {
  2567. setTimeout(() => {
  2568. applyHighlights();
  2569. observeDomChanges();
  2570. }, 500);
  2571. } else {
  2572. window.addEventListener('load', () => {
  2573. setTimeout(() => {
  2574. applyHighlights();
  2575. observeDomChanges();
  2576. }, 500);
  2577. });
  2578. }
  2579. // 注册(不可用)油猴菜单命令
  2580. GM_registerMenuCommand('打开侧边栏', () => {
  2581. toggleSidebar(true);
  2582. });
  2583. GM_registerMenuCommand('切换浮动按钮显示/隐藏', toggleFloatingButton);
  2584. GM_registerMenuCommand('启用当前域名高亮', () => {
  2585. if (enabledList.domains.includes(currentDomain)) {
  2586. alert(`当前域名(${currentDomain})已启用高亮功能`);
  2587. } else {
  2588. if (confirm(`确定要启用当前域名(${currentDomain})的高亮功能吗?`)) {
  2589. enableDomain(currentDomain);
  2590. }
  2591. }
  2592. });
  2593. GM_registerMenuCommand('启用当前网址高亮', () => {
  2594. if (enabledList.urls.includes(currentPageUrl)) {
  2595. alert(`当前网址已启用高亮功能`);
  2596. } else {
  2597. if (confirm(`确定要启用当前网址的高亮功能吗?`)) {
  2598. enableUrl(currentPageUrl);
  2599. }
  2600. }
  2601. });
  2602. }
  2603.  
  2604. init();
  2605. createFloatingButtonAndSidebar();
  2606. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址