// ==UserScript== // @name steam价格转换 // @namespace https://github.com/marioplus/steam-price-converter // @version 2.7.1 // @author marioplus // @description steam商店中的价格转换为人民币 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=store.steampowered.com // @homepage https://github.com/marioplus/steam-price-converter // @homepageURL https://github.com/marioplus/steam-price-converter // @match https://store.steampowered.com/* // @match https://steamcommunity.com/* // @match https://checkout.steampowered.com/checkout/* // @require https://cdn.jsdelivr.net/npm/vue@3.5.28/dist/vue.global.prod.js // @connect api.augmentedsteam.com // @connect store.steampowered.com // @connect cdn.jsdelivr.net // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_cookie // @grant GM_deleteValue // @grant GM_getValue // @grant GM_info // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== (function (vue) { 'use strict'; const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));}; importCSS(' .steam-dialog-overlay[data-v-dc7993c8]{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#000000b3;display:flex;align-items:center;justify-content:center;z-index:10000;font-family:"Motiva Sans",Sans-serif}.steam-dialog[data-v-dc7993c8]{width:800px;max-width:90vw;height:600px;background-color:#1b2838;color:#dcdedf;display:flex;flex-direction:column;box-shadow:0 0 20px #00000080;border-radius:8px;border:1px solid #444;overflow:hidden;position:relative}.close-btn[data-v-dc7993c8]{position:absolute;top:12px;right:12px;width:24px;height:24px;cursor:pointer;color:#8f98a0;z-index:10;transition:color .2s}.close-btn[data-v-dc7993c8]:hover{color:#fff}.steam-dialog-body[data-v-dc7993c8]{flex:1;display:flex;overflow:hidden}.steam-sidebar[data-v-dc7993c8]{width:200px;background-color:#2a2d34;display:flex;flex-direction:column}.steam-sidebar .sidebar-header[data-v-dc7993c8]{padding:24px 16px 12px}.steam-sidebar .sidebar-header .title[data-v-dc7993c8]{color:#1a9fff;font-size:18px;font-weight:700;letter-spacing:1px;text-transform:uppercase}.sidebar-item[data-v-dc7993c8]{height:40px;display:flex;align-items:center;padding:0 16px;cursor:pointer;color:#8f98a0;font-size:14px;gap:12px}.sidebar-item .icon[data-v-dc7993c8]{width:18px;height:18px;display:flex;align-items:center;justify-content:center}.sidebar-item .icon[data-v-dc7993c8] svg{width:100%;height:100%}.sidebar-item[data-v-dc7993c8]:hover{background-color:#ffffff0d;color:#fff}.sidebar-item.active[data-v-dc7993c8]{background-color:#ffffff1a;color:#fff;border-left:3px solid #1a9fff}.sidebar-item.disabled[data-v-dc7993c8]{cursor:default;opacity:.5}.sidebar-item.disabled[data-v-dc7993c8]:hover{background:transparent}.steam-content-area[data-v-dc7993c8]{flex:1;display:flex;flex-direction:column;padding:24px 32px;background-color:#171d25;position:relative}.steam-content-area .content-header[data-v-dc7993c8]{font-size:22px;font-weight:600;margin-bottom:24px;color:#fff}.steam-content-area .content-body[data-v-dc7993c8]{flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable}.steam-content-area .content-body[data-v-dc7993c8]::-webkit-scrollbar{width:8px}.steam-content-area .content-body[data-v-dc7993c8]::-webkit-scrollbar-track{background:transparent}.steam-content-area .content-body[data-v-dc7993c8]::-webkit-scrollbar-thumb{background:#3d4450;border-radius:4px}.steam-content-area .content-body[data-v-dc7993c8]::-webkit-scrollbar-thumb:hover{background:#4e5663}.steam-content-area .content-body[data-v-dc7993c8]{padding-right:8px}.steam-content-area .content-footer[data-v-dc7993c8]{height:64px;display:flex;align-items:center;justify-content:flex-end;gap:12px;border-top:1px solid #2a475e;margin-top:16px}.about-container[data-v-dc7993c8]{color:#dcdedf;line-height:1.6}.about-container .plugin-info[data-v-dc7993c8]{margin-bottom:32px}.about-container .plugin-info .plugin-name[data-v-dc7993c8]{font-size:20px;font-weight:700;color:#fff}.about-container .plugin-info .plugin-version[data-v-dc7993c8]{font-size:14px;color:#8f98a0}.about-container .about-section[data-v-dc7993c8]{margin-bottom:24px}.about-container .about-section .section-title[data-v-dc7993c8]{font-size:12px;font-weight:700;color:#1a9fff;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}.about-container .about-section .section-content[data-v-dc7993c8]{font-size:14px;color:#bdc3c7}.steam-link[data-v-dc7993c8]{color:#1a9fff;text-decoration:none;margin-top:8px;display:inline-block}.steam-link[data-v-dc7993c8]:hover{text-decoration:underline;color:#66c0f4}.steam-select-container[data-v-b7059657]{position:relative;width:100%;-webkit-user-select:none;user-select:none}.steam-select[data-v-b7059657]{min-width:40px;min-height:14px;background-color:#292e36;color:#eee;padding:7px 10px;font-size:14px;cursor:pointer;border-radius:2px;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s}.steam-select[data-v-b7059657]:hover,.steam-select.is-open[data-v-b7059657]{background-color:#464d58}.selected-text[data-v-b7059657]{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px}.steam-select-arrow[data-v-b7059657]{width:20px;height:20px;color:#1a9fff;transition:transform .2s}.steam-select-arrow.is-open[data-v-b7059657]{transform:rotate(180deg)}.steam-options-list{position:fixed;background-color:#373c44;border:1px solid #101822;box-shadow:0 4px 12px #00000080;z-index:99999;max-height:240px;overflow-y:auto;overflow-x:hidden;border-radius:2px}.steam-options-list::-webkit-scrollbar{width:8px}.steam-options-list::-webkit-scrollbar-track{background:transparent}.steam-options-list::-webkit-scrollbar-thumb{background:#3d4450;border-radius:4px}.steam-options-list::-webkit-scrollbar-thumb:hover{background:#4e5663}.steam-option{padding:8px 12px;font-size:14px;color:#fff;cursor:pointer;transition:background-color .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.steam-option:hover{background-color:#1a9fff;color:#fff}.steam-option.is-selected{background-color:#1a9fff66;color:#fff}.steam-switch[data-v-3c1a2067]{width:40px;height:24px;background-color:#3d4450;border-radius:14px;position:relative;cursor:pointer;transition:background-color .2s}.steam-switch.is-on[data-v-3c1a2067]{background-color:#1a9fff}.steam-switch.is-on .steam-switch-thumb[data-v-3c1a2067]{left:16px}.steam-switch-thumb[data-v-3c1a2067]{width:24px;height:24px;background-color:#fff;border-radius:50%;position:absolute;transition:left .2s;box-shadow:0 1px 3px #0000004d}.steam-settings-container[data-v-ffafb838]{display:flex;flex-direction:column;gap:16px}.setting-row[data-v-ffafb838]{display:flex;justify-content:space-between;align-items:center;padding-bottom:16px;border-bottom:1px solid rgba(255,255,255,.05)}.setting-row .info[data-v-ffafb838]{flex:1;min-width:0}.setting-row .info .label[data-v-ffafb838]{font-size:14px;color:#fff;margin-bottom:4px}.setting-row .info .desc[data-v-ffafb838]{font-size:12px;color:#8f98a0}.setting-row .control[data-v-ffafb838]{min-width:40px;display:flex;justify-content:flex-end}.steam-input[data-v-ffafb838]{width:30px;background-color:#292e36;border:0px solid #000;color:#fff;padding:8px 12px;border-radius:2px;font-family:inherit;outline:none}.steam-input[data-v-ffafb838]:hover{background-color:#313841}.steam-input[data-v-ffafb838]:focus{background-color:#23262e;box-shadow:inset 0 1px 6px #000000b3}.steam-input[data-v-ffafb838]::-webkit-outer-spin-button,.steam-input[data-v-ffafb838]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.steam-input[type=number][data-v-ffafb838]{-moz-appearance:textfield;appearance:textfield}.steam-btn[data-v-ffafb838]{padding:0 24px;height:32px;font-size:14px;border:none;border-radius:2px;cursor:pointer;font-family:inherit;transition:filter .2s}.steam-btn[data-v-ffafb838]:hover{filter:brightness(1.2)}.steam-btn.blue[data-v-ffafb838]{background:linear-gradient(to right,#3a9bed,#245ecf);color:#fff}.steam-btn.gray[data-v-ffafb838]{background-color:#3d4450;color:#fff} '); const homeScss = "div.home_tabs_content .tab_item_discount{width:185px}"; importCSS(homeScss); const searchScss = ".search_result_row .col.search_name{width:240px}.search_result_row .col.search_released{width:105px}.search_result_row .col.search_discount_and_price .discount_block{width:165px}"; importCSS(searchScss); const marketScss = "#popularItemsTable .market_listing_their_price{width:160px!important}"; importCSS(marketScss); const styleScss = '@charset "UTF-8";#spc-menu{position:absolute;top:0;width:100px;height:100px;z-index:99999;padding:0;margin:0}.tab_item_discount{min-width:113px!important;width:unset}.discount_final_price{display:inline-block!important}.search_result_row .col.search_price{width:175px}.search_result_row .col.search_name{width:200px}.market_listing_their_price{width:160px}'; importCSS(styleScss); const countyObjs = JSON.parse('[{"code":"US","currencyCode":"USD","name":"美国","nameEn":"United States","symbol":"$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"CN","currencyCode":"CNY","name":"中国","nameEn":"China","symbol":"¥","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"JP","currencyCode":"JPY","name":"日本","nameEn":"Japan","symbol":"¥","decimalSep":null,"thousandsSep":",","fractionDigits":0,"symbolPos":"front","symbolGap":""},{"code":"DE","currencyCode":"EUR","name":"德国","nameEn":"Germany","symbol":"€","decimalSep":",","thousandsSep":" ","fractionDigits":2,"symbolPos":"end","symbolGap":""},{"code":"RU","currencyCode":"RUB","name":"俄罗斯","nameEn":"Russia","symbol":"руб","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"PL","currencyCode":"PLN","name":"波兰","nameEn":"Poland","symbol":"zł","decimalSep":",","thousandsSep":" ","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"BR","currencyCode":"BRL","name":"巴西","nameEn":"Brazil","symbol":"R$","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"VN","currencyCode":"VND","name":"越南","nameEn":"Vietnam","symbol":"₫","decimalSep":null,"thousandsSep":".","fractionDigits":0,"symbolPos":"end","symbolGap":""},{"code":"KR","currencyCode":"KRW","name":"韩国","nameEn":"South Korea","symbol":"₩","decimalSep":null,"thousandsSep":",","fractionDigits":0,"symbolPos":"front","symbolGap":" "},{"code":"ID","currencyCode":"IDR","name":"印度尼西亚","nameEn":"Indonesia","symbol":"Rp","decimalSep":null,"thousandsSep":" ","fractionDigits":0,"symbolPos":"front","symbolGap":" "},{"code":"TW","currencyCode":"TWD","name":"中国台湾","nameEn":"Taiwan","symbol":"NT$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"HK","currencyCode":"HKD","name":"中国香港","nameEn":"Hong Kong","symbol":"HK$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"UA","currencyCode":"UAH","name":"乌克兰","nameEn":"Ukraine","symbol":"₴","decimalSep":",","thousandsSep":" ","fractionDigits":2,"symbolPos":"end","symbolGap":""},{"code":"KZ","currencyCode":"KZT","name":"哈萨克斯坦","nameEn":"Kazakhstan","symbol":"₸","decimalSep":",","thousandsSep":" ","fractionDigits":2,"symbolPos":"end","symbolGap":""},{"code":"GB","currencyCode":"GBP","name":"英国","nameEn":"United Kingdom","symbol":"£","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"CH","currencyCode":"CHF","name":"瑞士","nameEn":"Switzerland","symbol":"CHF","decimalSep":".","thousandsSep":" ","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"NO","currencyCode":"NOK","name":"挪威","nameEn":"Norway","symbol":"kr","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"TH","currencyCode":"THB","name":"泰国","nameEn":"Thailand","symbol":"฿","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"PH","currencyCode":"PHP","name":"菲律宾","nameEn":"Philippines","symbol":"₱","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"SG","currencyCode":"SGD","name":"新加坡","nameEn":"Singapore","symbol":"S$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"MY","currencyCode":"MYR","name":"马来西亚","nameEn":"Malaysia","symbol":"RM","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"MX","currencyCode":"MXN","name":"墨西哥","nameEn":"Mexico","symbol":"Mex$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"CA","currencyCode":"CAD","name":"加拿大","nameEn":"Canada","symbol":"C$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"AU","currencyCode":"AUD","name":"澳大利亚","nameEn":"Australia","symbol":"A$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"NZ","currencyCode":"NZD","name":"新西兰","nameEn":"New Zealand","symbol":"NZ$","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"IN","currencyCode":"INR","name":"印度","nameEn":"India","symbol":"₹","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"CL","currencyCode":"CLP","name":"智利","nameEn":"Chile","symbol":"CLP$","decimalSep":null,"thousandsSep":".","fractionDigits":0,"symbolPos":"front","symbolGap":""},{"code":"PE","currencyCode":"PEN","name":"秘鲁","nameEn":"Peru","symbol":"S/.","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"CO","currencyCode":"COP","name":"哥伦比亚","nameEn":"Colombia","symbol":"COL$","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"ZA","currencyCode":"ZAR","name":"南非","nameEn":"South Africa","symbol":"R","decimalSep":".","thousandsSep":" ","fractionDigits":2,"symbolPos":"front","symbolGap":" "},{"code":"SA","currencyCode":"SAR","name":"沙特阿拉伯","nameEn":"Saudi Arabia","symbol":"SR","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"AE","currencyCode":"AED","name":"阿联酋","nameEn":"United Arab Emirates","symbol":"AED","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"IL","currencyCode":"ILS","name":"以色列","nameEn":"Israel","symbol":"₪","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"KW","currencyCode":"KWD","name":"科威特","nameEn":"Kuwait","symbol":"KD","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"QA","currencyCode":"QAR","name":"卡塔尔","nameEn":"Qatar","symbol":"QR","decimalSep":".","thousandsSep":",","fractionDigits":2,"symbolPos":"end","symbolGap":" "},{"code":"CR","currencyCode":"CRC","name":"哥斯达黎加","nameEn":"Costa Rica","symbol":"₡","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"front","symbolGap":""},{"code":"UY","currencyCode":"UYU","name":"乌拉圭","nameEn":"Uruguay","symbol":"$U","decimalSep":",","thousandsSep":".","fractionDigits":2,"symbolPos":"front","symbolGap":""}]'); class Jsons { static toJson(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (obj instanceof Map) { const json2 = {}; for (const [key, value] of obj) { json2[key] = this.toJson(value); } return json2; } if (Array.isArray(obj)) { return obj.map((v) => this.toJson(v)); } const json = {}; const keys = Object.keys(obj); for (const key of keys) { json[key] = this.toJson(obj[key]); } return json; } static toString(obj) { return JSON.stringify(this.toJson(obj)); } static readJson(json, cls) { if (!cls) { if (typeof json !== "object" || json === null) { throw new Error("Invalid JSON input"); } return json; } const instance = new cls(); if (instance instanceof Map) { return this.handleMap(json, instance); } for (const key of Reflect.ownKeys(json)) { const value = json[key]; if (value === null || value === void 0) { instance[key] = value; continue; } const fieldValue = instance[key]; if (fieldValue !== null && typeof fieldValue === "object" && !(fieldValue instanceof Array)) { if (fieldValue instanceof Map) { instance[key] = this.handleMap(value, fieldValue); } else if (fieldValue instanceof Object) { instance[key] = this.readJson(value, fieldValue.constructor); } } else if (Array.isArray(fieldValue)) { instance[key] = value.map( (item) => typeof item === "object" ? this.readJson(item, fieldValue[0]?.constructor) : item ); } else { instance[key] = value; } } return instance; } static handleMap(value, mapInstance) { const map = new Map(); if (value && typeof value === "object") { for (const key of Object.keys(value)) { const mapValue = value[key]; if (mapValue === null || mapValue === void 0) { map.set(key, mapValue); continue; } const existingValue = mapInstance.get(key); if (this.isObject(mapValue)) { if (existingValue) { map.set(key, this.readJson(mapValue, existingValue.constructor)); } else { map.set(key, mapValue); } } else { map.set(key, mapValue); } } } return map; } static isObject(value) { return value !== null && typeof value === "object"; } static readString(jsonString, cls) { const json = JSON.parse(jsonString); return this.readJson(json, cls); } } const countyInfos = Jsons.readJson(countyObjs, Array).sort((a, b) => a.name.localeCompare(b.name)); const countyCode2Info = new Map(countyInfos.map((v) => [v.code, v])); class Setting { countyCode = "CN"; currencySymbol = "¥"; currencySymbolBeforeValue = true; rateCacheExpired = 1e3 * 60 * 60; useCustomRate = false; customRate = 1; oldVersion = "0.0.0"; currVersion = "0.0.0"; logLevel = "info"; disableOnMarket = false; } const STORAGE_KEY_PREFIX = "Storage:"; const STORAGE_KEY_RATE_CACHES = STORAGE_KEY_PREFIX + "RateCache"; const STORAGE_KEY_SETTING = STORAGE_KEY_PREFIX + "Setting"; const IM_MENU_SETTING = "设置"; const IM_MENU_ISSUES = "反馈"; class Attrs { static STATUS_KEY = "data-spc-status"; static STATUS_CONVERTED = "converted"; } var _GM_addValueChangeListener = (() => typeof GM_addValueChangeListener != "undefined" ? GM_addValueChangeListener : void 0)(); var _GM_cookie = (() => typeof GM_cookie != "undefined" ? GM_cookie : void 0)(); var _GM_deleteValue = (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)(); var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_info = (() => typeof GM_info != "undefined" ? GM_info : void 0)(); var _GM_openInTab = (() => typeof GM_openInTab != "undefined" ? GM_openInTab : void 0)(); var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)(); var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); function initializeLogTitle() { const isInIFrame = window.parent !== window || window.frames.length > 0; return isInIFrame ? `steam-price-convertor iframe(${( new Date()).getMilliseconds()})` : "steam-price-convertor"; } function createLogStyle(color) { return ` background: ${color}; color: white; padding: 1px 3px; border-radius: 2px; `; } function composeLogHint() { return `%c${initializeLogTitle()}`; } const LogDefinitions = { debug: { index: 0, color: "#009688", label: "debug", bindMethod: console.info }, info: { index: 1, color: "#2196f3", label: "info", bindMethod: console.info }, warn: { index: 2, color: "#ffc107", label: "warn", bindMethod: console.warn }, error: { index: 3, color: "#e91e63", label: "error", bindMethod: console.error }, off: { index: 4, color: "", label: "off", bindMethod: () => { } } }; const Logger = { debug: noopLog.bind(null), info: noopLog.bind(null), warn: noopLog.bind(null), error: noopLog.bind(null) }; function noopLog() { } let currLogLevel = LogDefinitions.info; function refreshBinding() { const hint = composeLogHint(); Object.entries(LogDefinitions).forEach(([label, def]) => { if (def.index >= currLogLevel.index) { const logStyle = createLogStyle(def.color); Logger[label.toLowerCase()] = def.bindMethod.bind(console, hint, logStyle); } else { Logger[label.toLowerCase()] = noopLog.bind(null); } }); } function setLogLevel(levelLabel) { const newLevel = LogDefinitions[levelLabel]; if (newLevel) { currLogLevel = newLevel; refreshBinding(); } else { console.error(`Invalid log level: ${levelLabel}`); } } refreshBinding(); class Strings { static format(format, ...args) { args = args || []; let message = format; for (let arg of args) { message = message.replace("%s", arg); } return message; } } class GmUtils { static getSimpleValue(key, defaultValue) { const value = _GM_getValue(key); return value || defaultValue; } static getObjValue(cls, key, defaultValue) { const value = _GM_getValue(key); return value ? Jsons.readString(value, cls) : defaultValue; } static setSimpleValue(key, value) { _GM_setValue(key, value); } static setObjValue(key, value) { _GM_setValue(key, Jsons.toString(value)); } static deleteValue(key) { _GM_deleteValue(key); } static registerMenuCommand(caption, onClick) { const menuValueKey = GmUtils.buildMenuValueKey(caption); _GM_registerMenuCommand(caption, () => { GmUtils.setSimpleValue(menuValueKey, ( new Date()).getTime()); Logger.debug("点击菜单:" + caption); if (onClick) { onClick(); } }); } static addMenuClickEventListener(caption, onClick) { const menuValueKey = GmUtils.buildMenuValueKey(caption); _GM_addValueChangeListener(menuValueKey, onClick); } static buildMenuValueKey(caption) { return `GM_registerMenuCommand@${caption}`; } static openInTab(url) { _GM_openInTab(url); } } class SettingManager { static instance = new SettingManager(); setting; constructor() { this.setting = this.loadSetting(); } loadSetting() { const setting = GmUtils.getObjValue(Setting, STORAGE_KEY_SETTING, new Setting()); setting.oldVersion = setting.currVersion; setting.currVersion = _GM_info.script.version; if (setting.oldVersion === setting.currVersion) { Logger.info("读取设置", setting); } else { Logger.debug(Strings.format(`版本更新重置设置:%s -> %s`, setting.oldVersion, setting.currVersion)); this.saveSetting(setting); } return setting; } saveSetting(setting) { Logger.info("保存设置", setting); this.setting = setting; GmUtils.setObjValue(STORAGE_KEY_SETTING, setting); } setCountyCode(countyCode) { const county = countyCode2Info.get(countyCode); if (!county) { throw Error(`国家代码不存在:${countyCode}`); } this.setting.countyCode = countyCode; this.saveSetting(this.setting); } setCurrencySymbol(currencySymbol) { this.setting.currencySymbol = currencySymbol; this.saveSetting(this.setting); } setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue) { this.setting.currencySymbolBeforeValue = isCurrencySymbolBeforeValue; this.saveSetting(this.setting); } reset() { this.saveSetting(new Setting()); } setRateCacheExpired(rateCacheExpired) { this.setting.rateCacheExpired = rateCacheExpired; this.saveSetting(this.setting); } setUseCustomRate(isUseCustomRate) { this.setting.useCustomRate = isUseCustomRate; this.saveSetting(this.setting); } setCustomRate(customRate) { this.setting.customRate = customRate; this.saveSetting(this.setting); } } const _hoisted_1$7 = { xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", viewBox: "0 0 24 24" }; function render$4(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$7, [..._cache[0] || (_cache[0] = [ vue.createElementVNode("path", { d: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }, null, -1) ])]); } const IconClose = { render: render$4 }; const _hoisted_1$6 = { xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", viewBox: "0 0 24 24" }; function render$3(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$6, [..._cache[0] || (_cache[0] = [ vue.createElementVNode("path", { d: "M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4" }, null, -1) ])]); } const IconPriceConvert = { render: render$3 }; const _hoisted_1$5 = { xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", viewBox: "0 0 24 24" }; function render$2(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$5, [..._cache[0] || (_cache[0] = [ vue.createElementVNode("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 15h-2v-6h2zm0-8h-2V7h2z" }, null, -1) ])]); } const IconAbout = { render: render$2 }; const _hoisted_1$4 = { xmlns: "http://www.w3.org/2000/svg", fill: "none", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", viewBox: "0 0 24 24" }; function render$1(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$4, [..._cache[0] || (_cache[0] = [ vue.createElementVNode("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3" }, null, -1) ])]); } const IconExternalLink = { render: render$1 }; const _hoisted_1$3 = { class: "steam-dialog" }; const _hoisted_2$2 = { class: "steam-dialog-body" }; const _hoisted_3$1 = { class: "steam-sidebar" }; const _hoisted_4$1 = { class: "icon" }; const _hoisted_5$1 = { class: "icon" }; const _hoisted_6$1 = { class: "steam-content-area" }; const _hoisted_7$1 = { class: "content-header" }; const _hoisted_8$1 = { class: "content-body" }; const _hoisted_9$1 = { key: 1, class: "about-container" }; const _hoisted_10$1 = { class: "plugin-info" }; const _hoisted_11$1 = { class: "plugin-version" }; const _hoisted_12$1 = { class: "about-section" }; const _hoisted_13$1 = { class: "section-content" }; const _hoisted_14$1 = { href: "https://github.com/marioplus/steam-price-converter", target: "_blank", class: "steam-link" }; const _hoisted_15$1 = { key: 0, class: "content-footer" }; const _sfc_main$3 = vue.defineComponent({ __name: "SteamDialog", props: { open: { type: Boolean }, title: {} }, emits: ["close"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const activeTab = vue.ref("settings"); const displayTitle = vue.computed(() => { return activeTab.value === "settings" ? props.title : "关于插件"; }); const close = () => { emit("close"); }; return (_ctx, _cache) => { return __props.open ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, class: "steam-dialog-overlay", onClick: vue.withModifiers(close, ["self"]) }, [ vue.createElementVNode("div", _hoisted_1$3, [ vue.createElementVNode("div", { class: "close-btn", onClick: close }, [ vue.createVNode(vue.unref(IconClose)) ]), vue.createElementVNode("div", _hoisted_2$2, [ vue.createElementVNode("div", _hoisted_3$1, [ _cache[4] || (_cache[4] = vue.createElementVNode("div", { class: "sidebar-header" }, [ vue.createElementVNode("span", { class: "title" }, "SPC 设置") ], -1)), vue.createElementVNode("div", { class: vue.normalizeClass(["sidebar-item", { active: activeTab.value === "settings" }]), onClick: _cache[0] || (_cache[0] = ($event) => activeTab.value = "settings") }, [ vue.createElementVNode("div", _hoisted_4$1, [ vue.createVNode(vue.unref(IconPriceConvert)) ]), _cache[2] || (_cache[2] = vue.createElementVNode("span", null, "价格转换", -1)) ], 2), vue.createElementVNode("div", { class: vue.normalizeClass(["sidebar-item", { active: activeTab.value === "about" }]), onClick: _cache[1] || (_cache[1] = ($event) => activeTab.value = "about") }, [ vue.createElementVNode("div", _hoisted_5$1, [ vue.createVNode(vue.unref(IconAbout)) ]), _cache[3] || (_cache[3] = vue.createElementVNode("span", null, "关于插件", -1)) ], 2) ]), vue.createElementVNode("div", _hoisted_6$1, [ vue.createElementVNode("div", _hoisted_7$1, vue.toDisplayString(displayTitle.value), 1), vue.createElementVNode("div", _hoisted_8$1, [ activeTab.value === "settings" ? vue.renderSlot(_ctx.$slots, "default", { key: 0 }, void 0, true) : activeTab.value === "about" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$1, [ vue.createElementVNode("div", _hoisted_10$1, [ _cache[5] || (_cache[5] = vue.createElementVNode("div", { class: "plugin-name" }, "Steam Price Converter (SPC)", -1)), vue.createElementVNode("div", _hoisted_11$1, "版本: " + vue.toDisplayString(vue.unref(_GM_info).script.version), 1) ]), _cache[9] || (_cache[9] = vue.createElementVNode("div", { class: "about-section" }, [ vue.createElementVNode("div", { class: "section-title" }, "功能简介"), vue.createElementVNode("div", { class: "section-content" }, " 基于第一性原理打造的 Steam 代购/多区域价格转换神器。 支持自动获取最新汇率、自定义汇率以及多种货币符号展示方案。 ") ], -1)), vue.createElementVNode("div", _hoisted_12$1, [ _cache[8] || (_cache[8] = vue.createElementVNode("div", { class: "section-title" }, "开源维护", -1)), vue.createElementVNode("div", _hoisted_13$1, [ _cache[7] || (_cache[7] = vue.createTextVNode(" 本项目已在 GitHub 开源,欢迎提交 Issue 或 Pull Request。 ", -1)), vue.createElementVNode("a", _hoisted_14$1, [ _cache[6] || (_cache[6] = vue.createTextVNode("获取最新源代码 ", -1)), vue.createVNode(vue.unref(IconExternalLink), { style: { "width": "14px", "height": "14px", "margin-left": "4px", "vertical-align": "middle", "display": "inline-flex" } }) ]) ]) ]), _cache[10] || (_cache[10] = vue.createElementVNode("div", { class: "about-section" }, [ vue.createElementVNode("div", { class: "section-title" }, "开发者"), vue.createElementVNode("div", { class: "section-content" }, " MarioPlus ") ], -1)) ])) : vue.createCommentVNode("", true) ]), activeTab.value === "settings" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_15$1, [ vue.renderSlot(_ctx.$slots, "action", {}, void 0, true) ])) : vue.createCommentVNode("", true) ]) ]) ]) ])) : vue.createCommentVNode("", true); }; } }); const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const SteamDialog = _export_sfc(_sfc_main$3, [["__scopeId", "data-v-dc7993c8"]]); const _hoisted_1$2 = { xmlns: "http://www.w3.org/2000/svg", fill: "none", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", viewBox: "0 0 24 24" }; function render(_ctx, _cache) { return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$2, [..._cache[0] || (_cache[0] = [ vue.createElementVNode("path", { d: "m6 9 6 6 6-6" }, null, -1) ])]); } const IconChevron = { render }; const _hoisted_1$1 = { class: "selected-text" }; const _hoisted_2$1 = ["onClick"]; const _sfc_main$2 = vue.defineComponent({ __name: "SteamSelect", props: { modelValue: { type: [String, Number, Boolean] }, options: {}, icon: {} }, emits: ["update:modelValue"], setup(__props, { emit: __emit }) { const activeSelectId = vue.ref(null); const props = __props; const emit = __emit; const isOpen = vue.ref(false); const instanceId = Symbol("SteamSelect"); const containerRef = vue.ref(null); const listRef = vue.ref(null); const dropdownStyle = vue.ref({ transform: "translate(0, 0)", width: "0px", top: "0px", left: "0px" }); const selectedLabel = vue.computed(() => { const opt = props.options.find((o) => o.value === props.modelValue); return opt ? opt.label : ""; }); const updatePosition = () => { if (containerRef.value && isOpen.value) { const rect = containerRef.value.getBoundingClientRect(); dropdownStyle.value = { transform: "none", width: `${rect.width}px`, top: `${rect.bottom + 1}px`, left: `${rect.left}px` }; } }; const scrollToSelected = async () => { await vue.nextTick(); if (listRef.value) { const selected = listRef.value.querySelector(".is-selected"); if (selected) { selected.scrollIntoView({ block: "nearest" }); } } }; const toggleDropdown = async () => { isOpen.value = !isOpen.value; if (isOpen.value) { await vue.nextTick(); updatePosition(); await scrollToSelected(); } }; const selectOption = (val) => { emit("update:modelValue", val); isOpen.value = false; }; const handleClickOutside = (event) => { if (containerRef.value && !containerRef.value.contains(event.target)) { const dropdown = document.querySelector(".steam-options-list"); if (dropdown && dropdown.contains(event.target)) return; isOpen.value = false; } }; vue.watch(isOpen, async (val) => { if (val) { activeSelectId.value = instanceId; window.addEventListener("scroll", updatePosition, true); window.addEventListener("resize", updatePosition); } else { if (activeSelectId.value === instanceId) { activeSelectId.value = null; } window.removeEventListener("scroll", updatePosition, true); window.removeEventListener("resize", updatePosition); } }); vue.watch(activeSelectId, (newId) => { if (newId !== instanceId) { isOpen.value = false; } }); vue.onMounted(() => { window.addEventListener("click", handleClickOutside); }); vue.onUnmounted(() => { window.removeEventListener("click", handleClickOutside); window.removeEventListener("scroll", updatePosition, true); window.removeEventListener("resize", updatePosition); }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { class: "steam-select-container", ref_key: "containerRef", ref: containerRef }, [ vue.createElementVNode("div", { class: vue.normalizeClass(["steam-select", { "is-open": isOpen.value }]), onClick: toggleDropdown }, [ vue.createElementVNode("span", _hoisted_1$1, vue.toDisplayString(selectedLabel.value), 1), vue.createElementVNode("div", { class: vue.normalizeClass(["steam-select-arrow", { "is-open": isOpen.value }]) }, [ vue.createVNode(vue.unref(IconChevron)) ], 2) ], 2), (vue.openBlock(), vue.createBlock(vue.Teleport, { to: "body" }, [ isOpen.value ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, class: "steam-options-list", style: vue.normalizeStyle(dropdownStyle.value), ref_key: "listRef", ref: listRef }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(__props.options, (opt) => { return vue.openBlock(), vue.createElementBlock("div", { key: opt.value, class: vue.normalizeClass(["steam-option", { "is-selected": opt.value === __props.modelValue }]), onClick: ($event) => selectOption(opt.value) }, vue.toDisplayString(opt.label), 11, _hoisted_2$1); }), 128)) ], 4)) : vue.createCommentVNode("", true) ])) ], 512); }; } }); const SteamSelect = _export_sfc(_sfc_main$2, [["__scopeId", "data-v-b7059657"]]); const _sfc_main$1 = vue.defineComponent({ __name: "SteamSwitch", props: { modelValue: { type: Boolean } }, emits: ["update:modelValue"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { class: vue.normalizeClass(["steam-switch", { "is-on": props.modelValue }]), onClick: _cache[0] || (_cache[0] = ($event) => emit("update:modelValue", !props.modelValue)) }, [..._cache[1] || (_cache[1] = [ vue.createElementVNode("div", { class: "steam-switch-thumb" }, null, -1) ])], 2); }; } }); const SteamSwitch = _export_sfc(_sfc_main$1, [["__scopeId", "data-v-3c1a2067"]]); const _hoisted_1 = { id: "spc-app" }; const _hoisted_2 = { class: "steam-settings-container" }; const _hoisted_3 = { class: "setting-row" }; const _hoisted_4 = { class: "control" }; const _hoisted_5 = { class: "setting-row" }; const _hoisted_6 = { class: "control" }; const _hoisted_7 = { class: "setting-row" }; const _hoisted_8 = { class: "control" }; const _hoisted_9 = { class: "setting-row" }; const _hoisted_10 = { class: "control" }; const _hoisted_11 = ["value"]; const _hoisted_12 = { class: "setting-row" }; const _hoisted_13 = { class: "control" }; const _hoisted_14 = { key: 0, class: "setting-row" }; const _hoisted_15 = { class: "control" }; const _hoisted_16 = { class: "setting-row" }; const _hoisted_17 = { class: "control" }; const _hoisted_18 = { class: "setting-row" }; const _hoisted_19 = { class: "control" }; const _sfc_main = vue.defineComponent({ __name: "App", setup(__props) { const vueCountyInfos = countyInfos.map((c) => ({ label: `${c.name} (${c.code})`, value: c.code })); const dialogOpen = vue.ref(false); const setting = vue.reactive(new Setting()); GmUtils.addMenuClickEventListener(IM_MENU_SETTING, () => dialogOpen.value = true); vue.onMounted(() => Object.assign(setting, SettingManager.instance.setting)); const resetSetting = () => { const defaultSetting = new Setting(); Object.assign(setting, defaultSetting); SettingManager.instance.saveSetting(setting); dialogOpen.value = false; }; const handleSave = () => { SettingManager.instance.saveSetting(setting); dialogOpen.value = false; }; const logLevels = Object.values(LogDefinitions).map((d) => ({ label: d.label, value: d.label })); const currencyPositionOptions = [ { label: "价格之前", value: true }, { label: "价格之后", value: false } ]; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ vue.createVNode(SteamDialog, { open: dialogOpen.value, title: "价格转换", onClose: _cache[9] || (_cache[9] = ($event) => dialogOpen.value = false) }, { action: vue.withCtx(() => [ vue.createElementVNode("button", { class: "steam-btn gray", onClick: resetSetting }, "重置"), vue.createElementVNode("button", { class: "steam-btn gray", onClick: _cache[8] || (_cache[8] = ($event) => dialogOpen.value = false) }, "取消"), vue.createElementVNode("button", { class: "steam-btn blue", onClick: handleSave }, "保存设置") ]), default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_2, [ vue.createElementVNode("div", _hoisted_3, [ _cache[10] || (_cache[10] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "目标区域"), vue.createElementVNode("div", { class: "desc" }, "将价格转换为此区域的货币") ], -1)), vue.createElementVNode("div", _hoisted_4, [ vue.createVNode(SteamSelect, { modelValue: setting.countyCode, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => setting.countyCode = $event), options: vue.unref(vueCountyInfos) }, null, 8, ["modelValue", "options"]) ]) ]), vue.createElementVNode("div", _hoisted_5, [ _cache[11] || (_cache[11] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "货币符号"), vue.createElementVNode("div", { class: "desc" }, "转换后的价格的货币符号") ], -1)), vue.createElementVNode("div", _hoisted_6, [ vue.withDirectives(vue.createElementVNode("input", { type: "text", class: "steam-input", "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => setting.currencySymbol = $event) }, null, 512), [ [vue.vModelText, setting.currencySymbol] ]) ]) ]), vue.createElementVNode("div", _hoisted_7, [ _cache[12] || (_cache[12] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "货币符号展示位置"), vue.createElementVNode("div", { class: "desc" }, "控制转换后的价格货币符号的位置") ], -1)), vue.createElementVNode("div", _hoisted_8, [ vue.createVNode(SteamSelect, { modelValue: setting.currencySymbolBeforeValue, "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => setting.currencySymbolBeforeValue = $event), options: currencyPositionOptions }, null, 8, ["modelValue"]) ]) ]), vue.createElementVNode("div", _hoisted_9, [ _cache[13] || (_cache[13] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "汇率缓存有效期 (小时)"), vue.createElementVNode("div", { class: "desc" }, "在此时间内将使用缓存汇率") ], -1)), vue.createElementVNode("div", _hoisted_10, [ vue.createElementVNode("input", { type: "number", class: "steam-input", value: setting.rateCacheExpired / (60 * 60 * 1e3), onInput: _cache[3] || (_cache[3] = ($event) => setting.rateCacheExpired = Number($event.target.value) * (60 * 60 * 1e3)) }, null, 40, _hoisted_11) ]) ]), vue.createElementVNode("div", _hoisted_12, [ _cache[14] || (_cache[14] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "使用自定义汇率"), vue.createElementVNode("div", { class: "desc" }, "手动指定转换比率,不再自动获取") ], -1)), vue.createElementVNode("div", _hoisted_13, [ vue.createVNode(SteamSwitch, { modelValue: setting.useCustomRate, "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => setting.useCustomRate = $event) }, null, 8, ["modelValue"]) ]) ]), setting.useCustomRate ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_14, [ _cache[15] || (_cache[15] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "自定义汇率"), vue.createElementVNode("div", { class: "desc" }, "开启“使用自定义汇率”后生效") ], -1)), vue.createElementVNode("div", _hoisted_15, [ vue.withDirectives(vue.createElementVNode("input", { type: "number", class: "steam-input", "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => setting.customRate = $event) }, null, 512), [ [vue.vModelText, setting.customRate] ]) ]) ])) : vue.createCommentVNode("", true), vue.createElementVNode("div", _hoisted_16, [ _cache[16] || (_cache[16] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "在市场首页禁用"), vue.createElementVNode("div", { class: "desc" }, "防止与其他市场插件冲突,开启后在市场首页不进行转换") ], -1)), vue.createElementVNode("div", _hoisted_17, [ vue.createVNode(SteamSwitch, { modelValue: setting.disableOnMarket, "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => setting.disableOnMarket = $event) }, null, 8, ["modelValue"]) ]) ]), vue.createElementVNode("div", _hoisted_18, [ _cache[17] || (_cache[17] = vue.createElementVNode("div", { class: "info" }, [ vue.createElementVNode("div", { class: "label" }, "日志等级"), vue.createElementVNode("div", { class: "desc" }, "用于调试插件运行问题") ], -1)), vue.createElementVNode("div", _hoisted_19, [ vue.createVNode(SteamSelect, { modelValue: setting.logLevel, "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => setting.logLevel = $event), options: vue.unref(logLevels) }, null, 8, ["modelValue", "options"]) ]) ]) ]) ]), _: 1 }, 8, ["open"]) ]); }; } }); const App = _export_sfc(_sfc_main, [["__scopeId", "data-v-ffafb838"]]); class SpcContext { _setting; _targetCountyInfo; _currentCountyInfo; constructor(setting, targetCountyInfo, targetCountyCode) { this._setting = setting; this._targetCountyInfo = targetCountyInfo; this._currentCountyInfo = targetCountyCode; } static getContext() { return _unsafeWindow.spcContext; } get setting() { return this._setting; } get targetCountyInfo() { return this._targetCountyInfo; } get currentCountyInfo() { return this._currentCountyInfo; } } class CookieCountyInfoProvider { name() { return "cookie"; } match() { return true; } async getCountyCode() { return new Promise(async (resolve, reject) => { _GM_cookie.list({ name: "steamCountry" }, (cookies) => { if (cookies && cookies.length > 0) { const match = cookies[0].value.match(/^[a-zA-Z][a-zA-Z]/); if (match) { resolve(match[0]); } } reject(); }); }); } } class Http { static get(cls, url, details) { if (!details) { details = { url }; } details.method = "GET"; return this.request(cls, details); } static post(url, cls, details) { if (!details) { details = { url }; } details.method = "POST"; return this.request(cls, details); } static request(cls, details) { return new Promise((resolve, reject) => { details.onload = (response) => { if (cls.name === String.name) { resolve(response.response); } else { const json = JSON.parse(response.response); resolve(Jsons.readJson(json, cls)); } }; details.onerror = (error) => reject(error); _GM_xmlhttpRequest(details); }); } } class StorePageCountyCodeProvider { name() { return "商店页面"; } match() { return window.location.href.includes("store.steampowered.com"); } async getStoreDocument() { return document; } async getCountyCode() { const storeDom = await this.getStoreDocument(); try { let countyCode = storeDom?.GStoreItemData?.rgNavParams?.__page_default_obj?.countrycode; if (countyCode) { return countyCode; } } catch (e) { Logger.warn("读取商店页面区域代码变量失败: " + e.message); } const dataConfig = storeDom.querySelector("div#application_config")?.getAttribute("data-config"); if (dataConfig) { const countyCode = JSON.parse(dataConfig)?.["COUNTRY"]; if (countyCode) { return countyCode; } } const matcher = new RegExp('(?<="countrycode":")[A-Z]{2}(?!=")', "g"); const scripts = storeDom.querySelectorAll("script"); for (let scriptText in scripts) { const flag = scriptText.includes(`("countrycode":")HK(")`); if (flag) { const match = scriptText.match(matcher); if (match) { return match.toString(); } } } throw new Error("无法从商店页面获取国家代码"); } } class RequestStorePageCountyCodeProvider extends StorePageCountyCodeProvider { name() { return "请求商店页面"; } match() { const href = window.location.href; return !href.includes("store.steampowered.com") || href.includes("store.steampowered.com/wishlist"); } async getStoreDocument() { const storeHtml = await Http.get(String, "https://store.steampowered.com/"); const parser = new DOMParser(); return parser.parseFromString(storeHtml, "text/html"); } } class MarketPageCountyCodeProvider { name() { return "市场页面"; } match() { return window.location.href.includes("steamcommunity.com"); } getCountyCode() { return new Promise((resolve, reject) => { try { const code = g_strCountryCode; if (code) return resolve(code); } catch (err) { Logger.error(err); } reject(); }); } } class CountyCodeProviderManager { static instance = new CountyCodeProviderManager(); providers; constructor() { this.providers = [ new StorePageCountyCodeProvider(), new MarketPageCountyCodeProvider(), new RequestStorePageCountyCodeProvider(), new CookieCountyInfoProvider() ]; } async getCountyCode() { Logger.info("尝试获取区域代码"); for (let provider of this.providers) { if (!provider.match()) { continue; } Logger.debug(`尝试通过[${provider.name()}]获取区域代码`); try { const countyCode = await provider.getCountyCode(); Logger.info(`通过[${provider.name()}]获取区域代码成功`); return countyCode; } catch (e) { Logger.error(`通过[${provider.name()}]获取区域代码失败`); } } throw new Error("所有获取区域代码策略都获取失败"); } } class RateCache { constructor(from, to, rate, createdAt) { this.from = from; this.to = to; this.createdAt = createdAt || 0; this.rate = rate || 0; } from; to; createdAt; rate; } class RateCaches { caches = new Map(); getCache(from, to) { const key = this.buildCacheKey(from, to); return this.caches.get(key); } setCache(cache) { this.caches.set(this.buildCacheKey(cache.from, cache.to), cache); } buildCacheKey(from, to) { return `${from}:${to}`; } } class RateManager { static instance = new RateManager(); memoryCache = null; pendingRequests = new Map(); isBroken = false; constructor() { } getName() { return "RateManager"; } async getRate4Remote(sourceCurrency, targetCurrency) { Logger.info(`[Rate] 远程获取汇率: ${sourceCurrency} -> ${targetCurrency}...`); let rate; const url = `https://api.augmentedsteam.com/rates/v1?to=${sourceCurrency}`; try { const res = await Http.get(Map, url); rate = res.get(targetCurrency)?.[sourceCurrency]; } catch (e) { Logger.error(`[Rate] 获取汇率对(${sourceCurrency} -> ${targetCurrency})失败`, e); } if (rate) return rate; throw Error(`所有汇率获取实现对币种对(${sourceCurrency} -> ${targetCurrency})均失败`); } async getRate(sourceCurrencyCode, targetCurrencyCode) { if (this.isBroken) { throw new Error("[Rate] 汇率服务已熔断,停止请求"); } const context = SpcContext.getContext(); const from = sourceCurrencyCode || context.currentCountyInfo.currencyCode; const to = targetCurrencyCode || context.targetCountyInfo.currencyCode; const cacheKey = `${from}:${to}`; if (context.setting.useCustomRate && !sourceCurrencyCode) { return context.setting.customRate; } const pending = this.pendingRequests.get(cacheKey); if (pending) return pending; const requestPromise = (async () => { try { if (!this.memoryCache) { this.memoryCache = this.loadRateCache(); Logger.info(`[Rate] 内存缓存加载:`, this.memoryCache); } let cache = this.memoryCache.getCache(from, to); const now = Date.now(); const expired = context.setting.rateCacheExpired; if (!cache || !cache.rate || now > cache.createdAt + expired) { Logger.info(`[Rate] 缓存缺失或过期: ${from} -> ${to}`); try { const remoteRate = await this.getRate4Remote(from, to); cache = new RateCache(from, to, remoteRate, now); this.memoryCache.setCache(cache); this.saveRateCache(); } catch (e) { if (cache && cache.rate) { Logger.warn(`[Rate] 远程刷新失败,回退至旧缓存: ${from} -> ${to}`); return cache.rate; } this.isBroken = true; Logger.error(`[Rate] 汇率获取彻底失败,触发全局熔断: ${from} -> ${to}`); throw e; } } return cache.rate; } finally { this.pendingRequests.delete(cacheKey); } })(); this.pendingRequests.set(cacheKey, requestPromise); return requestPromise; } loadRateCache() { const setting = SpcContext.getContext().setting; if (setting.oldVersion !== setting.currVersion) { Logger.info(`脚本版本发生变化需要刷新汇率缓存`); this.clear(); return new RateCaches(); } Logger.info(`读取汇率缓存`); return GmUtils.getObjValue(RateCaches, STORAGE_KEY_RATE_CACHES, new RateCaches()); } saveRateCache() { if (!this.memoryCache) return; Logger.info("保存汇率缓存", this.memoryCache); GmUtils.setObjValue(STORAGE_KEY_RATE_CACHES, this.memoryCache); } clear() { this.memoryCache = null; GmUtils.deleteValue(STORAGE_KEY_RATE_CACHES); } } class SpcManager { static instance = new SpcManager(); constructor() { } setCountyCode(code) { SettingManager.instance.setCountyCode(code); } setCurrencySymbol(symbol) { SettingManager.instance.setCurrencySymbol(symbol); } setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue) { SettingManager.instance.setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue); } setRateCacheExpired(expired) { SettingManager.instance.setRateCacheExpired(expired); } resetSetting() { SettingManager.instance.reset(); } clearCache() { RateManager.instance.clear(); } setUseCustomRate(isUseCustomRate) { SettingManager.instance.setUseCustomRate(isUseCustomRate); } setCustomRate(customRate) { SettingManager.instance.setCustomRate(customRate); } } class AbstractConverter { match(elementSnap) { if (!elementSnap || !elementSnap.element) { return false; } const status = elementSnap.element.getAttribute(Attrs.STATUS_KEY); const converted = Attrs.STATUS_CONVERTED === status; if (converted) { return false; } const content = elementSnap.textContext; if (!content) { return false; } if (content.match(/\d/) === null) { return false; } if (/^[,.\d\s]+$/.test(content)) { return false; } const parent = elementSnap.element.parentElement; if (!parent) { return false; } for (const selector of this.getCssSelectors()) { const element = parent.querySelector(selector); if (element && element === elementSnap.element) { elementSnap.selector = selector; return true; } } return false; } afterConvert(elementSnap) { elementSnap.element.setAttribute(Attrs.STATUS_KEY, Attrs.STATUS_CONVERTED); } } function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const allSepChars = Array.from(new Set( countyInfos.flatMap((c) => [c.decimalSep, c.thousandsSep].filter(Boolean)) )).map((s) => s === " " ? "\\s" : escapeRegex(s)).join(""); const NUMBER_BLOCK_REGEX = new RegExp(`\\d[\\d${allSepChars}]*\\d|\\d`, "g"); const SORTED_RULES_TEMPLATE = [...countyInfos].sort((a, b) => b.symbol.length - a.symbol.length || Number(a.fractionDigits === 0) - Number(b.fractionDigits === 0) || a.currencyCode.localeCompare(b.currencyCode)); function tryMatchSymbol(text, numStart, numEnd, info) { if (info.symbolPos === "front") { const prefix = info.symbol + info.symbolGap; const start2 = numStart - prefix.length; if (start2 < 0) return null; const slice = text.substring(start2, numStart); if (slice.toUpperCase() !== prefix.toUpperCase()) return null; return { matchStart: start2, matchEnd: numEnd }; } else { const suffix = info.symbolGap + info.symbol; const end = numEnd + suffix.length; if (end > text.length) return null; const slice = text.substring(numEnd, end); if (slice.toUpperCase() !== suffix.toUpperCase()) return null; return { matchStart: numStart, matchEnd: end }; } } function validateNumberDNA(numStr, info) { const seps = new Set( numStr.replace(/\d/g, "").split("").filter(Boolean).map((s) => /\s/.test(s) ? " " : s) ); for (const sep of seps) { if (sep !== info.thousandsSep && sep !== info.decimalSep) { return false; } } if (info.fractionDigits === 0 && info.decimalSep && seps.has(info.decimalSep)) { return false; } return true; } function parseNumber(numStr, info) { let clean = numStr; if (info.thousandsSep) { clean = clean.split(info.thousandsSep).join(""); } if (info.decimalSep && info.decimalSep !== ".") { clean = clean.replace(info.decimalSep, "."); } const val = parseFloat(clean); return isNaN(val) ? null : val; } class PriceProcessor { static async convertContent(text) { const setting = SettingManager.instance.setting; const targetCode = SpcContext.getContext().targetCountyInfo.code; return await this.processTextAsync( text, setting.countyCode, setting.currencySymbol, setting.currencySymbolBeforeValue, targetCode ); } static async processTextAsync(text, countryCode, targetSymbol, isBefore, targetCode) { if (!/\d/.test(text)) return text; const currentRule = countyInfos.find((c) => c.code === targetCode); const otherRules = SORTED_RULES_TEMPLATE.filter((c) => { return c.code !== countryCode && c.code !== targetCode; }); const matches = []; for (const numMatch of text.matchAll(NUMBER_BLOCK_REGEX)) { const numStr = numMatch[0]; const numStart = numMatch.index; const numEnd = numStart + numStr.length; let matched = false; if (currentRule && currentRule.code !== targetCode) { matched = this.tryRule(text, numStr, numStart, numEnd, currentRule, matches); } if (!matched) { for (const rule of otherRules) { if (this.tryRule(text, numStr, numStart, numEnd, rule, matches)) break; } } } if (matches.length === 0) return text; const uniqueCurrencies = [...new Set(matches.map((m) => m.info.currencyCode))]; const rateMap = new Map(); await Promise.all(uniqueCurrencies.map(async (code) => { try { const rate = await RateManager.instance.getRate(code); rateMap.set(code, rate); } catch (e) { Logger.error(`[PriceProcessor] 获取币种汇率失败: ${code}`, e); } })); let result = text; matches.sort((a, b) => b.index - a.index); for (const m of matches) { const rate = rateMap.get(m.info.currencyCode); if (!rate) continue; const converted = Number.parseFloat((m.value / rate).toFixed(2)); const formatted = isBefore ? `(${targetSymbol}${converted})` : `(${converted}${targetSymbol})`; result = result.substring(0, m.index) + m.raw + formatted + result.substring(m.index + m.length); } Logger.debug(`[PriceProcessor] 转换成功,命中 ${matches.length} 处价格`); return result; } static tryRule(text, numStr, numStart, numEnd, info, matches) { const range = tryMatchSymbol(text, numStart, numEnd, info); if (!range) return false; if (!validateNumberDNA(numStr, info)) return false; const value = parseNumber(numStr, info); if (value === null) return false; const raw = text.substring(range.matchStart, range.matchEnd); matches.push({ index: range.matchStart, length: raw.length, value, info, raw }); return true; } } class ElementConverter extends AbstractConverter { getCssSelectors() { const home = [ ".discount_prices > .discount_final_price", ".Hxi-pnf9Xlw- span.HOrB6lehQpg-", ".btnv6_white_transparent > span" ]; const category = [ ".Wh0L8EnwsPV_8VAu8TOYr", "._3j4dI1yA7cRfCvK8h406OB", "._1EKGZBnKFWOr3RqVdnLMRN", "._3fFFsvII7Y2KXNLDk_krOW" ]; const account = [ "div.accountData.price", ".addfunds_area_purchase_game.game_area_purchase_game > h1", ".addfunds_area_purchase_game.game_area_purchase_game.es_custom_money > p" ]; const wishlist = [ "div.Hxi-pnf9Xlw- > div._79DIT7RUQ5g-", "div.ME2eMO7C1Tk- > div.DOnsaVcV0Is-", "div.ME2eMO7C1Tk- > div.ywNldZ-YzEE-", "label.idELaaXmvTo-", "button.Wh-OfiQaHSM-" ]; const inventory = [ '#iteminfo1_item_market_actions div[id^="market_item_action_buyback_at_price_"]' ]; const cart = [ ".Panel.Focusable ._3-o3G9jt3lqcvbRXt8epsn.StoreOriginalPrice", ".Panel.Focusable .pk-LoKoNmmPK4GBiC9DR8", "._2WLaY5TxjBGVyuWe_6KS3N" ]; const cartCheckout = [ "#checkout_review_cart_area .checkout_review_item_price > .price", "#review_subtotal_value.price", "#review_total_value.price" ]; const market = [ "#account_dropdown .account_name", "#es_summary .es_market_summary_item" ]; const notify = [ ".QFW0BtI4l77AFmv1xLAkx._1B1XTNsfuwOaDPAkkr8M42 ._3hEeummFKRey8l5VXxZwxz > span" ]; const app = [ ".game_area_purchase_game_dropdown_selection > span", ".game_area_purchase_game_dropdown_menu_items_container_background.game_area_purchase_game_dropdown_menu_items_container td.game_area_purchase_game_dropdown_menu_item_text" ]; const selectors = [ ".discount_original_price", ".discount_final_price", ".col.search_price.responsive_secondrow strike", "#header_wallet_balance > span.tooltip", ".esi-wishlist-stat > .num", ".salepreviewwidgets_StoreOriginalPrice_1EKGZ", ".salepreviewwidgets_StoreSalePriceBox_Wh0L8", ".contenthubshared_OriginalPrice_3hBh3", ".contenthubshared_FinalPrice_F_tGv", ".salepreviewwidgets_StoreSalePriceBox_Wh0L8:not(.salepreviewwidgets_StoreSalePrepurchaseLabel_Wxeyn)", "#marketWalletBalanceAmount", "span.normal_price[data-price]", "span.sale_price", ".market_commodity_orders_header_promote:nth-child(even)", ".market_commodity_orders_table td:nth-child(odd)", ".market_table_value > span", ".jqplot-highlighter-tooltip", "tr.wallet_table_row > td.wht_total", "tr.wallet_table_row > td.wht_wallet_change.wallet_column", "tr.wallet_table_row > td.wht_wallet_balance.wallet_column", ".package_totals_row > .price:not(.bundle_discount)", "#package_savings_bar > .savings.bundle_savings", ".home_page_content_title a.btn_small_tall > span" ]; selectors.push(...home); selectors.push(...category); selectors.push(...account); selectors.push(...wishlist); selectors.push(...inventory); selectors.push(...cart); selectors.push(...cartCheckout); selectors.push(...market); selectors.push(...notify); selectors.push(...app); return selectors; } async convert(elementSnap) { elementSnap.element.textContent = await PriceProcessor.convertContent(elementSnap.textContext); return true; } } class TextNodeConverter extends AbstractConverter { parseFirstChildTextNodeFn = (el) => el.firstChild; cart = new Map([ [".Panel.Focusable ._18eO4-XadW5jmTpgdATkSz", [(el) => el.childNodes[1]]] ]); wishlist = new Map([ ["div.stat.svelte-1epviyc", [this.parseFirstChildTextNodeFn]] ]); targets = new Map([ [ ".col.search_price.responsive_secondrow", [ (el) => el.firstChild.nextSibling.nextSibling.nextSibling, this.parseFirstChildTextNodeFn ] ], ["#header_wallet_balance", [this.parseFirstChildTextNodeFn]], [".game_purchase_price.price", [this.parseFirstChildTextNodeFn]], [".home_page_content_title", [this.parseFirstChildTextNodeFn]], [".game_area_dlc_row > .game_area_dlc_price", [ (el) => el, this.parseFirstChildTextNodeFn ]], ...this.cart, ...this.wishlist ]); getCssSelectors() { return [...this.targets.keys()]; } async convert(elementSnap) { const selector = elementSnap.selector; this.targets.get(selector); const parseNodeFns = this.targets.get(selector) || []; if (!parseNodeFns) { return false; } const textNode = this.safeParseNode(selector, elementSnap.element, parseNodeFns); if (!textNode) { return false; } const content = textNode.nodeValue; if (!content || content.trim().length === 0) { return false; } textNode.nodeValue = await PriceProcessor.convertContent(content); return true; } safeParseNode(selector, el, fns) { for (let fn of fns) { try { const node = fn(el); if (node && node.nodeName === "#text" && node.nodeValue && node.nodeValue.length > 0) { return node; } } catch (e) { console.debug("获取文本节点失败,但不确定该节点是否一定会出现。selector:" + selector); } } return null; } } class ConverterManager { static instance = new ConverterManager(); converters; constructor() { this.converters = [ new ElementConverter(), new TextNodeConverter() ]; } getSelector() { return this.converters.map((exchanger) => exchanger.getCssSelectors()).flat(1).join(", "); } async convert(elements) { if (!elements) { return; } const tasks = []; elements.forEach((element) => { const elementSnap = { element, textContext: element.textContent, classList: element.classList, attributes: element.attributes }; this.converters.filter((converter) => converter.match(elementSnap)).forEach((converter) => { const task = (async () => { try { const exchanged = await converter.convert(elementSnap); if (exchanged) { converter.afterConvert(elementSnap); } } catch (e) { console.group("转换失败"); console.error(e); console.error("转换失败请将下列内容反馈给开发者,右键 > 复制(copy) > 复制元素(copy element)"); console.error("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); console.error(element); console.error("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"); console.groupEnd(); } })(); tasks.push(task); }); }); await Promise.all(tasks); } } (async () => { await initContext(); initApp(); registerMenu(); await start(); })(); async function start() { const context = SpcContext.getContext(); if (context.setting.disableOnMarket && window.location.href === "https://steamcommunity.com/market/") { Logger.info("已开启市场首页禁用,跳过转换"); return; } if (context.currentCountyInfo.code === context.targetCountyInfo.code) { Logger.info(`${context.currentCountyInfo.name}无需转换`); return; } Logger.info(` 核心启动:目标:${context.targetCountyInfo.name}`); await convert(); } async function convert() { const exchangerManager = ConverterManager.instance; const elements = document.querySelectorAll(exchangerManager.getSelector()); await exchangerManager.convert(elements); const selector = exchangerManager.getSelector(); const priceObserver = new MutationObserver(async (mutations) => { const uniqueTargets = new Set(); mutations.forEach((mutation) => { const target = mutation.target; const priceEls = target.querySelectorAll(selector); priceEls.forEach((el) => uniqueTargets.add(el)); }); if (uniqueTargets.size > 0) { await exchangerManager.convert(uniqueTargets); } }); priceObserver.observe(document.body, { childList: true, subtree: true }); } function initApp() { vue.createApp(App).mount( (() => { const app = document.createElement("div"); app.setAttribute("id", "spc-menu"); document.body.append(app); return app; })() ); } function registerMenu() { GmUtils.registerMenuCommand(IM_MENU_SETTING); GmUtils.registerMenuCommand(IM_MENU_ISSUES, () => GmUtils.openInTab("https://github.com/marioplus/steam-price-converter")); } async function initContext() { const setting = SettingManager.instance.setting; setLogLevel(setting.logLevel); let targetCountyInfo = countyCode2Info.get(setting.countyCode); if (!targetCountyInfo) { Logger.warn(`获取转换后的国家(${setting.countyCode})信息失败,默认为美国:` + setting.countyCode); targetCountyInfo = countyCode2Info.get("US"); } Logger.info("目标区域:", targetCountyInfo); const currCountyCode = await CountyCodeProviderManager.instance.getCountyCode(); if (currCountyCode) { Logger.info("区域代码:", currCountyCode); } let currCountInfo = countyCode2Info.get(currCountyCode); if (!currCountInfo) { Logger.warn("缺少当前国家的信息映射:county: " + currCountyCode + ", 默认为美国"); currCountInfo = countyCode2Info.get("US"); } Logger.info("当前区域:", currCountInfo); _unsafeWindow.SpcManager = SpcManager.instance; _unsafeWindow.spcContext = new SpcContext(setting, targetCountyInfo, currCountInfo); } })(Vue);