Bungie.net术语替换_可扩展到其他网站

在网页中替换文本,支持三种显示模式和术语表替换

目前為 2025-01-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bungie.net术语替换_可扩展到其他网站
// @namespace    your-namespace
// @version      1.0.3
// @description  在网页中替换文本,支持三种显示模式和术语表替换
// @match        https://www.bungie.net/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      20xiji.github.io
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/merged_sorted_data-250126.json';
    let replacementHistory = [];
    let termMap = new Map();
    let currentMode = 1;
    let dialogVisible = false;

    GM_addStyle(`
        #textReplacerDialog {
            position: fixed;
            top: 20px;
            right: 20px;
            background: #1a1a1a;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.25);
            z-index: 9999;
            width: 260px;
            font-family: Arial, sans-serif;
            color: #fff;
            display: none;
        }
        #modeButtons {
            display: grid;
            gap: 8px;
            margin: 12px 0;
        }
        .mode-btn {
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: #333;
            color: #888;
            cursor: pointer;
            transition: all 0.2s;
        }
        .mode-btn.active {
            background: #4CAF50;
            color: #fff;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }
        #actionButtons {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 12px;
        }
        #actionButtons button {
            flex: 1;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: #4CAF50;
            color: white;
            cursor: pointer;
            min-width: 80px;
        }
        #actionButtons button:disabled {
            background: #666;
            cursor: not-allowed;
        }
        #termCount {
            font-size: 12px;
            color: #888;
            margin-left: 8px;
        }
        #btnClearCache {
            background: #f44336 !important;
        }
    `);

    const dialog = document.createElement('div');
    dialog.id = 'textReplacerDialog';
    dialog.innerHTML = `
        <h3 style="margin:0 0 10px 0;font-size:16px;">文本替换工具 <span id="termCount">(加载中...)</span></h3>
        <div id="modeButtons">
            <button class="mode-btn" data-mode="1">中文模式</button>
            <button class="mode-btn" data-mode="2">英文|中文</button>
            <button class="mode-btn" data-mode="3">中文(英文)</button>
        </div>
        <div id="actionButtons">
            <button id="btnApplyAll">应用规则</button>
            <button id="btnUndo" disabled>撤销</button>
            <button id="btnClearCache">清除缓存</button>
        </div>
    `;
    document.body.appendChild(dialog);

    const elements = {
        modeButtons: dialog.querySelectorAll('.mode-btn'),
        btnApplyAll: dialog.querySelector('#btnApplyAll'),
        btnUndo: dialog.querySelector('#btnUndo'),
        btnClearCache: dialog.querySelector('#btnClearCache'),
        termCount: dialog.querySelector('#termCount')
    };

    elements.modeButtons.forEach(btn => btn.addEventListener('click', handleModeChange));
    elements.btnApplyAll.addEventListener('click', applyAllRules);
    elements.btnUndo.addEventListener('click', undoReplace);
    elements.btnClearCache.addEventListener('click', clearCache);

    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'k') {
            dialogVisible = !dialogVisible;
            dialog.style.display = dialogVisible ? 'block' : 'none';
            updateButtonStates();
        }
    });

    initTerminology();
    updateButtonStates();

    async function clearCache() {
        try {
            // 强制删除现有缓存
            GM_deleteValue('cachedTerms');
            GM_deleteValue('cacheTime');

            // 立即触发重新加载
            const freshData = await fetchTerms();
            termMap = new Map(Object.entries(freshData));
            GM_setValue('cachedTerms', freshData);
            GM_setValue('cacheTime', Date.now());

            updateTermCount();
            alert('✅ 缓存已清除并重新加载成功\n当前种类:武器、护甲、技能、模组\n已加载条目数:' + termMap.size);
        } catch (error) {
            console.error('缓存清除失败:', error);
            alert('❌ 缓存清除失败:' + error.message);
            termMap.clear();
            updateTermCount();
        }
    }

    async function initTerminology() {
        const CACHE_DAYS = 1;
        const cachedData = GM_getValue('cachedTerms');
        const cacheTime = GM_getValue('cacheTime', 0);

        try {
            // 缓存过期或不存在时强制更新
            if (!cachedData || Date.now() - cacheTime > 86400000 * CACHE_DAYS) {
                const freshData = await fetchTerms();
                termMap = new Map(Object.entries(freshData));
                GM_setValue('cachedTerms', freshData);
                GM_setValue('cacheTime', Date.now());
            } else {
                termMap = new Map(Object.entries(cachedData));
            }
        } catch (error) {
            console.error('术语表初始化失败:', error);
            if (cachedData) {
                termMap = new Map(Object.entries(cachedData));
            }
        }
        updateTermCount();
    }

    function updateTermCount() {
        elements.termCount.textContent = termMap.size > 0
            ? `(已加载${termMap.size}条)`
            : '(未加载数据)';
    }

    function fetchTerms() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: ITEM_LIST_URL,
                timeout: 15000,
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        try {
                            const data = JSON.parse(res.responseText);
                            if (Object.keys(data).length > 0) {
                                resolve(data);
                            } else {
                                reject(new Error('获取到空数据'));
                            }
                        } catch (e) {
                            reject(new Error('数据解析失败'));
                        }
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror: (err) => {
                    reject(new Error(`网络错误: ${err}`));
                },
                ontimeout: () => {
                    reject(new Error('请求超时(15秒)'));
                }
            });
        });
    }

    // 其他保持不变的功能函数
    function handleModeChange(e) {
        currentMode = parseInt(e.target.dataset.mode);
        updateButtonStates();
    }

    function updateButtonStates() {
        elements.modeButtons.forEach(btn => {
            btn.classList.toggle('active', parseInt(btn.dataset.mode) === currentMode);
        });
    }

    function applyAllRules() {
        const termRules = Array.from(termMap).map(([en, cn]) => {
            switch (currentMode) {
                case 1: return [en, cn];
                case 2: return [en, `${en} | ${cn}`];
                case 3: return [en, `${cn}(${en})`];
                default: return [en, cn];
            }
        });
        performReplace(termRules);
    }

    function performReplace(rules) {
        const regex = buildRegex(rules);
        const replaceMap = new Map(rules);
        const snapshot = [];

        const walker = document.createTreeWalker(
            document.body,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );

        while (walker.nextNode()) {
            const node = walker.currentNode;
            const original = node.nodeValue;
            const replaced = original.replace(regex, (m) => {
                const foundKey = Array.from(replaceMap.keys()).find(k =>
                    k.toLowerCase() === m.toLowerCase()
                );
                return foundKey ? replaceMap.get(foundKey) : m;
            });

            if (replaced !== original) {
                snapshot.push({ node, text: original });
                node.nodeValue = replaced;
            }
        }

        if (snapshot.length) {
            replacementHistory.push(snapshot);
            elements.btnUndo.disabled = false;
        }
    }

    function buildRegex(rules) {
        const sortedKeys = [...new Set(rules.map(([k]) => k))]
            .sort((a, b) => b.length - a.length)
            .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));

        return new RegExp(`\\b(${sortedKeys.join('|')})\\b`, 'gi');
    }

    function undoReplace() {
        if (replacementHistory.length) {
            const last = replacementHistory.pop();
            last.forEach(({ node, text }) => {
                if (node.parentNode) node.nodeValue = text;
            });
            elements.btnUndo.disabled = !replacementHistory.length;
        }
    }
})();

QingJ © 2025

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