// ==UserScript==
// @name ✨💱汇率转换器|货币换算|Currency Converter - 美元/欧元/比特币价格实时转换✨
// @name:zh-CN ✨💱汇率转换器|货币换算 - 价格转换工具✨
// @name:en ✨💱Currency Converter|Exchange Rate - Real-time Price Conversion✨
// @name:ja ✨💱為替換算|通貨変換 - リアルタイム価格コンバーター✨
// @name:ko ✨💱환율 변환기|통화 계산기 - 실시간 가격 변환✨
// @namespace https://greasyfork.org/en/scripts/553280-%E5%85%A8%E8%83%BD%E8%B4%A7%E5%B8%81%E8%BD%AC%E6%8D%A2%E5%99%A8-universal-currency-converter?locale_override=1
// @version 1.7.5
// @description 💰汇率换算工具|实时货币转换器|价格转换助手💰 自动识别USD美元/EUR欧元/CNY人民币/JPY日元/GBP英镑/KRW韩元等127种货币(含BTC比特币/ETH以太坊/USDT等加密货币)。支持淘宝/天猫/京东/拼多多/Amazon亚马逊/eBay/Steam游戏/阿里云/AWS等全网站。鼠标悬停显示汇率弹窗tooltip,支持外汇牌价查询、跨境电商海淘、游戏充值、VPS服务器购买等场景。Currency Exchange Rate Converter Price Calculator Bitcoin Crypto 換算 両替 為替レート
// @description:zh-CN 💰汇率换算|货币转换|价格计算💰 网页价格自动识别转换工具,支持美元/欧元/人民币/日元/英镑/韩元等127种货币。淘宝天猫京东Amazon海淘Steam游戏充值阿里云服务器全支持。实时外汇牌价查询,跨境电商必备神器。
// @description:en 💰Currency Converter|Exchange Rate Calculator|Price Conversion Tool💰 Auto-detect & convert USD/EUR/CNY/JPY/GBP/KRW + 127 currencies including Bitcoin/Ethereum crypto. Works on Amazon/eBay/Taobao/Steam/AWS. Hover tooltip shows real-time forex rates. Perfect for cross-border shopping, gaming, cloud services. 汇率 货币 両替 換算
// @description:ja 💰為替換算ツール|通貨コンバーター|価格変換💰 USD/EUR/JPY/CNY等127通貨(ビットコイン/イーサリアム含)を自動認識変換。Amazon/楽天/Steam/AWSで利用可能。マウスホバーでリアルタイム為替レート表示。越境EC・ゲーム課金・クラウドサービス購入に最適。Currency Exchange Rate 汇率 환율
// @description:ko 💰환율 계산기|통화 변환기|가격 변환 도구💰 USD/EUR/CNY/JPY/KRW 등 127개 통화(비트코인/이더리움 포함) 자동 인식 변환. Amazon/쿠팡/Steam/AWS 지원. 마우스 호버로 실시간 환율 툴팁 표시. 해외직구/게임충전/클라우드서비스 구매 필수. Currency Converter 汇率 為替
// @author FronNian
// @copyright 2025, FronNian (huayuan4564@gmail.com)
// @match *://*/*
// @match *://*.youtube.com/*
// @match *://*.twitch.tv/*
// @match *://*.bilibili.com/*
// @match *://*.douyin.com/*
// @match *://*.tiktok.com/*
// @match *://*.kuaishou.com/*
// @match *://*.gifshow.com/*
// @match *://*.huya.com/*
// @match *://*.douyu.com/*
// @match *://*.xiaohongshu.com/*
// @match *://*.xhslink.com/*
// @match *://*.netflix.com/*
// @match *://*.primevideo.com/*
// @match *://*.disneyplus.com/*
// @match *://*.hulu.com/*
// @match *://*.kick.com/*
// @match *://*.rumble.com/*
// @match *://*.vimeo.com/*
// @match *://*.dailymotion.com/*
// @match *://*.nicovideo.jp/*
// @match *://*.afreecatv.com/*
// @match *://*.naver.com/*
// @match *://*.youku.com/*
// @match *://*.iqiyi.com/*
// @match *://*.qq.com/*
// @match *://*.mgtv.com/*
// @match *://*.acfun.cn/*
// @match *://*.weibo.com/*
// @match *://*.weishi.qq.com/*
// @match *://*.huoshan.com/*
// @match *://*.ixigua.com/*
// @match *://*.v.qq.com/*
// @match *://*.live.com/*
// @match *://*.mixer.com/*
// @match *://*.facebook.com/*
// @match *://*.instagram.com/*
// @match *://*.twitter.com/*
// @match *://*.x.com/*
// @match *://*.amazon.com/*
// @match *://*.amazon.cn/*
// @match *://*.amazon.co.jp/*
// @match *://*.amazon.co.uk/*
// @match *://*.amazon.de/*
// @match *://*.amazon.fr/*
// @match *://*.ebay.com/*
// @match *://*.aliexpress.com/*
// @match *://*.taobao.com/*
// @match *://*.tmall.com/*
// @match *://*.jd.com/*
// @match *://*.pinduoduo.com/*
// @match *://*.shopify.com/*
// @match *://*.etsy.com/*
// @match *://*.walmart.com/*
// @match *://*.bestbuy.com/*
// @match *://*.target.com/*
// @match *://*.steam.com/*
// @match *://*.epicgames.com/*
// @match *://*.playstation.com/*
// @match *://*.xbox.com/*
// @match *://*.nintendo.com/*
// @match *://*.gog.com/*
// @match *://*.origin.com/*
// @match *://*.ea.com/*
// @match *://*.ubisoft.com/*
// @match *://*.ubisoftconnect.com/*
// @match *://*.battle.net/*
// @match *://*.blizzard.com/*
// @match *://*.riotgames.com/*
// @match *://*.leagueoflegends.com/*
// @match *://*.valorant.com/*
// @match *://*.humblebundle.com/*
// @match *://*.fanatical.com/*
// @match *://*.greenmangaming.com/*
// @match *://*.cdkeys.com/*
// @match *://*.kinguin.net/*
// @match *://*.g2a.com/*
// @match *://*.gamersgate.com/*
// @match *://*.indiegala.com/*
// @match *://*.itch.io/*
// @match *://*.gamebillet.com/*
// @match *://*.gamesplanet.com/*
// @match *://*.nuuvem.com/*
// @match *://*.dlgamer.com/*
// @match *://*.wingamestore.com/*
// @match *://*.gamestop.com/*
// @match *://*.playasia.com/*
// @match *://*.razer.com/*
// @match *://*.logitechg.com/*
// @match *://*.corsair.com/*
// @match *://*.nzxt.com/*
// @match *://*.game.qq.com/*
// @match *://*.wegame.com/*
// @match *://*.tgp.qq.com/*
// @match *://*.yx.tv/*
// @match *://*.youxi.com/*
// @match *://*.3dmgame.com/*
// @match *://*.ali213.net/*
// @match *://*.gamersky.com/*
// @match *://*.3dm.com/*
// @match *://*.coinmarketcap.com/*
// @match *://*.coingecko.com/*
// @match *://*.binance.com/*
// @match *://*.coinbase.com/*
// @match *://*.kraken.com/*
// @match *://*.booking.com/*
// @match *://*.airbnb.com/*
// @match *://*.expedia.com/*
// @match *://*.trip.com/*
// @match *://*.ctrip.com/*
// @match *://*.agoda.com/*
// @match *://*.hotels.com/*
// @match *://*.priceline.com/*
// @match *://*.kayak.com/*
// @match *://*.trivago.com/*
// @match *://*.skyscanner.com/*
// @match *://*.momondo.com/*
// @match *://*.hotwire.com/*
// @match *://*.orbitz.com/*
// @match *://*.travelocity.com/*
// @match *://*.cheaptickets.com/*
// @match *://*.marriott.com/*
// @match *://*.hilton.com/*
// @match *://*.hyatt.com/*
// @match *://*.ihg.com/*
// @match *://*.accor.com/*
// @match *://*.radisson.com/*
// @match *://*.wyndham.com/*
// @match *://*.choicehotels.com/*
// @match *://*.bestwestern.com/*
// @match *://*.hostelworld.com/*
// @match *://*.hostelbookers.com/*
// @match *://*.vrbo.com/*
// @match *://*.vacationrentals.com/*
// @match *://*.homeaway.com/*
// @match *://*.flipkey.com/*
// @match *://*.tripadvisor.com/*
// @match *://*.yelp.com/*
// @match *://*.opentable.com/*
// @match *://*.rentalcars.com/*
// @match *://*.enterprise.com/*
// @match *://*.hertz.com/*
// @match *://*.avis.com/*
// @match *://*.budget.com/*
// @match *://*.europcar.com/*
// @match *://*.sixt.com/*
// @match *://*.thrifty.com/*
// @match *://*.alamo.com/*
// @match *://*.dollar.com/*
// @match *://*.national.com/*
// @match *://*.viator.com/*
// @match *://*.getyourguide.com/*
// @match *://*.klook.com/*
// @match *://*.tiqets.com/*
// @match *://*.musement.com/*
// @match *://*.trainline.com/*
// @match *://*.rome2rio.com/*
// @match *://*.omio.com/*
// @match *://*.12306.cn/*
// @match *://*.qunar.com/*
// @match *://*.elong.com/*
// @match *://*.tuniu.com/*
// @match *://*.lvmama.com/*
// @match *://*.mafengwo.cn/*
// @match *://*.qyer.com/*
// @match *://*.meituan.com/*
// @match *://*.dianping.com/*
// @match *://*.fliggy.com/*
// @match *://*.alitrip.com/*
// @match *://*.aws.amazon.com/*
// @match *://*.console.aws.amazon.com/*
// @match *://*.cloud.google.com/*
// @match *://*.console.cloud.google.com/*
// @match *://*.azure.microsoft.com/*
// @match *://*.portal.azure.com/*
// @match *://*.digitalocean.com/*
// @match *://*.vultr.com/*
// @match *://*.linode.com/*
// @match *://*.hetzner.com/*
// @match *://*.ovh.com/*
// @match *://*.ovhcloud.com/*
// @match *://*.cloudflare.com/*
// @match *://*.heroku.com/*
// @match *://*.vercel.com/*
// @match *://*.netlify.com/*
// @match *://*.railway.app/*
// @match *://*.render.com/*
// @match *://*.fly.io/*
// @match *://*.aliyun.com/*
// @match *://*.cloud.tencent.com/*
// @match *://*.console.cloud.tencent.com/*
// @match *://*.huaweicloud.com/*
// @match *://*.console.huaweicloud.com/*
// @match *://*.cloud.baidu.com/*
// @match *://*.ucloud.cn/*
// @match *://*.qiniu.com/*
// @match *://*.upyun.com/*
// @match *://*.qcloud.com/*
// @match *://*.bandwagonhost.com/*
// @match *://*.bwh88.net/*
// @match *://*.kiwivm.64clouds.com/*
// @match *://*.virmach.com/*
// @match *://*.hostwinds.com/*
// @match *://*.contabo.com/*
// @match *://*.racknerd.com/*
// @match *://*.hosthatch.com/*
// @match *://*.buyvm.net/*
// @match *://*.namecheap.com/*
// @match *://*.godaddy.com/*
// @match *://*.bluehost.com/*
// @match *://*.hostgator.com/*
// @match *://*.siteground.com/*
// @match *://*.dreamhost.com/*
// @match *://*.a2hosting.com/*
// @match *://*.inmotion.com/*
// @match *://*.greengeeks.com/*
// @match *://*.spotify.com/*
// @match *://*.apple.com/*
// @match *://*.icloud.com/*
// @match *://*.adobe.com/*
// @match *://*.creativecloud.com/*
// @match *://*.microsoft.com/*
// @match *://*.office.com/*
// @match *://*.office365.com/*
// @match *://*.canva.com/*
// @match *://*.figma.com/*
// @match *://*.notion.so/*
// @match *://*.notion.site/*
// @match *://*.slack.com/*
// @match *://*.discord.com/*
// @match *://*.zoom.us/*
// @match *://*.dropbox.com/*
// @match *://*.onedrive.live.com/*
// @match *://*.box.com/*
// @match *://*.mega.nz/*
// @match *://*.pcloud.com/*
// @match *://*.sync.com/*
// @match *://*.nordvpn.com/*
// @match *://*.expressvpn.com/*
// @match *://*.surfshark.com/*
// @match *://*.protonvpn.com/*
// @match *://*.cyberghostvpn.com/*
// @match *://*.privateinternetaccess.com/*
// @match *://*.ipvanish.com/*
// @match *://*.tunnelbear.com/*
// @match *://*.udemy.com/*
// @match *://*.coursera.org/*
// @match *://*.edx.org/*
// @match *://*.skillshare.com/*
// @match *://*.linkedin.com/*
// @match *://*.pluralsight.com/*
// @match *://*.datacamp.com/*
// @match *://*.codecademy.com/*
// @match *://*.udacity.com/*
// @match *://*.domestika.com/*
// @match *://*.masterclass.com/*
// @match *://*.grammarly.com/*
// @match *://*.quillbot.com/*
// @match *://*.overleaf.com/*
// @match *://*.medium.com/*
// @match *://*.substack.com/*
// @match *://*.patreon.com/*
// @match *://*.buymeacoffee.com/*
// @match *://*.ko-fi.com/*
// @match *://*.github.com/*
// @match *://*.gitlab.com/*
// @match *://*.bitbucket.org/*
// @match *://*.jira.atlassian.com/*
// @match *://*.trello.com/*
// @match *://*.asana.com/*
// @match *://*.monday.com/*
// @match *://*.clickup.com/*
// @match *://*.airtable.com/*
// @match *://*.coda.io/*
// @match *://*.evernote.com/*
// @match *://*.onenote.com/*
// @match *://*.goodnotes.com/*
// @match *://*.bear.app/*
// @match *://*.obsidian.md/*
// @match *://*.roamresearch.com/*
// @match *://*.todoist.com/*
// @match *://*.any.do/*
// @match *://*.ticktick.com/*
// @match *://*.chatgpt.com/*
// @match *://*.openai.com/*
// @match *://*.anthropic.com/*
// @match *://*.claude.ai/*
// @match *://*.midjourney.com/*
// @match *://*.stability.ai/*
// @match *://*.fireflies.ai/*
// @match *://*.otter.ai/*
// @match *://*.jasper.ai/*
// @match *://*.copy.ai/*
// @match *://*.writesonic.com/*
// @match *://*.rytr.me/*
// @match *://*.mailchimp.com/*
// @match *://*.sendinblue.com/*
// @match *://*.constantcontact.com/*
// @match *://*.activecampaign.com/*
// @match *://*.convertkit.com/*
// @match *://*.calendly.com/*
// @match *://*.acuityscheduling.com/*
// @match *://*.setmore.com/*
// @match *://*.zapier.com/*
// @match *://*.ifttt.com/*
// @match *://*.make.com/*
// @match *://*.integromat.com/*
// @match *://*.semrush.com/*
// @match *://*.ahrefs.com/*
// @match *://*.moz.com/*
// @match *://*.similarweb.com/*
// @match *://*.hotjar.com/*
// @match *://*.crazyegg.com/*
// @match *://*.optimizely.com/*
// @match *://*.vwo.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @connect v6.exchangerate-api.com
// @connect api.fixer.io
// @connect api.currencyapi.com
// @connect ipapi.co
// @connect api.coingecko.com
// @license GPL-3.0-or-later
// @icon https://raw.githubusercontent.com/FronNian/Currency-Converter/f34fe97c36eb706e51e6b8d252ea63f6da620797/assets/icon.svg
// @run-at document-idle
// @homepage https://greasyfork.org/zh-CN/scripts/553280-%E5%85%A8%E8%83%BD%E8%B4%A7%E5%B8%81%E8%BD%AC%E6%8D%A2%E5%99%A8-universal-currency-converter
// @supportURL https://greasyfork.org/en/scripts/553280-%E5%85%A8%E8%83%BD%E8%B4%A7%E5%B8%81%E8%BD%AC%E6%8D%A2%E5%99%A8-universal-currency-converter?locale_override=1
// @downloadURL https://update.greasyfork.icu/scripts/553280/%E2%9C%A8%F0%9F%92%B1%E6%B1%87%E7%8E%87%E8%BD%AC%E6%8D%A2%E5%99%A8%7C%E8%B4%A7%E5%B8%81%E6%8D%A2%E7%AE%97%7CCurrency%20Converter%20-%20%E7%BE%8E%E5%85%83%E6%AC%A7%E5%85%83%E6%AF%94%E7%89%B9%E5%B8%81%E4%BB%B7%E6%A0%BC%E5%AE%9E%E6%97%B6%E8%BD%AC%E6%8D%A2%E2%9C%A8.user.js
// @updateURL https://update.greasyfork.icu/scripts/553280/%E2%9C%A8%F0%9F%92%B1%E6%B1%87%E7%8E%87%E8%BD%AC%E6%8D%A2%E5%99%A8%7C%E8%B4%A7%E5%B8%81%E6%8D%A2%E7%AE%97%7CCurrency%20Converter%20-%20%E7%BE%8E%E5%85%83%E6%AC%A7%E5%85%83%E6%AF%94%E7%89%B9%E5%B8%81%E4%BB%B7%E6%A0%BC%E5%AE%9E%E6%97%B6%E8%BD%AC%E6%8D%A2%E2%9C%A8.meta.js
// ==/UserScript==
(function() {
'use strict';
/*
* 全能货币转换器 - Universal Currency Converter
* Copyright (C) 2025 FronNian (huayuan4564@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 如果您修改了此代码,请:
* 1. 保留原作者信息(FronNian - huayuan4564@gmail.com)
* 2. 注明修改内容
* 3. 使用相同的GPL-3.0许可证
* 4. 建议通知原作者(邮箱或GreasyFork评论区)
*
* 完整许可证: https://www.gnu.org/licenses/gpl-3.0.txt
*/
// API密钥配置
// ExchangeRate-API: 04529d4768099d362afffc31
// Fixer.io: 147078d87fed12fc4266aa216b3c98c9
// CurrencyAPI: cur_live_cqiOETlTuk2UvLSDONtdIxhTZIlq6PPElZ9wtxlv
/* ==================== 货币名称映射 ==================== */
/**
* 货币中文名称映射(57种主流货币)
*/
const CURRENCY_NAMES_ZH = {
// 主要货币
'USD': '美元', 'EUR': '欧元', 'GBP': '英镑', 'JPY': '日元', 'CHF': '瑞士法郎',
// 亚洲
'CNY': '人民币', 'HKD': '港币', 'TWD': '新台币', 'KRW': '韩元', 'SGD': '新加坡元',
'THB': '泰铢', 'MYR': '马来西亚林吉特', 'IDR': '印尼盾', 'PHP': '菲律宾比索', 'VND': '越南盾',
'INR': '印度卢比', 'PKR': '巴基斯坦卢比', 'BDT': '孟加拉塔卡', 'LKR': '斯里兰卡卢比', 'NPR': '尼泊尔卢比',
// 大洋洲
'AUD': '澳元', 'NZD': '新西兰元',
// 北美
'CAD': '加元', 'MXN': '墨西哥比索',
// 南美
'BRL': '巴西雷亚尔', 'ARS': '阿根廷比索', 'CLP': '智利比索', 'COP': '哥伦比亚比索', 'PEN': '秘鲁索尔',
// 欧洲
'RUB': '卢布', 'PLN': '波兰兹罗提', 'CZK': '捷克克朗', 'HUF': '匈牙利福林', 'RON': '罗马尼亚列伊',
'BGN': '保加利亚列弗', 'HRK': '克罗地亚库纳', 'SEK': '瑞典克朗', 'NOK': '挪威克朗', 'DKK': '丹麦克朗',
'ISK': '冰岛克朗', 'TRY': '土耳其里拉', 'UAH': '乌克兰格里夫纳',
// 中东
'AED': '阿联酋迪拉姆', 'SAR': '沙特里亚尔', 'QAR': '卡塔尔里亚尔', 'KWD': '科威特第纳尔',
'BHD': '巴林第纳尔', 'OMR': '阿曼里亚尔', 'JOD': '约旦第纳尔', 'ILS': '以色列新谢克尔', 'EGP': '埃及镑',
// 非洲
'ZAR': '南非兰特', 'NGN': '尼日利亚奈拉', 'KES': '肯尼亚先令', 'GHS': '加纳塞地',
'MAD': '摩洛哥迪拉姆', 'TND': '突尼斯第纳尔', 'DZD': '阿尔及利亚第纳尔'
};
/* ==================== 默认配置 ==================== */
/**
* 默认配置对象
* @type {Object}
*/
const DEFAULT_CONFIG = {
// 界面语言
language: 'auto', // auto: 自动检测, zh-CN, en, ja, ko
// 排除的域名(不进行货币转换)
excludedDomains: ['localhost', '127.0.0.1', 'xe.com', 'wise.com'],
// 目标货币列表(最多5个,可在设置中修改)
targetCurrencies: ['CNY', 'USD', 'EUR', 'GBP', 'JPY'],
// 智能货币显示
autoDetectLocation: true, // 根据IP自动检测用户所在国家
excludeSourceCurrency: true, // 排除原货币(如价格是USD就不显示USD转换)
userCountryCurrency: null, // 用户所在国家货币(自动检测后保存)
maxDisplayCurrencies: 3, // 最多显示的货币数量
// 内联显示模式
inlineMode: false, // 直接在价格旁显示转换结果,无需悬停
inlineShowCurrency: 'CNY', // 内联模式显示的货币(默认显示第一个)
// 自定义汇率(离线模式)
enableCustomRates: false, // 启用自定义汇率
customRates: { // 自定义汇率表(基准货币:USD)
// 示例:'CNY': 7.25 表示 1 USD = 7.25 CNY
},
// API密钥配置(主密钥)
apiKeys: {
exchangeRateApi: '04529d4768099d362afffc31',
fixer: '147078d87fed12fc4266aa216b3c98c9',
currencyapi: 'cur_live_cqiOETlTuk2UvLSDONtdIxhTZIlq6PPElZ9wtxlv'
},
// API密钥池(备用密钥,用于轮换)
apiKeyPools: {
exchangeRateApi: [], // 用户可添加多个备用密钥
fixer: [],
currencyapi: []
},
// 当前使用的密钥索引(用于轮换)
currentKeyIndex: {
exchangeRateApi: 0,
fixer: 0,
currencyapi: 0
},
// 缓存配置
cacheExpiry: 3600000, // 1小时(毫秒)
cryptoCacheExpiry: 300000, // 加密货币缓存5分钟(波动大)
// 加密货币支持
enableCrypto: false, // 启用加密货币识别和转换
cryptoCurrencies: ['BTC', 'ETH', 'USDT', 'BNB', 'SOL', 'XRP', 'ADA', 'DOGE', 'DOT', 'MATIC'],
showCryptoInTooltip: true, // 在工具提示中显示加密货币
cryptoApiKey: '', // CoinGecko Pro API Key (可选,免费版无需)
// UI配置
tooltipDelay: 500, // 工具提示显示延迟(毫秒,推荐300-800)
tooltipTheme: 'gradient', // 工具提示主题:gradient | light | dark
// 性能配置
enableLazyLoad: true, // 启用懒加载
scanOnIdle: true, // 在空闲时扫描
// 识别配置
minAmount: 0.01, // 最小金额
maxAmount: 999999999, // 最大金额
};
/* ==================== 配置管理模块 ==================== */
/**
* 配置管理器类
* 负责用户配置的加载、保存、获取和重置
*/
class ConfigManager {
constructor() {
this.config = this.load();
}
/**
* 从GM_storage加载配置
* @returns {Object} 配置对象
*/
load() {
try {
const saved = GM_getValue('cc_config');
if (saved) {
const parsedConfig = JSON.parse(saved);
// 合并默认配置和已保存配置,确保向后兼容
const mergedConfig = { ...DEFAULT_CONFIG, ...parsedConfig };
// 检查是否使用了自定义API密钥
if (parsedConfig.apiKeys) {
const customKeys = [];
if (parsedConfig.apiKeys.exchangeRateApi !== DEFAULT_CONFIG.apiKeys.exchangeRateApi) {
customKeys.push('ExchangeRate-API');
}
if (parsedConfig.apiKeys.fixer !== DEFAULT_CONFIG.apiKeys.fixer) {
customKeys.push('Fixer.io');
}
if (parsedConfig.apiKeys.currencyapi !== DEFAULT_CONFIG.apiKeys.currencyapi) {
customKeys.push('CurrencyAPI');
}
if (customKeys.length > 0) {
console.log(`[CC] 🔑 使用自定义API密钥: ${customKeys.join(', ')}`);
} else {
console.log('[CC] 使用默认API密钥');
}
}
return mergedConfig;
}
} catch (error) {
console.error('[CurrencyConverter] Failed to load config:', error);
}
// 返回默认配置的副本
console.log('[CC] 使用默认配置');
return { ...DEFAULT_CONFIG };
}
/**
* 保存配置到GM_storage
* @param {Object} newConfig - 新的配置对象(部分或完整)
*/
save(newConfig) {
try {
// 合并现有配置和新配置
this.config = { ...this.config, ...newConfig };
GM_setValue('cc_config', JSON.stringify(this.config));
// 显示保存的密钥信息
if (newConfig.apiKeys) {
const keys = [];
if (newConfig.apiKeys.exchangeRateApi) {
keys.push(`ExchangeRate-API: ${newConfig.apiKeys.exchangeRateApi.substring(0, 8)}****`);
}
if (newConfig.apiKeys.fixer) {
keys.push(`Fixer: ${newConfig.apiKeys.fixer.substring(0, 8)}****`);
}
if (newConfig.apiKeys.currencyapi) {
keys.push(`CurrencyAPI: ${newConfig.apiKeys.currencyapi.substring(0, 8)}****`);
}
console.log('[CC] ✅ API密钥已保存:', keys.join(', '));
} else {
console.log('[CC] 配置已保存');
}
} catch (error) {
console.error('[CurrencyConverter] Failed to save config:', error);
}
}
/**
* 获取单个配置项
* @param {string} key - 配置项的键
* @returns {*} 配置项的值
*/
get(key) {
return this.config[key];
}
/**
* 设置单个配置项
* @param {string} key - 配置项的键
* @param {*} value - 配置项的值
*/
set(key, value) {
this.config[key] = value;
this.save(this.config);
}
/**
* 重置为默认配置
*/
reset() {
try {
this.config = { ...DEFAULT_CONFIG };
GM_setValue('cc_config', JSON.stringify(this.config));
console.log('[CurrencyConverter] Config reset to defaults');
} catch (error) {
console.error('[CurrencyConverter] Failed to reset config:', error);
}
}
/**
* 获取所有配置
* @returns {Object} 完整的配置对象
*/
getAll() {
return { ...this.config };
}
}
/* ==================== 国际化翻译 ==================== */
/**
* 多语言翻译对象
* 支持中文(zh-CN)、英文(en)、日文(ja)、韩文(ko)
*/
const I18N_TRANSLATIONS = {
'zh-CN': {
tooltip: { update: '更新', history: '历史', errorUnavailable: '汇率数据暂时不可用', errorQuota: '可能是API配额用完了', errorHint: '点击油猴菜单 → 设置面板', close: '关闭' },
settings: { title: '货币转换器设置', smartDisplay: '智能显示', autoDetect: '根据IP自动检测所在国家', autoDetectDesc: '启用后,优先显示你所在国家的货币(首次加载时检测)', excludeSource: '排除原货币', excludeSourceDesc: '转换结果中不显示原价格的货币(例如:美元价格不再显示美元转换)', maxDisplay: '最多显示货币数量', inlineMode: '一键批量显示模式', inlineModeDesc: '直接在价格旁显示转换结果,无需鼠标悬停(Alt+I 切换)', inlineCurrency: '内联显示货币', inlineCurrencyDesc: '选择在内联模式中显示的货币', targetCurrency: '目标货币', targetCurrencyDesc: '选择2-5个要转换的目标货币', apiKeys: 'API密钥(可选)', apiKeysDesc: '如果默认API配额用完,可以免费申请自己的API密钥:', getKey: '获取密钥', placeholder: '留空使用默认密钥', customRates: '自定义汇率(离线模式)', enableCustom: '启用自定义汇率', enableCustomDesc: '开启后将使用您手动设置的汇率,不再调用API(适用于离线或固定汇率场景)', customTip: '所有汇率以 USD(美元) 为基准货币', customExample: '例如:输入 CNY = 7.25 表示 1美元 = 7.25人民币', excludeSites: '排除网站', excludeSitesDesc: '不进行货币转换的网站', excludeSitesPlaceholder: '这些域名的网页不会进行价格识别和转换(每行一个域名)', excludeCurrent: '排除当前网站', hotkeys: '快捷键', hotkeysAvailable: '可用的快捷键:', language: '界面语言', languageDesc: '选择界面显示语言', cancel: '取消', save: '保存并刷新' },
menu: { settings: '⚙️ 设置面板', reset: '🔄 重置配置', view: '🔍 查看当前配置', calculator: '💱 货币计算器 (Alt+C)' },
calculator: { title: '货币计算器', rate: '汇率', updated: '更新', error: '无法获取汇率数据' },
messages: { saved: '✅ 配置已保存!\n\n页面即将刷新以应用新设置。', resetConfirm: '确定要重置所有配置吗?\n这将恢复到默认设置。', resetSuccess: '配置已重置!刷新页面后生效。', minCurrency: '❌ 请至少选择2个目标货币!', maxCurrency: '❌ 最多只能选择5个目标货币!', invalidRate: '❌ 无效的汇率值', invalidRateDesc: '请输入大于0的数字!', minCustomRate: '❌ 请至少设置一个货币的汇率,或关闭自定义汇率功能!', excludeAdded: '已将 "{domain}" 添加到排除列表\n刷新页面后生效', excludeExists: '"{domain}" 已在排除列表中', excludeAddedPanel: '已添加 "{domain}" 到排除列表\n保存后将生效', rateUnavailable: '汇率数据不可用,请检查网络' },
notification: { apiQuotaTitle: '🚨 汇率API配额已用完', apiQuotaBody: '所有内置API密钥的免费配额已耗尽,货币转换功能暂时不可用。', apiQuotaAction: '点击此处注册免费API密钥', apiQuotaHint: '在油猴菜单 → 设置面板中填入您的API密钥', dismiss: '我知道了' },
config: { apiKeyTitle: 'API密钥配置', displaySettings: '显示设置', targetCurrenciesLabel: '目标货币', maxDisplay: '最多显示', pieces: '个', enabled: '启用', disabled: '禁用', userCountryCurrency: '用户国家货币', notDetected: '未检测', customKey: '自定义', defaultKey: '默认', freeQuota: '免费额度', requestsPerMonth: '请求/月', exampleText: '例如:输入 CNY = 7.25 表示 1美元 = 7.25人民币', selectCurrencyHint: '选择要显示的货币(至少2个,最多5个)', getKeyLink: '获取密钥 →' }
},
'en': {
tooltip: { update: 'Updated', history: 'History', errorUnavailable: 'Exchange rate data temporarily unavailable', errorQuota: 'API quota may be exhausted', errorHint: 'Click Tampermonkey Menu → Settings', close: 'Close' },
settings: { title: 'Currency Converter Settings', smartDisplay: 'Smart Display', autoDetect: 'Auto-detect country by IP', autoDetectDesc: 'When enabled, prioritize displaying your country\'s currency', excludeSource: 'Exclude source currency', excludeSourceDesc: 'Don\'t show the original currency in conversion results', maxDisplay: 'Max currencies to display', inlineMode: 'Batch Inline Display Mode', inlineModeDesc: 'Show conversion results directly next to prices (Alt+I to toggle)', inlineCurrency: 'Inline display currency', inlineCurrencyDesc: 'Select the currency to display in inline mode', targetCurrency: 'Target Currencies', targetCurrencyDesc: 'Select 2-5 target currencies for conversion', apiKeys: 'API Keys (Optional)', apiKeysDesc: 'If default API quota is exhausted, you can apply for free API keys:', getKey: 'Get Key', placeholder: 'Leave blank to use default key', customRates: 'Custom Exchange Rates (Offline Mode)', enableCustom: 'Enable custom rates', enableCustomDesc: 'When enabled, use your manually set rates instead of API calls', customTip: 'All rates are based on USD (US Dollar)', customExample: 'Example: CNY = 7.25 means 1 USD = 7.25 CNY', excludeSites: 'Exclude Websites', excludeSitesDesc: 'Websites where currency conversion will be disabled', excludeSitesPlaceholder: 'These domains will not have price detection and conversion (one domain per line)', excludeCurrent: 'Exclude Current Site', hotkeys: 'Keyboard Shortcuts', hotkeysAvailable: 'Available shortcuts:', language: 'Interface Language', languageDesc: 'Select interface display language', cancel: 'Cancel', save: 'Save & Refresh' },
menu: { settings: '⚙️ Settings', reset: '🔄 Reset Config', view: '🔍 View Current Config', calculator: '💱 Currency Calculator (Alt+C)' },
calculator: { title: 'Currency Calculator', rate: 'Rate', updated: 'Updated', error: 'Unable to fetch exchange rates' },
messages: { saved: '✅ Settings saved!\n\nPage will refresh to apply changes.', resetConfirm: 'Reset all settings to defaults?', resetSuccess: 'Settings reset! Refresh the page to take effect.', minCurrency: '❌ Please select at least 2 target currencies!', maxCurrency: '❌ Maximum 5 target currencies allowed!', invalidRate: '❌ Invalid exchange rate', invalidRateDesc: 'Please enter a number greater than 0!', minCustomRate: '❌ Please set at least one currency rate, or disable custom rates!', excludeAdded: 'Added "{domain}" to exclusion list\nRefresh the page to take effect', excludeExists: '"{domain}" is already in the exclusion list', excludeAddedPanel: 'Added "{domain}" to exclusion list\nWill take effect after saving', rateUnavailable: 'Exchange rate data unavailable, please check network' },
notification: { apiQuotaTitle: '🚨 API Quota Exhausted', apiQuotaBody: 'All built-in API keys have run out of free quota. Currency conversion is temporarily unavailable.', apiQuotaAction: 'Click here to get your free API key', apiQuotaHint: 'Enter your API key in Tampermonkey Menu → Settings Panel', dismiss: 'Got it' },
config: { apiKeyTitle: 'API Key Configuration', displaySettings: 'Display Settings', targetCurrenciesLabel: 'Target Currencies', maxDisplay: 'Max Display', pieces: '', enabled: 'Enabled', disabled: 'Disabled', userCountryCurrency: 'User Country Currency', notDetected: 'Not Detected', customKey: 'Custom', defaultKey: 'Default', freeQuota: 'Free Quota', requestsPerMonth: 'requests/month', exampleText: 'Example: CNY = 7.25 means 1 USD = 7.25 CNY', selectCurrencyHint: 'Select currencies to display (minimum 2, maximum 5)', getKeyLink: 'Get Key →' }
},
'ja': {
tooltip: { update: '更新', history: '履歴', errorUnavailable: '為替レートデータが一時的に利用できません', errorQuota: 'APIクォータが使い果たされた可能性があります', errorHint: 'Tampermonkeyメニュー → 設定', close: '閉じる' },
settings: { title: '通貨換算設定', smartDisplay: 'スマート表示', autoDetect: 'IPで国を自動検出', autoDetectDesc: '有効にすると、あなたの国の通貨を優先表示します', excludeSource: '元の通貨を除外', excludeSourceDesc: '換算結果に元の通貨を表示しない', maxDisplay: '最大表示通貨数', inlineMode: '一括インライン表示モード', inlineModeDesc: '価格の横に直接換算結果を表示(Alt+I で切替)', inlineCurrency: 'インライン表示通貨', inlineCurrencyDesc: 'インラインモードで表示する通貨を選択', targetCurrency: '対象通貨', targetCurrencyDesc: '換算する通貨を2~5個選択', apiKeys: 'APIキー(オプション)', apiKeysDesc: 'デフォルトのAPIクォータが使い果たされた場合、無料でAPIキーを申請できます:', getKey: 'キー取得', placeholder: '空白でデフォルトキーを使用', customRates: 'カスタム為替レート(オフラインモード)', enableCustom: 'カスタムレートを有効化', enableCustomDesc: '有効にすると、APIの代わりに手動設定したレートを使用します', customTip: 'すべてのレートはUSD(米ドル)を基準にしています', customExample: '例:CNY = 7.25 は 1米ドル = 7.25人民元を意味します', excludeSites: '除外するウェブサイト', excludeSitesDesc: '通貨換算が無効になるウェブサイト', excludeSitesPlaceholder: 'これらのドメインでは価格検出と換算が行われません(1行に1ドメイン)', excludeCurrent: '現在のサイトを除外', hotkeys: 'キーボードショートカット', hotkeysAvailable: '利用可能なショートカット:', language: 'インターフェース言語', languageDesc: 'インターフェース表示言語を選択', cancel: 'キャンセル', save: '保存して更新' },
menu: { settings: '⚙️ 設定', reset: '🔄 リセット', view: '🔍 現在の設定を表示', calculator: '💱 通貨計算機 (Alt+C)' },
calculator: { title: '通貨計算機', rate: 'レート', updated: '更新', error: '為替レートを取得できません' },
messages: { saved: '✅ 設定を保存しました!\n\nページを更新して変更を適用します。', resetConfirm: 'すべての設定をデフォルトにリセットしますか?', resetSuccess: '設定をリセットしました!ページを更新して反映してください。', minCurrency: '❌ 少なくとも2つの通貨を選択してください!', maxCurrency: '❌ 最大5つまでの通貨を選択できます!', invalidRate: '❌ 無効な為替レート', invalidRateDesc: '0より大きい数値を入力してください!', minCustomRate: '❌ 少なくとも1つの通貨レートを設定するか、カスタムレートを無効にしてください!', excludeAdded: '"{domain}" を除外リストに追加しました\nページを更新して反映してください', excludeExists: '"{domain}" は既に除外リストにあります', excludeAddedPanel: '"{domain}" を除外リストに追加しました\n保存後に反映されます', rateUnavailable: '為替レートデータが利用できません、ネットワークを確認してください' },
notification: { apiQuotaTitle: '🚨 APIクォータが枯渇しました', apiQuotaBody: 'すべての組み込みAPIキーの無料枠が使い果たされました。通貨変換機能は一時的に利用できません。', apiQuotaAction: 'ここをクリックして無料APIキーを取得', apiQuotaHint: 'Tampermonkeyメニュー → 設定パネルでAPIキーを入力', dismiss: '了解' },
config: { apiKeyTitle: 'APIキー設定', displaySettings: '表示設定', targetCurrenciesLabel: '対象通貨', maxDisplay: '最大表示', pieces: '個', enabled: '有効', disabled: '無効', userCountryCurrency: 'ユーザー国通貨', notDetected: '未検出', customKey: 'カスタム', defaultKey: 'デフォルト', freeQuota: '無料枠', requestsPerMonth: 'リクエスト/月', exampleText: '例:CNY = 7.25 は 1米ドル = 7.25人民元を意味します', selectCurrencyHint: '表示する通貨を選択(最低2個、最大5個)', getKeyLink: 'キー取得 →' }
},
'ko': {
tooltip: { update: '업데이트', history: '기록', errorUnavailable: '환율 데이터를 일시적으로 사용할 수 없습니다', errorQuota: 'API 할당량이 소진되었을 수 있습니다', errorHint: 'Tampermonkey 메뉴 → 설정', close: '닫기' },
settings: { title: '통화 변환기 설정', smartDisplay: '스마트 표시', autoDetect: 'IP로 국가 자동 감지', autoDetectDesc: '활성화하면 귀하의 국가 통화를 우선 표시합니다', excludeSource: '원본 통화 제외', excludeSourceDesc: '변환 결과에 원본 통화를 표시하지 않음', maxDisplay: '최대 표시 통화 수', inlineMode: '일괄 인라인 표시 모드', inlineModeDesc: '가격 옆에 직접 변환 결과 표시 (Alt+I로 전환)', inlineCurrency: '인라인 표시 통화', inlineCurrencyDesc: '인라인 모드에서 표시할 통화 선택', targetCurrency: '대상 통화', targetCurrencyDesc: '변환할 통화 2~5개 선택', apiKeys: 'API 키 (선택사항)', apiKeysDesc: '기본 API 할당량이 소진된 경우 무료로 API 키를 신청할 수 있습니다:', getKey: '키 받기', placeholder: '비워두면 기본 키 사용', customRates: '사용자 정의 환율 (오프라인 모드)', enableCustom: '사용자 정의 환율 활성화', enableCustomDesc: '활성화하면 API 대신 수동 설정한 환율을 사용합니다', customTip: '모든 환율은 USD (미국 달러)를 기준으로 합니다', customExample: '예: CNY = 7.25는 1달러 = 7.25위안을 의미합니다', excludeSites: '제외할 웹사이트', excludeSitesDesc: '통화 변환이 비활성화될 웹사이트', excludeSitesPlaceholder: '이러한 도메인에서는 가격 감지 및 변환이 수행되지 않습니다 (한 줄에 하나의 도메인)', excludeCurrent: '현재 사이트 제외', hotkeys: '키보드 단축키', hotkeysAvailable: '사용 가능한 단축키:', language: '인터페이스 언어', languageDesc: '인터페이스 표시 언어 선택', cancel: '취소', save: '저장 및 새로고침' },
menu: { settings: '⚙️ 설정', reset: '🔄 재설정', view: '🔍 현재 설정 보기', calculator: '💱 통화 계산기 (Alt+C)' },
calculator: { title: '통화 계산기', rate: '환율', updated: '업데이트됨', error: '환율 데이터를 가져올 수 없습니다' },
messages: { saved: '✅ 설정이 저장되었습니다!\n\n변경사항을 적용하기 위해 페이지를 새로고침합니다.', resetConfirm: '모든 설정을 기본값으로 재설정하시겠습니까?', resetSuccess: '설정이 재설정되었습니다! 페이지를 새로고침하여 적용하세요.', minCurrency: '❌ 최소 2개의 통화를 선택하세요!', maxCurrency: '❌ 최대 5개의 통화까지 선택할 수 있습니다!', invalidRate: '❌ 잘못된 환율', invalidRateDesc: '0보다 큰 숫자를 입력하세요!', minCustomRate: '❌ 최소 하나의 통화 환율을 설정하거나 사용자 정의 환율을 비활성화하세요!', excludeAdded: '"{domain}"을(를) 제외 목록에 추가했습니다\n페이지를 새로고침하여 적용하세요', excludeExists: '"{domain}"은(는) 이미 제외 목록에 있습니다', excludeAddedPanel: '"{domain}"을(를) 제외 목록에 추가했습니다\n저장 후 적용됩니다', rateUnavailable: '환율 데이터를 사용할 수 없습니다. 네트워크를 확인하세요' },
notification: { apiQuotaTitle: '🚨 API 할당량 소진', apiQuotaBody: '모든 내장 API 키의 무료 할당량이 소진되었습니다. 통화 변환 기능을 일시적으로 사용할 수 없습니다.', apiQuotaAction: '여기를 클릭하여 무료 API 키 받기', apiQuotaHint: 'Tampermonkey 메뉴 → 설정 패널에서 API 키 입력', dismiss: '알겠습니다' },
config: { apiKeyTitle: 'API 키 설정', displaySettings: '표시 설정', targetCurrenciesLabel: '대상 통화', maxDisplay: '최대 표시', pieces: '개', enabled: '활성화', disabled: '비활성화', userCountryCurrency: '사용자 국가 통화', notDetected: '미감지', customKey: '커스텀', defaultKey: '기본', freeQuota: '무료 할당량', requestsPerMonth: '요청/월', exampleText: '예: CNY = 7.25는 1달러 = 7.25위안을 의미합니다', selectCurrencyHint: '표시할 통화 선택 (최소 2개, 최대 5개)', getKeyLink: '키 받기 →' }
}
};
/**
* 国际化管理器类
*/
class I18nManager {
constructor(configManager) {
this.config = configManager;
this.currentLang = this.detectLanguage();
this.translations = I18N_TRANSLATIONS[this.currentLang] || I18N_TRANSLATIONS['zh-CN'];
}
detectLanguage() {
const savedLang = this.config.get('language');
if (savedLang && savedLang !== 'auto') return savedLang;
const browserLang = navigator.language || navigator.userLanguage;
if (browserLang.startsWith('zh')) return 'zh-CN';
if (browserLang.startsWith('ja')) return 'ja';
if (browserLang.startsWith('ko')) return 'ko';
return 'en';
}
t(key, params = {}) {
const keys = key.split('.');
let value = this.translations;
for (const k of keys) {
value = value?.[k];
if (!value) return key;
}
if (typeof value === 'string' && Object.keys(params).length > 0) {
return value.replace(/\{(\w+)\}/g, (match, param) => params[param] || match);
}
return value;
}
setLanguage(lang) {
if (I18N_TRANSLATIONS[lang]) {
this.currentLang = lang;
this.translations = I18N_TRANSLATIONS[lang];
this.config.set('language', lang);
}
}
getCurrentLanguage() {
return this.currentLang;
}
}
/* ==================== 通知管理器 ==================== */
/**
* 通知管理器类
* 负责显示右上角 Toast 通知,支持每日一次提醒
*/
class NotificationManager {
constructor(i18nManager) {
this.i18n = i18nManager;
this.STORAGE_KEY = 'cc_last_quota_notification';
this.ONE_DAY_MS = 24 * 60 * 60 * 1000; // 24小时
}
/**
* 检查今天是否已经显示过通知
* @returns {boolean}
*/
hasShownToday() {
try {
const lastShown = GM_getValue(this.STORAGE_KEY, 0);
const now = Date.now();
return (now - lastShown) < this.ONE_DAY_MS;
} catch (e) {
return false;
}
}
/**
* 记录今天已显示通知
*/
markAsShown() {
try {
GM_setValue(this.STORAGE_KEY, Date.now());
} catch (e) {
console.error('[CC] Failed to save notification state:', e);
}
}
/**
* 显示 API 配额耗尽通知(每天最多一次)
*/
showApiQuotaExhausted() {
// 检查今天是否已显示
if (this.hasShownToday()) {
console.log('[CC] API quota notification already shown today, skipping');
return;
}
// 标记已显示
this.markAsShown();
// 创建并显示 Toast
this.createToast();
}
/**
* 创建 Toast 通知元素
*/
createToast() {
// 如果已存在则移除
const existing = document.getElementById('cc-quota-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'cc-quota-toast';
toast.innerHTML = `
${this.i18n.t('notification.apiQuotaBody')}
${this.i18n.t('notification.apiQuotaAction')} →
💡 ${this.i18n.t('notification.apiQuotaHint')}
`;
document.body.appendChild(toast);
// 绑定关闭事件
const closeBtn = toast.querySelector('.cc-toast-close');
const dismissBtn = toast.querySelector('.cc-toast-dismiss');
const hideToast = () => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
};
closeBtn.addEventListener('click', hideToast);
dismissBtn.addEventListener('click', hideToast);
// 30秒后自动隐藏
setTimeout(() => {
if (document.getElementById('cc-quota-toast')) {
hideToast();
}
}, 30000);
console.log('[CC] 🚨 API quota exhausted notification shown');
}
}
/* ==================== 工具函数库 ==================== */
/**
* 通用工具函数库
* 提供防抖、节流、休眠等辅助功能
*/
const Utils = {
/**
* 防抖函数 - 延迟执行,多次调用只执行最后一次
* @param {Function} func - 要防抖的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 防抖后的函数
*
* @example
* const debouncedFn = Utils.debounce(() => console.log('Hello'), 300);
* debouncedFn(); // 只有在300ms内没有再次调用时才会执行
*/
debounce(func, delay) {
let timer = null;
return function(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
},
/**
* 节流函数 - 限制函数执行频率
* @param {Function} func - 要节流的函数
* @param {number} limit - 时间间隔(毫秒)
* @returns {Function} 节流后的函数
*
* @example
* const throttledFn = Utils.throttle(() => console.log('Hello'), 300);
* throttledFn(); // 在300ms内多次调用只执行一次
*/
throttle(func, limit) {
let inThrottle = false;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
},
/**
* 异步休眠函数
* @param {number} ms - 休眠时间(毫秒)
* @returns {Promise} Promise对象
*
* @example
* await Utils.sleep(1000); // 休眠1秒
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
/**
* HTML转义函数 - 防止XSS攻击
* @param {string} text - 要转义的文本
* @returns {string} 转义后的文本
*
* @example
* Utils.escapeHTML('');
* // 返回: <script>alert("XSS")</script>
*/
escapeHTML(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return String(text).replace(/[&<>"']/g, m => map[m]);
},
/**
* 数字格式化函数
* @param {number} num - 要格式化的数字
* @param {number} decimals - 小数位数(默认2位)
* @returns {string} 格式化后的数字字符串
*
* @example
* Utils.formatNumber(1234567.89); // 返回: "1,234,567.89"
* Utils.formatNumber(1234.5, 0); // 返回: "1,235"
*/
formatNumber(num, decimals = 2) {
if (isNaN(num)) return '0';
const fixed = Number(num).toFixed(decimals);
const parts = fixed.split('.');
// 添加千分位分隔符
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
};
/* ==================== 地理位置检测模块 ==================== */
/**
* 地理位置检测器类
* 根据IP地址检测用户所在国家,并映射到对应货币
*/
class GeoLocationDetector {
constructor(configManager) {
this.config = configManager;
this.countryToCurrency = {
'US': 'USD', 'CN': 'CNY', 'GB': 'GBP', 'JP': 'JPY', 'EU': 'EUR',
'DE': 'EUR', 'FR': 'EUR', 'IT': 'EUR', 'ES': 'EUR', 'NL': 'EUR',
'HK': 'HKD', 'TW': 'TWD', 'KR': 'KRW', 'AU': 'AUD', 'CA': 'CAD',
'SG': 'SGD', 'CH': 'CHF', 'RU': 'RUB', 'IN': 'INR', 'BR': 'BRL',
'MX': 'MXN', 'ID': 'IDR', 'TR': 'TRY', 'SA': 'SAR', 'ZA': 'ZAR'
};
}
/**
* 检测用户所在国家并返回对应货币
* @returns {Promise} 国家对应的货币代码
*/
async detectUserCurrency() {
// 先检查是否已缓存
const cached = this.config.get('userCountryCurrency');
if (cached) {
console.log(`[CC] 使用缓存的用户国家货币: ${cached}`);
return cached;
}
// 如果用户禁用了自动检测
if (!this.config.get('autoDetectLocation')) {
console.log('[CC] 自动检测已禁用');
return null;
}
try {
console.log('[CC] 正在检测用户地理位置...');
// 使用免费IP地理位置API(ipapi.co)
const countryCode = await this.fetchCountryCode();
if (!countryCode) {
console.log('[CC] 无法获取国家代码');
return null;
}
const currency = this.countryToCurrency[countryCode] || null;
if (currency) {
console.log(`[CC] 🌍 检测到用户位于: ${countryCode}, 货币: ${currency}`);
// 保存到配置
this.config.save({ userCountryCurrency: currency });
return currency;
} else {
console.log(`[CC] 国家代码 ${countryCode} 未映射到货币`);
return null;
}
} catch (error) {
console.error('[CC] 地理位置检测失败:', error);
return null;
}
}
/**
* 调用IP API获取国家代码
* @returns {Promise}
*/
async fetchCountryCode() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://ipapi.co/country/',
timeout: 5000,
onload: (response) => {
if (response.status === 200) {
const countryCode = response.responseText.trim().toUpperCase();
resolve(countryCode);
} else {
console.warn('[CC] IP API返回错误:', response.status);
resolve(null);
}
},
onerror: (error) => {
console.error('[CC] IP API请求失败:', error);
resolve(null);
},
ontimeout: () => {
console.warn('[CC] IP API请求超时');
resolve(null);
}
});
});
}
/**
* 手动设置用户国家货币
* @param {string} currency - 货币代码
*/
setUserCurrency(currency) {
this.config.save({ userCountryCurrency: currency });
console.log(`[CC] 用户国家货币已设置为: ${currency}`);
}
/**
* 清除缓存的国家货币
*/
clearCache() {
this.config.save({ userCountryCurrency: null });
console.log('[CC] 已清除用户国家货币缓存');
}
}
/* ==================== 汇率数据管理器 ==================== */
/**
* 汇率数据管理器类
* 负责调用汇率API、缓存管理和货币转换计算
*/
class ExchangeRateManager {
constructor(configManager, notificationManager = null) {
this.config = configManager;
this.notificationManager = notificationManager;
this.apis = [
{
name: 'exchangerate-api',
url: 'https://v6.exchangerate-api.com/v6/{key}/latest/{base}',
priority: 1,
requiresKey: true,
parseResponse: (data) => ({
base: data.base_code,
rates: data.conversion_rates,
timestamp: Date.now(),
source: 'exchangerate-api'
})
},
{
name: 'fixer',
url: 'https://api.fixer.io/latest?access_key={key}&base={base}',
priority: 2,
requiresKey: true,
parseResponse: (data) => ({
base: data.base,
rates: data.rates,
timestamp: Date.now(),
source: 'fixer'
})
},
{
name: 'currencyapi',
url: 'https://api.currencyapi.com/v3/latest?apikey={key}&base_currency={base}',
priority: 3,
requiresKey: true,
parseResponse: (data) => {
const rates = {};
if (data.data) {
for (const [currency, info] of Object.entries(data.data)) {
rates[currency] = info.value;
}
}
return {
base: data.meta?.last_updated_at ? 'USD' : 'USD',
rates: rates,
timestamp: Date.now(),
source: 'currencyapi'
};
}
}
];
this.currentRates = null;
this.updatePromise = null;
}
/**
* 获取汇率数据(带缓存)
* @param {string} baseCurrency - 基准货币代码(默认USD)
* @returns {Promise