// ==UserScript== // @name Linux Do Hub LDC 汇率转换器 // @namespace https://linux.do/ // @version 2.3.1 // @description 修复关闭转换时表格金额未还原的问题:还原改为按快照属性全局反查 // @author Tabbit // @match https://hub.linux.do/* // @run-at document-end // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const EXCHANGE_RATE = 50; // 1 平台币 = 50 LDC const STORAGE_KEY = 'ldc_converter_enabled'; let isEnabled = localStorage.getItem(STORAGE_KEY) === '1'; const DOLLAR_REGEX = /(-?)\$([\d,]+(?:\.\d+)?)(?!\s*\/\s*[\d,]+(?:\.\d+)?\s*LDC)/g; const CELL_REGEX = /^(-?)(\$?)([\d,]+(?:\.\d+)?)(\s*(?:\/\s*1?[MK万]?|USD|美元)?\s*)$/i; const HEADER_HINTS = ['USD', '价格', '金额', '余额', '费用', 'PRICE', 'COST']; const btn = document.createElement('button'); btn.textContent = isEnabled ? '恢复平台币' : '开启 LDC 模式'; btn.style.cssText = 'position:fixed;top:10px;right:10px;z-index:2147483647;pointer-events:auto;' + 'padding:6px 12px;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;' + 'box-shadow:0 2px 4px rgba(0,0,0,.1);background:' + (isEnabled ? '#10b981' : '#3b82f6'); document.body.appendChild(btn); const toLdc = s => (parseFloat(String(s).replace(/,/g, '')) * EXCHANGE_RATE).toFixed(4); /* ========== 全量还原:按快照属性反查,不依赖表格结构与列号 ========== */ function restoreAll() { document.querySelectorAll('[data-ldc-orig]').forEach(el => { // 表格单元格 el.innerText = el.dataset.ldcOrig; delete el.dataset.ldcOrig; }); document.querySelectorAll('[data-ldc-orig-html]').forEach(el => { // 卡片/概览区 el.innerHTML = el.dataset.ldcOrigHtml; delete el.dataset.ldcOrigHtml; delete el.dataset.ldcConverted; }); document.querySelectorAll('[data-ldc-orig-header]').forEach(el => { // 表头 el.innerText = el.dataset.ldcOrigHeader; delete el.dataset.ldcOrigHeader; }); } /* ========== 1. 表格:按 USD 列转换(仅负责开启方向) ========== */ function processTables() { document.querySelectorAll('table, [role="table"], [role="grid"]').forEach(table => { const headerCells = [...new Set(table.querySelectorAll('th, [role="columnheader"]'))]; const usdCols = new Set(); headerCells.forEach((th, idx) => { if (th.dataset.ldcOrigHeader !== undefined) { usdCols.add(idx); return; } // 已转换过的列 const text = (th.innerText || '').trim(); if (HEADER_HINTS.some(k => text.toUpperCase().includes(k))) { th.dataset.ldcOrigHeader = text; th.innerText = text.includes('USD') ? text.replace(/USD/g, 'LDC') : text + ' (LDC)'; usdCols.add(idx); } }); if (!usdCols.size) return; // 开启方向才受此约束,还原已剥离到 restoreAll table.querySelectorAll('tbody tr, [role="row"]').forEach(row => { row.querySelectorAll('td, [role="cell"], [role="gridcell"]').forEach((cell, idx) => { if (!usdCols.has(idx)) return; const leaves = [...cell.querySelectorAll('span, div')].filter(e => !e.children.length); const targets = leaves.length ? leaves : [cell]; targets.forEach(el => { if (el.dataset.ldcOrig !== undefined) return; // 已转换 const text = (el.innerText || '').trim(); const m = text.match(CELL_REGEX); if (m && (m[2] === '$' || m[4].trim() === '' || /\/|USD|美元/i.test(m[4]))) { el.dataset.ldcOrig = text; el.innerText = m[1] + toLdc(m[3]) + m[4]; } }); }); }); }); } /* ========== 2. 非表格区域:追加 "/ xx LDC"(仅负责开启方向) ========== */ function processCards() { document.querySelectorAll('div, span, p, h2').forEach(el => { if (el.children.length || !el.innerText || !el.innerText.includes('$')) return; if (el.closest('table, [role="table"], [role="grid"]')) return; if (el.dataset.ldcConverted === '1') return; DOLLAR_REGEX.lastIndex = 0; if (!DOLLAR_REGEX.test(el.innerText)) return; DOLLAR_REGEX.lastIndex = 0; el.dataset.ldcOrigHtml = el.innerHTML; el.innerHTML = el.innerText.replace(DOLLAR_REGEX, (m, sign, amt) => `${m} / ${sign}${toLdc(amt)} LDC`); el.dataset.ldcConverted = '1'; }); } const run = () => { processTables(); processCards(); }; btn.addEventListener('click', () => { isEnabled = !isEnabled; localStorage.setItem(STORAGE_KEY, isEnabled ? '1' : '0'); btn.textContent = isEnabled ? '恢复平台币' : '开启 LDC 模式'; btn.style.background = isEnabled ? '#10b981' : '#3b82f6'; isEnabled ? run() : restoreAll(); // 关闭时走全量快照还原 }); if (isEnabled) { setTimeout(run, 300); setTimeout(run, 1000); } let t1, t2; new MutationObserver(() => { if (!btn.isConnected) document.body.appendChild(btn); if (!isEnabled) return; clearTimeout(t1); clearTimeout(t2); t1 = setTimeout(run, 120); t2 = setTimeout(run, 600); }).observe(document.body, { childList: true, subtree: true, characterData: true }); })();