// ==UserScript== // @name Destiny2_Term_replace // @namespace your-namespace // @version 2.3 // @description 替换网页中出现的命运2术语 // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect 20xiji.github.io // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/524822/Destiny2_Term_replace.user.js // @updateURL https://update.greasyfork.icu/scripts/524822/Destiny2_Term_replace.meta.js // ==/UserScript== (function() { 'use strict'; const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/Destiny2_term.json'; let replacementHistory = []; let termMap = new Map(); let currentMode = 1; let dialogVisible = false; let dialogXOffset = 0; let dialogYOffset = 0; let isDragging = false; let posObjs = []; let hintDialogVisible = 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; overflow: visible; } #textReplacerDialog.dragging { cursor: grabbing; } #dialogHeader { cursor: grab; margin-bottom: 10px; } #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; } .dialogButton { /* 统一关闭和提示按钮样式 */ position: absolute; top: 8px; width: 12px; height: 12px; border-radius: 50%; background-color: #ff6058; border: 1px solid #e0443e; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 0 rgba(0,0,0,.1); padding: 0; z-index: 10000; } .dialogButton:hover { background-color: #f0413a; border-color: #d02828; } .dialogButton::before { content: ''; display: block; width: 8px; height: 8px; border-radius: 50%; background-color: #fff; transform: scale(0.5); /* 调整小白点初始大小 */ opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; /* 添加transform过渡 */ } .dialogButton:hover::before { opacity: 1; transform: scale(1); } #dialogCloseButton { right: 8px; } #dialogHintButton { right: 30px; /* 提示按钮位置在关闭按钮左侧 */ background-color: #ffc107; /* 提示按钮颜色 */ border-color: #e0a300; } #dialogHintButton:hover { background-color: #f0b200; border-color: #d09500; } #dialogHintButton:hover::before { background-color: #333; /* 提示按钮悬停小白点颜色 */ } #hintDialog { position: fixed; top: 60px; /* 调整提示框的垂直位置 */ right: 20px; background: #333; color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); z-index: 10001; /* 确保提示框在最上层 */ width: 300px; /* 调整宽度 */ font-size: 14px; line-height: 1.6; display: none; /* 初始隐藏 */ } #hintDialog p { margin-bottom: 10px; } #hintDialog p:last-child { margin-bottom: 0; } `); const dialog = document.createElement('div'); dialog.id = 'textReplacerDialog'; const dialogHeader = document.createElement('div'); dialogHeader.id = 'dialogHeader'; dialogHeader.style.margin = '0 0 10px 0'; dialogHeader.style.fontSize = '16px'; dialogHeader.textContent = '文本替换工具 '; dialog.appendChild(dialogHeader); const termCountSpan = document.createElement('span'); termCountSpan.id = 'termCount'; termCountSpan.textContent = '(加载中...)'; dialogHeader.appendChild(termCountSpan); const modeButtonsDiv = document.createElement('div'); modeButtonsDiv.id = 'modeButtons'; const modeButton1 = document.createElement('button'); modeButton1.className = 'mode-btn'; modeButton1.dataset.mode = '1'; modeButton1.textContent = '中文模式'; modeButtonsDiv.appendChild(modeButton1); const modeButton2 = document.createElement('button'); modeButton2.className = 'mode-btn'; modeButton2.dataset.mode = '2'; modeButton2.textContent = '英文|中文'; modeButtonsDiv.appendChild(modeButton2); const modeButton3 = document.createElement('button'); modeButton3.className = 'mode-btn'; modeButton3.dataset.mode = '3'; modeButton3.textContent = '中文(英文)'; modeButtonsDiv.appendChild(modeButton3); const actionButtonsDiv = document.createElement('div'); actionButtonsDiv.id = 'actionButtons'; const btnApplyAll = document.createElement('button'); btnApplyAll.id = 'btnApplyAll'; btnApplyAll.textContent = '应用规则'; actionButtonsDiv.appendChild(btnApplyAll); const btnUndo = document.createElement('button'); btnUndo.id = 'btnUndo'; btnUndo.textContent = '撤销'; btnUndo.disabled = true; actionButtonsDiv.appendChild(btnUndo); const btnClearCache = document.createElement('button'); btnClearCache.id = 'btnClearCache'; btnClearCache.textContent = '清除缓存'; actionButtonsDiv.appendChild(btnClearCache); const closeButton = document.createElement('button'); closeButton.id = 'dialogCloseButton'; closeButton.className = 'dialogButton'; // 添加统一样式类 closeButton.addEventListener('click', toggleDialog); dialog.appendChild(closeButton); // 新增提示按钮 const hintButton = document.createElement('button'); hintButton.id = 'dialogHintButton'; hintButton.className = 'dialogButton'; // 添加统一样式类 hintButton.addEventListener('click', toggleHintDialog); // 添加点击事件监听器 dialog.appendChild(hintButton); // 创建提示对话框 const hintDialog = document.createElement('div'); hintDialog.id = 'hintDialog'; hintDialog.textContent = `

网页多层嵌套操作说明:

当处理采用多层嵌套结构的网页时,系统表现如下特点:

  1. 非快捷键触发场景
    当用户使用不使用快捷键调用功能面板时,由于网页存在多层嵌套,系统会同时激活两个功能面板。这两个面板各自对应不同层级网页的替换操作需求。
  2. 快捷键触发场景
    当用户使用快捷键调用功能面板时,系统会根据当前鼠标点击位置智能判定目标层级,此时呼出的面板仅作用于用户当前操作的网页层级。
    (说明:网页结构的多层嵌套特性导致了不同触发方式下的面板响应差异,自动触发会启动全量面板,而快捷键触发则是上下文感知的精准响应)
`; document.body.appendChild(hintDialog); dialog.appendChild(modeButtonsDiv); dialog.appendChild(actionButtonsDiv); 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') { toggleDialog(); } }); GM_registerMenuCommand("打开文本替换工具", toggleDialog); document.addEventListener('click', (e) => { if (e.target.matches('.gm-open-text-replacer')) { toggleDialog(); } }); // Make dialog draggable dialogHeader.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', dragMove); document.addEventListener('mouseup', dragEnd); function dragStart(e) { isDragging = true; dialog.classList.add('dragging'); dialogXOffset = dialog.offsetLeft - e.clientX; dialogYOffset = dialog.offsetTop - e.clientY; } function dragMove(e) { if (!isDragging) return; dialog.style.left = e.clientX + dialogXOffset + 'px'; dialog.style.top = e.clientY + dialogYOffset + 'px'; } function dragEnd() { isDragging = false; dialog.classList.remove('dragging'); } initTerminology(); updateButtonStates(); function toggleDialog() { dialogVisible = !dialogVisible; dialog.style.display = dialogVisible ? 'block' : 'none'; updateButtonStates(); if (dialogVisible && hintDialogVisible) { // 关闭主面板时同时关闭提示框 toggleHintDialog(); } } function toggleHintDialog() { hintDialogVisible = !hintDialogVisible; hintDialog.style.display = hintDialogVisible ? 'block' : 'none'; if (hintDialogVisible && dialogVisible === false) { // 如果提示框显示时主面板未显示,则同时显示主面板 toggleDialog(); } } 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 (data && data.data && Object.keys(data.data).length > 0) { resolve(data.data); } else { reject(new Error('获取到空数据或data.data为空')); } } 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; } } })();