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