已访问链接样式修改(带预设和自定义)

让点击过的链接变色并显示下划线,支持预设样式和自定义(含中文输入),支持鼠标中键点击和按钮内链接

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         已访问链接样式修改(带预设和自定义)
// @namespace    https://scriptcat.org/zh-CN/search
// @version      0.7
// @description  让点击过的链接变色并显示下划线,支持预设样式和自定义(含中文输入),支持鼠标中键点击和按钮内链接
// @tag          链接 工具 link tools
// @license      MIT
// @author       zzzwq&AI
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @icon         https://img.soogif.com/6QlDQPIFsYakXFhmBt1a02vNk9G5ecYf.gif
// ==/UserScript==

(function() {
    'use strict';

    // 预设选项(保留字体粗度不变)
    const presets = {
        '红色实线': { color: '#FF0000', underline: 'solid' },
        '蓝色虚线': { color: '#0000FF', underline: 'dashed' },
        '绿色波浪线': { color: '#00FF00', underline: 'wavy' },
        '紫色双下划线': { color: '#800080', underline: 'double' },
        '橙色点状线': { color: '#FFA500', underline: 'dotted' }
    };

    // 中文映射表(保留字体粗度不变)
    const colorMap = {
        '红色': '#FF0000',
        '蓝色': '#0000FF',
        '绿色': '#00FF00',
        '黄色': '#FFFF00',
        '黑色': '#000000',
        '白色': '#FFFFFF',
        '灰色': '#808080',
        '橙色': '#FFA500',
        '紫色': '#800080',
        '青色': '#00FFFF',
        '品红': '#FF00FF',
        '透明': 'transparent',
        'red': '#FF0000',
        'blue': '#0000FF',
        'green': '#00FF00',
        'yellow': '#FFFF00',
        'black': '#000000',
        'white': '#FFFFFF',
        'gray': '#808080',
        'orange': '#FFA500',
        'purple': '#800080',
        'cyan': '#00FFFF',
        'magenta': '#FF00FF',
        'transparent': 'transparent'
    };

    const underlineMap = {
        '实线': 'solid',
        '虚线': 'dashed',
        '波浪线': 'wavy',
        '双线': 'double',
        '点状线': 'dotted',
        '无下划线': 'none',
        'solid': 'solid',
        'dashed': 'dashed',
        'wavy': 'wavy',
        'double': 'double',
        'dotted': 'dotted',
        'none': 'none'
    };

    // 获取当前设置
    const settings = {
        color: GM_getValue('linkColor', '#FF0000'),
        underline: GM_getValue('underlineType', 'solid'),
        weight: GM_getValue('linkWeight', '400')
    };

    // 使用Set存储已访问链接(性能优化)
    let visitedLinks = new Set();
    
    // 初始化已访问链接数据
    function initVisitedLinks() {
        try {
            const stored = GM_getValue('visitedLinks', '[]');
            const links = JSON.parse(stored);
            visitedLinks = new Set(links);
        } catch (e) {
            console.warn('Failed to load visited links:', e);
            visitedLinks = new Set();
        }
    }
    
    // 保存已访问链接到存储
    function saveVisitedLinks() {
        try {
            GM_setValue('visitedLinks', JSON.stringify([...visitedLinks]));
        } catch (e) {
            console.warn('Failed to save visited links:', e);
        }
    }

    // 生成提示信息的辅助函数 - 优化显示
    function generatePromptMessage(map, type) {
        const keys = Object.keys(map);
        const half = Math.ceil(keys.length / 2);
        let message = '支持输入以下值(中文/英文均可):\n';
        
        for (let i = 0; i < half; i++) {
            const key1 = keys[i];
            const key2 = keys[i + half];
            const value1 = map[key1];
            const value2 = key2 ? map[key2] : '';
            
            // 格式化显示为两列
            const col1 = `${key1} (${value1})`;
            const col2 = key2 ? `${key2} (${value2})` : '';
            message += `${col1.padEnd(18)}${col2}\n`;
        }
        
        message += `\n其他格式:${type === 'color' ? '十六进制/RGB' : 'CSS标准值'}`;
        return message;
    }

    // 更新样式(使用更具体的选择器提高优先级)
    function updateStyles() {
        const css = `
            a.visited-link {
                color: ${settings.color} !important;
                text-decoration: ${settings.underline} underline !important;
                font-weight: ${settings.weight} !important;
            }
            
            /* 针对特殊网站增加更具体的选择器 */
            .menu-list a.visited-link,
            .nav-item a.visited-link,
            .list-item a.visited-link,
            .item a.visited-link,
            li a.visited-link,
            td a.visited-link,
            span a.visited-link,
            p a.visited-link,
            div a.visited-link {
                color: ${settings.color} !important;
                text-decoration: ${settings.underline} underline !important;
                font-weight: ${settings.weight} !important;
            }
        `;
        
        // 移除旧样式(如果存在)
        const oldStyle = document.getElementById('visited-links-style');
        if (oldStyle) {
            oldStyle.remove();
        }
        
        // 添加新样式
        GM_addStyle(css);
    }

    // 注册设置菜单
    function registerMenuCommands() {
        // 添加预设选项(不修改字体粗度)
        Object.entries(presets).forEach(([name, style]) => {
            GM_registerMenuCommand(`预设:${name}`, () => {
                settings.color = style.color;
                settings.underline = style.underline;
                GM_setValue('linkColor', style.color);
                GM_setValue('underlineType', style.underline);
                updateStyles();
                applyVisitedStyles(); // 重新应用样式
            });
        });

        // 自定义颜色(支持中文)
        GM_registerMenuCommand('自定义颜色', () => {
            const promptMessage = generatePromptMessage(colorMap, 'color');
            const input = prompt(promptMessage, settings.color);
            if (input) {
                const color = colorMap[input] || input;
                if (isValidColor(color)) {
                    settings.color = color;
                    GM_setValue('linkColor', color);
                    updateStyles();
                    applyVisitedStyles(); // 重新应用样式
                } else {
                    alert('无效的颜色值!');
                }
            }
        });

        // 自定义下划线(支持中文)
        GM_registerMenuCommand('自定义下划线', () => {
            const promptMessage = generatePromptMessage(underlineMap, 'underline');
            const input = prompt(promptMessage, settings.underline);
            if (input) {
                const underline = underlineMap[input] || input;
                if (isValidUnderline(underline)) {
                    settings.underline = underline;
                    GM_setValue('underlineType', underline);
                    updateStyles();
                    applyVisitedStyles(); // 重新应用样式
                } else {
                    alert('无效的下划线样式!');
                }
            }
        });

        // 自定义字体粗度(独立设置)
        GM_registerMenuCommand('自定义字体粗度', () => {
            const input = prompt('请输入字体粗度(100-900之间的整数):', settings.weight);
            if (input) {
                const weight = parseInt(input);
                if (weight >= 100 && weight <= 900 && !isNaN(weight)) {
                    settings.weight = weight.toString();
                    GM_setValue('linkWeight', weight);
                    updateStyles();
                    applyVisitedStyles(); // 重新应用样式
                } else {
                    alert('请输入100到900之间的整数!');
                }
            }
        });

        // 清除已访问链接记录
        GM_registerMenuCommand('清除已访问链接记录', () => {
            if (confirm('确定要清除所有已访问链接的记录吗?')) {
                visitedLinks.clear();
                GM_deleteValue('visitedLinks');
                
                // 使用更高效的方式移除类
                const visitedElements = document.querySelectorAll('.visited-link');
                for (let i = 0; i < visitedElements.length; i++) {
                    visitedElements[i].classList.remove('visited-link');
                }
            }
        });
    }

    // 验证函数
    function isValidColor(value) {
        // 创建临时元素测试颜色值
        const temp = document.createElement('div');
        temp.style.color = 'rgb(0, 0, 0)';
        temp.style.color = value;
        return temp.style.color !== 'rgb(0, 0, 0)';
    }

    function isValidUnderline(value) {
        const validValues = ['solid', 'dashed', 'wavy', 'double', 'dotted', 'none'];
        return validValues.includes(value);
    }

    // 处理链接标记
    function markLinkAsVisited(link) {
        const href = link.href;
        if (!href || visitedLinks.has(href)) return;
        
        visitedLinks.add(href);
        link.classList.add('visited-link');
        
        // 使用防抖保存,避免频繁写入
        clearTimeout(window.visitedLinksSaveTimeout);
        window.visitedLinksSaveTimeout = setTimeout(saveVisitedLinks, 1000);
    }

    // 处理链接交互的通用函数
    function handleLinkInteraction(target) {
        // 向上查找最近的链接
        let link = target.closest('a');
        if (link && link.href) {
            markLinkAsVisited(link);
        }
    }

    // 初始应用样式 - 增强版本
    function applyVisitedStyles() {
        // 使用更高效的选择器
        const links = document.querySelectorAll('a[href]');
        const { length } = links;
        
        for (let i = 0; i < length; i++) {
            const link = links[i];
            if (visitedLinks.has(link.href)) {
                link.classList.add('visited-link');
            }
        }
    }

    // 检查并修复样式应用
    function checkAndFixStyles() {
        const visitedElements = document.querySelectorAll('.visited-link');
        for (let i = 0; i < visitedElements.length; i++) {
            const element = visitedElements[i];
            // 检查是否实际应用了样式
            const computedStyle = window.getComputedStyle(element);
            if (computedStyle.color !== settings.color.replace(/\s+/g, '') && 
                computedStyle.textDecorationLine.indexOf('underline') === -1) {
                // 重新添加类
                element.classList.remove('visited-link');
                setTimeout(() => {
                    element.classList.add('visited-link');
                }, 10);
            }
        }
    }

    // 初始化
    function init() {
        // 初始化已访问链接
        initVisitedLinks();
        
        // 更新样式
        updateStyles();
        
        // 注册菜单命令
        registerMenuCommands();
        
        // 初始应用样式
        setTimeout(applyVisitedStyles, 500); // 延迟执行以确保DOM完全加载

        // 使用事件委托提高性能
        document.addEventListener('click', function(e) {
            handleLinkInteraction(e.target);
        });

        // 中键点击事件处理
        document.addEventListener('auxclick', function(e) {
            if (e.button === 1) { // 鼠标中键
                handleLinkInteraction(e.target);
            }
        });

        // 按钮点击事件处理
        document.addEventListener('click', function(e) {
            const button = e.target.closest('button');
            if (button) {
                // 查找按钮内的链接
                const linkInButton = button.querySelector('a[href]');
                if (linkInButton) {
                    markLinkAsVisited(linkInButton);
                }
            }
        });

        // 使用更高效的DOM变化观察
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length) {
                    // 只对新添加的节点应用样式
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1) { // 元素节点
                            const links = node.querySelectorAll ? node.querySelectorAll('a[href]') : [];
                            for (let i = 0; i < links.length; i++) {
                                if (visitedLinks.has(links[i].href)) {
                                    links[i].classList.add('visited-link');
                                }
                            }
                        }
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        // 添加定期检查机制,确保样式正确应用(更温和的方式)
        let checkCount = 0;
        const maxChecks = 5;
        const checkInterval = setInterval(() => {
            if (checkCount >= maxChecks) {
                clearInterval(checkInterval);
                return;
            }
            checkAndFixStyles();
            checkCount++;
        }, 1000);
    }

    // 延迟初始化以提高页面加载性能
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 100); // 延迟执行以减少对页面加载的影响
    }
})();