网页文本高亮 (自动保存) - 极速版

在网页上划词高亮,支持“划词即高亮”的全局自动模式。双击高亮区域可删除。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页文本高亮 (自动保存) - 极速版
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  在网页上划词高亮,支持“划词即高亮”的全局自动模式。双击高亮区域可删除。
// @description:en Highlight text on web pages, supports global "Instant Highlight" mode. Double-click to remove.
// @author       luoluoluo
// @license      MIT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // === 配置项 ===
    const CONFIG = {
        storageKey: 'tampermonkey_page_highlights', // 高亮数据存储键名 (本地)
        settingKey: 'tampermonkey_highlight_auto_mode', // 设置存储键名 (全局)
        highlightTag: 'tm-mark',
        highlightClass: 'tm-highlight-span',
        color: '#B9B962',      // 橄榄黄
        textColor: '#000000'   // 黑色文字
    };

    // 读取全局设置
    let isAutoMode = GM_getValue(CONFIG.settingKey, false);

    // === 样式注入 ===
    const style = document.createElement('style');
    style.innerHTML = `
        /* 高亮区域样式 */
        .${CONFIG.highlightClass} {
            background-color: ${CONFIG.color} !important;
            color: ${CONFIG.textColor} !important;
            cursor: pointer;
            border-bottom: 2px solid rgba(0,0,0,0.2);
            border-radius: 3px;
            padding: 0 2px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
            text-decoration: none !important;
            transition: background-color 0.2s;
        }
        .${CONFIG.highlightClass}:hover {
            opacity: 0.9;
        }
        
        /* 悬浮工具条容器 */
        #tm-action-bar {
            position: absolute;
            display: none;
            background: #333;
            border-radius: 4px;
            z-index: 2147483647;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            font-family: sans-serif;
            font-size: 13px;
            overflow: hidden;
            white-space: nowrap;
        }

        /* 工具条按钮通用样式 */
        .tm-bar-btn {
            display: inline-block;
            padding: 6px 12px;
            color: #fff;
            cursor: pointer;
            transition: background 0.2s;
            user-select: none;
        }
        .tm-bar-btn:hover { background: #555; }
        
        /* 高亮按钮 */
        #tm-btn-highlight { border-right: 1px solid #555; }
        #tm-btn-highlight::after { content: "🖊️ 标记"; }
        
        /* 自动模式开关 (在工具条上) */
        #tm-btn-toggle-auto { color: #aaa; }
        #tm-btn-toggle-auto:hover { color: #ffeb3b; }
        #tm-btn-toggle-auto::after { content: "⚡ 自动"; }

        /* 右下角全局状态指示器 (仅在自动模式开启时显示) */
        #tm-auto-indicator {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 40px;
            height: 40px;
            background: ${CONFIG.color};
            color: #000;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            cursor: pointer;
            z-index: 2147483647;
            opacity: 0.5;
            transition: all 0.3s;
            border: 2px solid #fff;
        }
        #tm-auto-indicator:hover {
            opacity: 1;
            transform: scale(1.1);
        }
        #tm-auto-indicator::after { content: "⚡"; }
        #tm-auto-indicator:hover::after { content: "🛑"; font-size: 16px; } /* hover时变成停止图标 */
        
    `;
    document.head.appendChild(style);

    // 创建悬浮工具条
    const actionBar = document.createElement('div');
    actionBar.id = 'tm-action-bar';
    actionBar.innerHTML = `
        <div id="tm-btn-highlight" class="tm-bar-btn" title="手动高亮"></div>
        <div id="tm-btn-toggle-auto" class="tm-bar-btn" title="点击开启:划词直接高亮 (全局生效)"></div>
    `;
    document.body.appendChild(actionBar);

    // 创建右下角指示器
    const indicator = document.createElement('div');
    indicator.id = 'tm-auto-indicator';
    indicator.title = "自动高亮模式已开启。点击关闭。";
    document.body.appendChild(indicator);
    
    // 初始化显示状态
    updateUIState();

    // ============================
    // 逻辑控制层
    // ============================

    function updateUIState() {
        if (isAutoMode) {
            indicator.style.display = 'flex';
            actionBar.style.display = 'none'; // 自动模式下不显示工具条
        } else {
            indicator.style.display = 'none';
        }
    }

    function toggleAutoMode() {
        isAutoMode = !isAutoMode;
        GM_setValue(CONFIG.settingKey, isAutoMode);
        updateUIState();
        
        // 简单的提示
        showToast(isAutoMode ? "⚡ 自动高亮已开启 (全局)" : "🖊️ 已切换回手动模式");
    }

    function showToast(msg) {
        const toast = document.createElement('div');
        toast.style.cssText = `
            position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
            background: rgba(0,0,0,0.8); color: white; padding: 8px 16px;
            border-radius: 4px; font-size: 14px; z-index: 999999; pointer-events: none;
            transition: opacity 0.5s;
        `;
        toast.innerText = msg;
        document.body.appendChild(toast);
        setTimeout(() => { toast.style.opacity = 0; setTimeout(() => toast.remove(), 500); }, 2000);
    }

    // ============================
    // 数据持久化层 (保持不变)
    // ============================
    const Store = {
        get: () => { try { return JSON.parse(localStorage.getItem(CONFIG.storageKey)) || {}; } catch(e) { return {}; } },
        save: (data) => { try { localStorage.setItem(CONFIG.storageKey, JSON.stringify(data)); } catch(e) {} },
        getPageKey: () => window.location.pathname + window.location.search,
        add: (info) => {
            const store = Store.get();
            const key = Store.getPageKey();
            if (!store[key]) store[key] = [];
            store[key].push(info);
            Store.save(store);
        },
        remove: (id) => {
            const store = Store.get();
            const key = Store.getPageKey();
            if (store[key]) {
                store[key] = store[key].filter(item => item.id !== id);
                if (store[key].length === 0) delete store[key];
                Store.save(store);
            }
        },
        clearPage: () => {
            const store = Store.get();
            delete store[Store.getPageKey()];
            Store.save(store);
            location.reload();
        }
    };

    // ============================
    // DOM 操作层
    // ============================
    function getPathTo(element) {
        if (element.id !== '') return 'id("' + element.id + '")';
        if (element === document.body) return element.tagName;
        let ix = 0;
        const siblings = element.parentNode.childNodes;
        for (let i = 0; i < siblings.length; i++) {
            const sibling = siblings[i];
            if (sibling === element) return getPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']';
            if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++;
        }
    }

    function highlightRange(range, id = null) {
        try {
            const mark = document.createElement(CONFIG.highlightTag);
            mark.className = CONFIG.highlightClass;
            mark.dataset.id = id || Date.now().toString(36) + Math.random().toString(36).substr(2);
            mark.title = "双击删除";
            mark.appendChild(range.extractContents());
            range.insertNode(mark);
            return { id: mark.dataset.id, text: mark.innerText, path: getPathTo(mark.parentNode) };
        } catch (e) { return null; }
    }

    function restoreHighlights() {
        const store = Store.get();
        const items = store[Store.getPageKey()] || [];
        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
        let node;
        while(node = walker.nextNode()) {
            items.forEach(item => {
                 if (node.nodeValue.includes(item.text) && node.parentNode.tagName !== CONFIG.highlightTag.toUpperCase()) {
                     try {
                         const range = document.createRange();
                         const start = node.nodeValue.indexOf(item.text);
                         range.setStart(node, start);
                         range.setEnd(node, start + item.text.length);
                         const mark = document.createElement(CONFIG.highlightTag);
                         mark.className = CONFIG.highlightClass;
                         mark.dataset.id = item.id;
                         mark.title = "双击删除";
                         range.surroundContents(mark);
                     } catch (e) {}
                 }
            });
        }
    }

    // ============================
    // 事件交互层
    // ============================

    // 1. 划词处理
    document.addEventListener('mouseup', (e) => {
        if (actionBar.contains(e.target) || indicator.contains(e.target)) return;
        
        setTimeout(() => {
            const selection = window.getSelection();
            const text = selection.toString().trim();
            
            if (text.length > 1) {
                const range = selection.getRangeAt(0);
                
                // === 分支逻辑 ===
                if (isAutoMode) {
                    // A. 自动模式:直接高亮
                    const info = highlightRange(range);
                    if (info) {
                        Store.add(info);
                        selection.removeAllRanges();
                    }
                } else {
                    // B. 手动模式:显示工具条
                    const rect = range.getBoundingClientRect();
                    let top = window.scrollY + rect.top - 40;
                    let left = window.scrollX + rect.left + (rect.width / 2) - 50; // 稍微修正居中
                    
                    if (top < 0) top = window.scrollY + rect.bottom + 10;
                    if (left < 0) left = 10;

                    actionBar.style.display = 'block';
                    actionBar.style.top = top + 'px';
                    actionBar.style.left = left + 'px';
                    
                    // 绑定按钮事件
                    const btnHighlight = document.getElementById('tm-btn-highlight');
                    const btnAuto = document.getElementById('tm-btn-toggle-auto');
                    
                    btnHighlight.onclick = (evt) => {
                        evt.stopPropagation();
                        const info = highlightRange(range);
                        if (info) {
                            Store.add(info);
                            selection.removeAllRanges();
                            actionBar.style.display = 'none';
                        }
                    };
                    
                    btnAuto.onclick = (evt) => {
                        evt.stopPropagation();
                        toggleAutoMode(); // 开启自动模式
                        actionBar.style.display = 'none'; // 隐藏工具条
                    };
                }
            } else {
                actionBar.style.display = 'none';
            }
        }, 10);
    });

    // 2. 隐藏工具条
    document.addEventListener('mousedown', (e) => {
        if (!actionBar.contains(e.target)) {
            actionBar.style.display = 'none';
        }
    });

    // 3. 点击指示器关闭自动模式
    indicator.onclick = () => {
        toggleAutoMode();
    };

    // 4. 双击删除
    document.addEventListener('dblclick', (e) => {
        if (e.target.classList.contains(CONFIG.highlightClass)) {
            const id = e.target.dataset.id;
            const text = e.target.innerText;
            const parent = e.target.parentNode;
            parent.replaceChild(document.createTextNode(text), e.target);
            parent.normalize();
            Store.remove(id);
        }
    });

    // 5. 加载还原
    window.addEventListener('load', () => {
        setTimeout(restoreHighlights, 500);
        setTimeout(restoreHighlights, 2000);
    });
    
    // 6. 注册油猴菜单
    GM_registerMenuCommand("⚡ 切换自动高亮模式", toggleAutoMode);
    GM_registerMenuCommand("🗑️ 清空当前页面所有高亮", () => {
        if(confirm('清空当前页面所有记录?')) Store.clearPage();
    });

})();