// ==UserScript== // @name Epic 价格自动换算 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 自动识别全球各区货币并换算人民币,智能处理数字格式,解决所有已知问题 // @author ashes // @match https://store.epicgames.com/* // @connect open.er-api.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; const TAG_CLASS = 'epic-price-tag-final'; const CURRENCY_MAP = { '₺': 'TRY', 'TRY': 'TRY', 'TL': 'TRY', '₴': 'UAH', 'UAH': 'UAH', '₸': 'KZT', 'KZT': 'KZT', '₹': 'INR', 'Rs': 'INR', 'INR': 'INR', 'Rp': 'IDR', 'IDR': 'IDR', 'R$': 'BRL', 'BRL': 'BRL', 'ARS': 'ARS', 'ARS$': 'ARS', 'Mex$': 'MXN', 'MXN': 'MXN', 'COP': 'COP', 'COL$': 'COP', 'R': 'ZAR', 'ZAR': 'ZAR', '₫': 'VND', 'VND': 'VND', '₱': 'PHP', 'PHP': 'PHP', 'S/.': 'PEN', 'PEN': 'PEN', 'zł': 'PLN', 'PLN': 'PLN', '$': 'USD', 'US$': 'USD', 'USD': 'USD', '€': 'EUR', 'EUR': 'EUR', '£': 'GBP', 'GBP': 'GBP' }; const SYMBOLS_REGEX = Object.keys(CURRENCY_MAP).map(k => { const escaped = k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return /^[a-zA-Z]+$/.test(k) ? `\\b${escaped}\\b` : escaped; }).join('|'); const PRICE_REGEX = new RegExp(`(${SYMBOLS_REGEX})\\s?([\\d.,]+)|([\\d.,]+)\\s?(${SYMBOLS_REGEX})`); let rateCache = GM_getValue('rate_cache_final', {}); function getRate(code) { if (['CNY', 'RMB', '¥'].includes(code)) return 1; const now = Date.now(); const cache = rateCache[code]; if (cache && (now - cache.time < 86400000)) return cache.rate; if (!cache || !cache.fetching) { if (!rateCache[code]) rateCache[code] = {}; rateCache[code].fetching = true; GM_xmlhttpRequest({ method: "GET", url: `https://open.er-api.com/v6/latest/${code}`, onload: (res) => { try { const data = JSON.parse(res.responseText); if (data.rates && data.rates.CNY) { rateCache[code] = { rate: data.rates.CNY, time: Date.now(), fetching: false }; GM_setValue('rate_cache_final', rateCache); convert(true); } } catch (e) { rateCache[code].fetching = false; } } }); } return null; } function parsePrice(str) { let s = str.replace(/[^\d.,]/g, ''); if (s.includes(',') && s.includes('.')) { return s.lastIndexOf(',') < s.lastIndexOf('.') ? parseFloat(s.replace(/,/g, '')) : parseFloat(s.replace(/\./g, '').replace(',', '.')); } if (s.includes(',')) return parseFloat(s.replace(/,/g, '')); return parseFloat(s); } function convert(forceUpdate = false) { if (forceUpdate) document.querySelectorAll(`.${TAG_CLASS}`).forEach(el => el.remove()); const elements = document.querySelectorAll('span, div, p, b, strong, em'); for (let i = 0; i < elements.length; i++) { const el = elements[i]; if (el.childElementCount > 0 || ['H1','H2','H3','H4','H5','H6'].includes(el.tagName)) continue; if (el.classList.contains(TAG_CLASS) || el.querySelector(`.${TAG_CLASS}`)) continue; const text = el.innerText; if (text.length > 40 || text.length < 2) continue; if (text.includes('%') || !/\d/.test(text)) continue; const match = text.match(PRICE_REGEX); if (match) { const symbol = match[1] || match[4]; const numStr = match[2] || match[3]; const code = CURRENCY_MAP[symbol]; if (code) { const rate = getRate(code); if (rate) { const price = parsePrice(numStr); if (!isNaN(price) && price > 0.5) { const rmb = (price * rate).toFixed(2); const tag = document.createElement('span'); tag.className = TAG_CLASS; tag.style.cssText = "color: #22c55e !important; font-weight: bold; margin-left: 6px; font-family: sans-serif; font-size: 0.9em;"; tag.innerText = `(≈¥${rmb})`; el.appendChild(tag); el.dataset.epicProcessed = "true"; } } } } } } let timeout; const observer = new MutationObserver((mutations) => { let shouldTrigger = false; for (const m of mutations) { if (m.type === 'childList' && m.addedNodes.length > 0) { if (m.addedNodes[0].className !== TAG_CLASS) { shouldTrigger = true; break; } } } if (shouldTrigger) { clearTimeout(timeout); timeout = setTimeout(() => convert(), 350); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(convert, 500); })();