├── .github ├── ISSUE_TEMPLATE │ └── bug-反馈.md └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── RealMain.ts ├── SpcContext.ts ├── SpcManager.ts ├── constant │ └── Constant.ts ├── converter │ ├── AbstractConverter.ts │ ├── ConvertUtils.ts │ ├── ConverterManager.ts │ ├── ElementConverter.ts │ └── TextNodeConverter.ts ├── county │ ├── CookieCountyInfoGetter.ts │ ├── CountyCodeGetterManager.ts │ ├── CountyInfo.ts │ ├── ICountyInfoGetter.ts │ ├── MarketPageCountyCodeGetter.ts │ ├── RequestStorePageCountyCodeGetter.ts │ ├── StorePageCountyCodeGetter.ts │ ├── UserConfigCountyInfoGetter.ts │ ├── county-data-all.json │ └── county-data.json ├── main.ts ├── rate │ ├── AugmentedSteamRateApi.ts │ ├── IRateApi.ts │ ├── RateCaches.ts │ └── RateManager.ts ├── setting │ ├── Setting.ts │ └── SettingManager.ts ├── style │ ├── home.less │ ├── market.less │ ├── search.less │ └── style.less ├── utils │ ├── GmUtils.ts │ ├── Http.ts │ ├── Jsons.ts │ ├── Logger.ts │ ├── ReactUtils.ts │ └── Strings.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug-反馈.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 反馈 3 | about: 创建一个反馈帮助我们更进一步 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **bug 描述** 11 | 对错误的清晰描述。 12 | 13 | **如何重现** 14 | 重现该问题的步骤: 15 | 1. 进入 '...' 16 | 2. 点击 '....' 17 | 3. 滑动到 '....' 18 | 4. 看见的异常行为 19 | 20 | **预期** 21 | 对于改步骤您希望得到什么结果 22 | 23 | **截图** 24 | 如果可以的话, 添加几张截图以帮助您更好的说明问题。 25 | 26 | **猜测** 27 | 如果您关于该问题的怀疑方向,请在此处提供,以帮助我们更快的修复问题 28 | 29 | **环境(请填写以下信息):** 30 | - OS: [如: win] 31 | - Browser [如. chrome, safari] 32 | - Version [如. 100] 33 | 34 | **附加信息** 35 | 在此处填写有关该问题的其他信息 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Releases 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | extract-metadata: 8 | name: Extract Version and Changelog 9 | runs-on: ubuntu-latest 10 | outputs: 11 | version: ${{ steps.extract.outputs.version }} 12 | changelog: ${{ steps.extract.outputs.changelog }} 13 | artifact: ${{ steps.extract.outputs.artifact }} 14 | steps: 15 | # 检出代码 16 | - name: Check Out Code 17 | uses: actions/checkout@v4 18 | 19 | # 提取版本号和日志 20 | - name: Extract Version and Changelog 21 | id: extract 22 | run: | 23 | # 验证 CHANGELOG 文件是否存在 24 | if [ ! -f CHANGELOG.md ]; then 25 | echo "Error: CHANGELOG.md not found" >&2 26 | exit 1 27 | fi 28 | 29 | # 提取最新版本号 30 | VERSION=$(awk '/^## \[/{gsub(/[^0-9.]/, "", $2); print $2; exit}' CHANGELOG.md) 31 | if [ -z "$VERSION" ]; then 32 | echo "Error: Version not found in CHANGELOG.md" >&2 33 | exit 1 34 | fi 35 | 36 | # 提取物料名称 37 | ARTIFACT=$(basename "${{ github.repository }}").user.js-$VERSION 38 | echo "artifact=$ARTIFACT" 39 | 40 | # 提取变更日志 41 | START_LINE=$(grep -n '^## \[' CHANGELOG.md | head -n 1 | cut -d: -f1) 42 | END_LINE=$(grep -n '^## \[' CHANGELOG.md | sed -n '2p' | cut -d: -f1) 43 | END_LINE=${END_LINE:-$(wc -l < CHANGELOG.md)} 44 | 45 | CHANGELOG=$(sed -n "$((START_LINE - 1)),$((END_LINE - 1))p" CHANGELOG.md) 46 | 47 | echo "version=$VERSION" >> $GITHUB_OUTPUT 48 | echo "changelog<> $GITHUB_OUTPUT 49 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 50 | echo "EOF" >> $GITHUB_OUTPUT 51 | echo "artifact=$ARTIFACT" >> $GITHUB_OUTPUT 52 | 53 | build: 54 | name: Build and Package 55 | runs-on: ubuntu-latest 56 | needs: extract-metadata 57 | steps: 58 | # 检出代码 59 | - name: Check Out Code 60 | uses: actions/checkout@v4 61 | 62 | # 安装 Node.js 环境 63 | - name: Setup Node.js 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: 20 67 | 68 | # 安装依赖 69 | - name: Install Dependencies 70 | run: npm ci 71 | 72 | # 运行构建 73 | - name: Build Project 74 | run: npm run build 75 | 76 | # 上传构建产物 77 | - name: Upload Build Artifact 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: ${{ needs.extract-metadata.outputs.artifact }} 81 | path: dist/* 82 | 83 | release: 84 | name: Create GitHub Release 85 | runs-on: ubuntu-latest 86 | needs: [ extract-metadata, build ] 87 | permissions: 88 | contents: write 89 | steps: 90 | # 检出代码 91 | - name: Check Out Code 92 | uses: actions/checkout@v4 93 | 94 | # 下载构建产物 95 | - name: Download Build Artifacts 96 | uses: actions/download-artifact@v4 97 | with: 98 | name: ${{ needs.extract-metadata.outputs.artifact }} 99 | path: dist 100 | 101 | # 创建发布草稿 102 | - name: Create Release Draft 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | run: | 106 | # 删除现有草稿(可选) 107 | gh api repos/${{ github.repository }}/releases \ 108 | --jq '.[] | select(.draft == true) | .id' \ 109 | | xargs -I '{}' gh api -X DELETE repos/${{ github.repository }}/releases/{} || true 110 | 111 | # 创建发布草稿 112 | gh release create v${{ needs.extract-metadata.outputs.version }} \ 113 | --draft \ 114 | --title "v${{ needs.extract-metadata.outputs.version }}" \ 115 | --notes "${{ needs.extract-metadata.outputs.changelog }}" \ 116 | dist/* 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .fastRequest 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # steam-price-converter Changelog 4 | 5 | ## [2.5.4] - 2024-12-21 6 | 7 | 1. 重新适配购物车页面 8 | 9 | ## [2.5.3] - 2024-12-21 10 | 11 | 1. 区域数据只保留有效的 12 | 2. 优化脚本大小 13 | 3. 获取不到区域是默认为美国 14 | 15 | ## [2.5.2] - 2024-12-19 16 | 17 | 1. 先修复价格被转换多次的问题 18 | 2. 更换展示效果有“/”调整为“()” 19 | 3. 对首页/搜索页/市场列表的样式进行调整,避免价格溢出 20 | 21 | ## [2.5.1] - 2024-12-18 22 | 23 | 1. 更换展示效果有“()”调整为“/”,减少当金额过大时金额挤成多行 24 | 2. 重新适配分类页面 25 | 26 | ## [2.5.0] - 2024-12-18 27 | 28 | 1. 适配愿望单改版 29 | 30 | ## [2.4.5] - 2024-12-09 31 | 32 | 1. 适配愿望单改版 33 | 34 | ## [2.4.4] - 2024-10-13 35 | 36 | 1. 俄罗斯货币提取失败,pуб中的p应该使用 \u440 37 | 38 | ## [2.4.3] - 2024-09-12 39 | 40 | 1. 巴基斯坦和南非货币调整为 USD 41 | 42 | ## [2.4.2] - 2024-07-17 43 | 44 | 1. 更正设置界面错别字 45 | 2. 更正宽度设置问题出现的横向滚动条 46 | 47 | ## [2.4.1] - 2024-06-28 48 | 49 | 1. 斯里兰卡货币调整为 USD 50 | 51 | ## [2.4.0] - 2024-05-12 52 | 53 | 1. fix: 台湾货币转换千分符处理问题 54 | 2. 设置界面使用 mdui2 重写 55 | 3. 增加日志等级可配置 56 | 57 | ## [2.3.10] - 2024-04-03 58 | 59 | 1. fix: 巴林货币改为USD 60 | 61 | ## [2.3.9] - 2024-03-05 62 | 63 | 1. fix: 印尼区价格转换不正确 64 | 65 | ## [2.3.8] - 2024-02-23 66 | 67 | 1. fix: dlc中有折扣的价格转换异常 68 | 69 | ## [2.3.7] - 2024-02-23 70 | 71 | 1. 修复加载时机正确导致获取区域代码失败 72 | 73 | ## [2.3.6] - 2024-02-22 74 | 75 | 1. 修复俄罗斯货币转换问题 76 | 77 | ## [2.3.5] - 2024-02-18 78 | 79 | 1. 使用新的日志工具类 80 | 81 | ## [2.3.5] - 2024-02-18 82 | 83 | 1. 修复价格中的空格无法被正确处理问题 84 | 85 | ## [2.3.4] - 2024-02-17 86 | 87 | 1. 适配商店分类卡片 & 消费历史页面 88 | 89 | ## [2.3.3] - 2024-02-17 90 | 91 | 1. AugmentedSteam Api获取接口调整兼容 92 | 93 | ## [2.3.2] - 2023-11-22 94 | 95 | 1. 阿根廷和土耳其的货币改为USD 96 | 97 | ## [2.3.1] - 2023-09-18 98 | 99 | 1. 根据GaCoY的反馈,阿塞拜疆的steam货币改为USD 100 | 2. 不再兼容steam++ 101 | 102 | ## [2.3.0] - 2023-07-05 103 | 104 | 1. 使用AugmentedSteam提供的Api获取汇率 105 | 106 | ## [2.2.2] - 2023-06-09 107 | 108 | 1. 处理市场白屏的情况 109 | 110 | ## [2.2.1] - 2023-06-09 111 | 112 | 1. 修复2.2.0发版有问题 113 | 114 | ## [2.2.0] - 2023-06-09 115 | 116 | 1. 新增一个用于设置的菜单页面 117 | 118 | ## [2.1.1] - 2023-05-28 119 | 120 | 1. 兼容南非大于1000时的价格解析异常 121 | 122 | ## [2.1.0] - 2023-05-20 123 | 124 | 1. 增加自定义汇率功能 125 | 126 | ## [2.0.3] - 2023-04-05 127 | 128 | 1. 兼容商店网页结构发生变化导致获取无法获取当前区域 129 | 130 | ## [2.0.2] - 2023-01-28 131 | 132 | 1. 适配购物车页面的漏网之鱼 133 | 2. 适配捆绑包页面的漏网之鱼 134 | 3. 适配低于xxx的分类标题 135 | 4. 增加一种获取区域代码的方式 136 | 5. 增加 SpcManager 用于统一设置,包括设置以及其他的一切 137 | 6. 汇率缓存有效期可设置 138 | 7. 解决一个低概率出现的问题 139 | 140 | ## [2.0.1] - 2023-01-18 141 | 142 | 1. 适配游戏详情的dlc列表 143 | 144 | ## [2.0.0] - 2023-01-18 145 | 146 | 1. 可同时缓存多个汇率,通过手动修改目标国家代码,理论上支持所有国家价格互相转换 147 | 148 | ## [1.0.11] - 2023-01-14 149 | 150 | 1. Augmented Steam 兼容问题修复 151 | 152 | ## [1.0.10] - 2022-12-26 153 | 154 | 1. 适配G胖更新网页结构导致获取区域代码失效(iframe) 155 | 156 | ## [1.0.9] - 2022-12-21 157 | 158 | 1. 适配G胖更新网页结构导致获取区域代码失效(#3、#5) 159 | 160 | ## [1.0.8] - 2022-10-08 161 | 162 | ### 新增 163 | 164 | 1. 消费记录页面适配 165 | 166 | ## [1.0.7] - 2022-10-07 167 | 168 | ### 新增 169 | 170 | 1. 兼容 `Augmented Steam` 171 | 172 | ## [1.0.6] - 2022-10-06 173 | 174 | ### 新增 175 | 176 | 1. 使用 `cdn` 导入 `reflect-metadata` 减少脚本体积 177 | 178 | ## [1.0.4] - 2022-10-06 179 | 180 | ### 新增 181 | 182 | 1. 转换失败能打印出对应的元素信息,方便排查错误 183 | 184 | ### 修复 185 | 186 | 1. 购物车复核适配 187 | 2. 待处理的余额适配 188 | 189 | ## [1.0.3] - 2022-10-05 190 | 191 | ### 修复 192 | 193 | 1. 增加几种适配 194 | 195 | ## [1.0.2] - 2022-10-05 196 | 197 | ### 修复 198 | 199 | 1. 增加 license 设置 200 | 201 | ## [1.0.1] - 2022-10-05 202 | 203 | ### 修复 204 | 205 | 1. build 时 `await` 不能出现在 `main.ts` 206 | 207 | ## [1.0.0] - 2022-10-04 208 | 209 | ### 新增 210 | 211 | 1. 商店和市场的价格能正常被转换 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # steam-price-converter 2 | 3 | 一个 Tampermonkey 插件,能将 steam 商店和市场的的价格转换为某种货币展示。理论上支持所有区域。 4 | 5 | ## 使用 6 | 7 | ### 安装 8 | 9 | 脚本托管于 [Greasy Fork](https://greasyfork.org/zh-CN/scripts/452504-steam%E4%BB%B7%E6%A0%BC%E8%BD%AC%E6%8D%A2) 10 | 11 | 由于脚本使用了某种[特别的东西](https://github.com/BeyondDimension/SteamTools/issues/2497),Greasy Fork上的脚本不能直接导入steam++,~~需要使用 [releases](https://github.com/marioplus/steam-price-converter/releases) 中额外构建的版本。~~ 12 | 13 | ### 设置 14 | 15 | 默认将价格转化为人名币,如果需要转换其他货币展示可以在设置页面进行设置,所有设置都是实时生效的。 16 | 17 | ![](https://s3.bmp.ovh/imgs/2023/06/09/de3f84f9f3c2c1f0.jpg) 18 | ![](https://s3.bmp.ovh/imgs/2023/06/09/f500fb8f8517953d.jpg) 19 | 20 | ## 开发 21 | 22 | 根据[此处](https://github.com/lisonge/vite-plugin-monkey/issues/1)提示关闭 Tampermonkey 的 CSP 检测 23 | 24 | ```shell 25 | npm i 26 | npm run dev 27 | ``` 28 | 29 | ## 发布 30 | 31 | ```shell 32 | npm run build 33 | ``` 34 | 35 | ## 已知问题 36 | 37 | 1. 在未登录的状态下访问市场,可能或出现货币转换不正确 38 | 2. [市场首页](https://steamcommunity.com/market/)会出现转换不及时的情况 39 | 40 | ## 效果展示 41 | 42 | - 香港 43 | 44 | ![香港](https://s3.bmp.ovh/imgs/2022/10/05/6846453fc306362c.png) 45 | - 台湾 46 | 47 | ![台湾](https://s3.bmp.ovh/imgs/2022/10/05/14e9bc3760657721.png) 48 | - 新加坡 49 | 50 | ![新加坡](https://s3.bmp.ovh/imgs/2022/10/05/38ca54a79b9ed8bd.png) 51 | - 日本 52 | 53 | ![日本](https://s3.bmp.ovh/imgs/2022/10/05/aeab092828370c3f.png) 54 | - 韩国 55 | 56 | ![韩国](https://s3.bmp.ovh/imgs/2022/10/05/1db32a99e1176c58.png) 57 | - 美国 58 | 59 | ![美国](https://s3.bmp.ovh/imgs/2022/10/05/947c49e4d1b2d452.png) 60 | - 加拿大 61 | 62 | ![加拿大](https://s3.bmp.ovh/imgs/2022/10/05/a82b8d29e90f2662.png) 63 | - 泰国 64 | 65 | ![泰国](https://s3.bmp.ovh/imgs/2022/10/05/63f135d0f3bc3b67.png) 66 | - 英国 67 | 68 | ![英国](https://s3.bmp.ovh/imgs/2022/10/05/c837a7fb2a68e996.png) 69 | - 德国 70 | 71 | ![德国](https://s3.bmp.ovh/imgs/2022/10/05/7d72efc7e10479f4.png) 72 | - 俄罗斯 73 | 74 | ![俄罗斯](https://s3.bmp.ovh/imgs/2022/10/05/93718d86a3fa2635.png) 75 | - 印度 76 | 77 | ![印度](https://s3.bmp.ovh/imgs/2022/10/05/793a93213c2ed841.png) 78 | - 法国 79 | 80 | ![法国](https://s3.bmp.ovh/imgs/2022/10/05/c833b9d57c6b172f.png) 81 | - 阿根廷 82 | 83 | ![阿根廷](https://s3.bmp.ovh/imgs/2022/10/05/7f77627cdc0526e5.png) 84 | - 巴西 85 | 86 | ![巴西](https://s3.bmp.ovh/imgs/2022/10/05/29ffdade87a79a76.png) 87 | - 土耳其 88 | 89 | ![土耳其](https://s3.bmp.ovh/imgs/2022/10/05/0717bfc0df89dcd7.png) 90 | - 澳大利亚 91 | 92 | ![澳大利亚](https://s3.bmp.ovh/imgs/2022/10/05/6984db4cf8803438.png) 93 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steam-price-converter", 3 | "private": true, 4 | "version": "2.5.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "less": "^4.1.3", 13 | "mdui": "^2.1.1", 14 | "vue": "^3.2.47" 15 | }, 16 | "devDependencies": { 17 | "@vitejs/plugin-vue": "^5.0.4", 18 | "typescript": "~5.4.0", 19 | "vite": "^5.2.8", 20 | "vite-plugin-monkey": "^3.5.2", 21 | "vue-tsc": "^2.0.11" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 31 | 152 | 153 | 189 | -------------------------------------------------------------------------------- /src/RealMain.ts: -------------------------------------------------------------------------------- 1 | import {ConverterManager} from './converter/ConverterManager' 2 | import {RateManager} from './rate/RateManager' 3 | import {Logger} from './utils/Logger' 4 | import {Strings} from './utils/Strings' 5 | import {SpcContext} from './SpcContext' 6 | 7 | export async function main() { 8 | const context = SpcContext.getContext() 9 | 10 | if (context.currentCountyInfo.code === context.targetCountyInfo.code) { 11 | Logger.info(`${context.currentCountyInfo.name}无需转换`) 12 | return 13 | } 14 | 15 | // 获取汇率 16 | const rate = await RateManager.instance.getRate() 17 | if (!rate) { 18 | throw Error('获取汇率失败') 19 | } 20 | Logger.info(Strings.format(`汇率 %s -> %s:%s`, context.currentCountyInfo.currencyCode, context.targetCountyInfo.currencyCode, rate)) 21 | await convert(rate) 22 | } 23 | 24 | async function convert(rate: number) { 25 | const exchangerManager = ConverterManager.instance 26 | // 手动触发一次 27 | const elements = document.querySelectorAll(exchangerManager.getSelector()) 28 | exchangerManager.convert(elements, rate) 29 | 30 | // 注册观察者 31 | const selector = exchangerManager.getSelector() 32 | const priceObserver = new MutationObserver(mutations => { 33 | 34 | mutations.forEach(mutation => { 35 | const target = mutation.target 36 | let priceEls = target.querySelectorAll(selector) 37 | if (!priceEls || priceEls.length === 0) { 38 | return 39 | } 40 | exchangerManager.convert(priceEls, rate) 41 | }) 42 | }) 43 | priceObserver.observe(document.body, { 44 | childList: true, 45 | subtree: true, 46 | }) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/SpcContext.ts: -------------------------------------------------------------------------------- 1 | import {CountyInfo} from './county/CountyInfo' 2 | import {Setting} from './setting/Setting' 3 | import {unsafeWindow} from 'vite-plugin-monkey/dist/client' 4 | 5 | export class SpcContext { 6 | 7 | private readonly _setting: Setting 8 | private readonly _targetCountyInfo: CountyInfo 9 | private readonly _currentCountyInfo: CountyInfo 10 | 11 | constructor(setting: Setting, targetCountyInfo: CountyInfo, targetCountyCode: CountyInfo) { 12 | this._setting = setting 13 | this._targetCountyInfo = targetCountyInfo 14 | this._currentCountyInfo = targetCountyCode 15 | } 16 | 17 | public static getContext(): SpcContext { 18 | // @ts-ignore 19 | return unsafeWindow.spcContext 20 | } 21 | 22 | 23 | get setting(): Setting { 24 | return this._setting 25 | } 26 | 27 | get targetCountyInfo(): CountyInfo { 28 | return this._targetCountyInfo 29 | } 30 | 31 | get currentCountyInfo(): CountyInfo { 32 | return this._currentCountyInfo 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SpcManager.ts: -------------------------------------------------------------------------------- 1 | import {SettingManager} from './setting/SettingManager' 2 | import {RateManager} from './rate/RateManager' 3 | 4 | export class SpcManager { 5 | 6 | public static instance: SpcManager = new SpcManager() 7 | 8 | private constructor() { 9 | 10 | } 11 | 12 | public setCountyCode(code: string) { 13 | SettingManager.instance.setCountyCode(code) 14 | } 15 | 16 | public setCurrencySymbol(symbol: string) { 17 | SettingManager.instance.setCurrencySymbol(symbol) 18 | } 19 | 20 | public setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue: boolean) { 21 | SettingManager.instance.setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue) 22 | } 23 | 24 | public setRateCacheExpired(expired: number) { 25 | SettingManager.instance.setRateCacheExpired(expired) 26 | } 27 | 28 | public resetSetting() { 29 | SettingManager.instance.reset() 30 | } 31 | 32 | public clearCache() { 33 | RateManager.instance.clear() 34 | } 35 | 36 | public setUseCustomRate(isUseCustomRate: boolean) { 37 | SettingManager.instance.setUseCustomRate(isUseCustomRate) 38 | } 39 | 40 | public setCustomRate(customRate: number) { 41 | SettingManager.instance.setCustomRate(customRate) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/constant/Constant.ts: -------------------------------------------------------------------------------- 1 | const STORAGE_KEY_PREFIX = 'Storage:' 2 | /** 3 | * 汇率缓存 4 | */ 5 | export const STORAGE_KEY_RATE_CACHES = STORAGE_KEY_PREFIX + 'RateCache' 6 | /** 7 | * 设置 8 | */ 9 | export const STORAGE_KEY_SETTING = STORAGE_KEY_PREFIX + 'Setting' 10 | 11 | 12 | /** 13 | * 设置 14 | */ 15 | export const IM_MENU_SETTING = '设置' 16 | 17 | export class Attrs { 18 | static readonly STATUS_KEY: string = 'data-spc-status' 19 | static readonly STATUS_CONVERTED: string = 'converted' 20 | } 21 | -------------------------------------------------------------------------------- /src/converter/AbstractConverter.ts: -------------------------------------------------------------------------------- 1 | import {ElementSnap} from './ConverterManager' 2 | import {Attrs} from '../constant/Constant' 3 | 4 | export abstract class AbstractConverter { 5 | 6 | /** 7 | * 获取css选择器 8 | */ 9 | abstract getCssSelectors(): string[] 10 | 11 | /** 12 | * 匹配到的元素,是否匹配这个 exchanger 13 | * @param elementSnap 选择器选择到的元素快照 14 | */ 15 | match(elementSnap: ElementSnap): boolean { 16 | if (!elementSnap || !elementSnap.element) { 17 | return false 18 | } 19 | 20 | // 防止重复转换 21 | const status = elementSnap.element.getAttribute(Attrs.STATUS_KEY) 22 | const converted = Attrs.STATUS_CONVERTED === status 23 | if (converted) { 24 | return false 25 | } 26 | 27 | const content = elementSnap.textContext 28 | if (!content) { 29 | return false 30 | } 31 | 32 | // 文本中有数字 eg: ARS$ 399,53 33 | if (content.match(/\d/) === null) { 34 | return false 35 | } 36 | 37 | // 只有数字没有货币特征 38 | if (/^[,.\d\s]+$/.test(content)) { 39 | return false 40 | } 41 | 42 | // 该 exchanger 定义的选择器找到的 43 | const parent = elementSnap.element.parentElement 44 | if (!parent) { 45 | return false 46 | } 47 | for (const selector of this.getCssSelectors()) { 48 | const element = parent.querySelector(selector) 49 | if (element && element === elementSnap.element) { 50 | elementSnap.selector = selector 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | 57 | /** 58 | * 具体操作 59 | * @param elementSnap 选择器选择到的元素快照 60 | * @param rate 汇率 61 | * @return 处理结果 62 | */ 63 | abstract convert(elementSnap: ElementSnap, rate: number): boolean 64 | 65 | /** 66 | * 替换之后的操作 67 | * @param elementSnap 选择器选择到的元素快照 68 | */ 69 | // @ts-ignore 70 | afterConvert(elementSnap: ElementSnap): void { 71 | // 标记被转换 72 | elementSnap.element.setAttribute(Attrs.STATUS_KEY, Attrs.STATUS_CONVERTED) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/converter/ConvertUtils.ts: -------------------------------------------------------------------------------- 1 | import {SettingManager} from '../setting/SettingManager' 2 | import {Logger} from '../utils/Logger' 3 | 4 | /** 5 | * 提取获取价格 6 | * 1 1 7 | * 1.0 1.0 8 | * 1. 1 9 | * ARS$ 1.399,53 1.399,53 10 | * 1.399,53€ 1.399,53 11 | * @param content 包含货币和价格的字符串 12 | */ 13 | function parsePrice(content: string) { 14 | // 1.399,53 15 | let priceStr = content 16 | // 俄罗斯货币 两行的p 不一样 \u440 和 \u70 17 | .replace(/руб\./g, '') 18 | .replace(/pуб\./g, '') 19 | // 去掉空白符 20 | .replace(/\s/g, '') 21 | // 去掉货币符号 22 | .replace(/^[^0-9]+/, '') 23 | .replace(/[^0-9,.]+$/, '') 24 | // 去掉千分位 25 | .replace(/,(?=\d\d\d)/g, '') 26 | // 139953 27 | const numberStr = priceStr.replace(/\D/g, '') 28 | let price = Number.parseInt(numberStr) ?? 0 29 | // 小数点 1399.53 30 | if (priceStr.match(/\D/)) { 31 | price = price / 100.0 32 | } 33 | return price 34 | } 35 | 36 | /** 37 | * 根据汇率转换价格 38 | * @param price 价格 39 | * @param rate 汇率 40 | */ 41 | function convertPrice(price: number, rate: number) { 42 | return Number.parseFloat((price / rate).toFixed(2)) 43 | } 44 | 45 | /** 46 | * 具体操作,执行替换字符 47 | * @param originalContent 原始内容 48 | * @param rate 汇率 49 | * @return 替换后的内容 50 | * @return 处理结果 51 | */ 52 | export function convertPriceContent(originalContent: string, rate: number): string { 53 | const safeContent = originalContent.trim() 54 | .replaceAll(/\(.+$/g, '') 55 | .trim() 56 | const price = parsePrice(safeContent) 57 | const convertedPrice = convertPrice(price, rate) 58 | const setting = SettingManager.instance.setting 59 | 60 | let finalContent = setting.currencySymbolBeforeValue 61 | ? `${safeContent}(${setting.currencySymbol}${convertedPrice})` 62 | : `${safeContent}(${convertedPrice}${setting.currencySymbol})` 63 | 64 | const message = `转换前文本:${safeContent}; 提取到的价格:${price}; 转换后的价格:${convertedPrice}; 转换后文本:${finalContent}` 65 | Logger.debug(message) 66 | 67 | return finalContent 68 | } 69 | -------------------------------------------------------------------------------- /src/converter/ConverterManager.ts: -------------------------------------------------------------------------------- 1 | import {AbstractConverter} from './AbstractConverter' 2 | import {ElementConverter} from './ElementConverter' 3 | import {TextNodeConverter} from './TextNodeConverter' 4 | 5 | export type ElementSnap = { 6 | element: Element, 7 | readonly textContext: string | null, 8 | readonly classList: DOMTokenList, 9 | readonly attributes: NamedNodeMap, 10 | selector?: string 11 | } 12 | 13 | export class ConverterManager { 14 | 15 | static instance: ConverterManager = new ConverterManager() 16 | 17 | private converters: AbstractConverter[] 18 | 19 | private constructor() { 20 | this.converters = [ 21 | new ElementConverter(), 22 | new TextNodeConverter(), 23 | ] 24 | } 25 | 26 | getSelector(): string { 27 | return this.converters 28 | .map(exchanger => exchanger.getCssSelectors()) 29 | .flat(1) 30 | .join(', ') 31 | } 32 | 33 | convert(elements: NodeListOf, rate: number) { 34 | if (!elements) { 35 | return 36 | } 37 | elements.forEach(element => { 38 | const elementSnap: ElementSnap = { 39 | element, 40 | textContext: element.textContent, 41 | classList: element.classList, 42 | attributes: element.attributes 43 | } 44 | 45 | this.converters 46 | .filter(converter => converter.match(elementSnap)) 47 | .forEach(converter => { 48 | try { 49 | const exchanged = converter.convert(elementSnap, rate) 50 | // 转换后续操作 51 | if (exchanged) { 52 | converter.afterConvert(elementSnap) 53 | } 54 | } catch (e) { 55 | console.group('转换失败') 56 | console.error(e) 57 | console.error('转换失败请将下列内容反馈给开发者,右键 > 复制(copy) > 复制元素(copy element)') 58 | console.error('↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓') 59 | console.error(element) 60 | console.error('↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑') 61 | console.groupEnd() 62 | } 63 | }) 64 | }) 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/converter/ElementConverter.ts: -------------------------------------------------------------------------------- 1 | import {AbstractConverter} from './AbstractConverter' 2 | import {ElementSnap} from './ConverterManager' 3 | import {convertPriceContent} from './ConvertUtils' 4 | 5 | export class ElementConverter extends AbstractConverter { 6 | 7 | getCssSelectors(): string[] { 8 | const home = [ 9 | // 大图 10 | '.discount_prices > .discount_final_price' 11 | ] 12 | 13 | // 商店 分类 14 | const category = [ 15 | // 原价 16 | '.Wh0L8EnwsPV_8VAu8TOYr', 17 | '._3j4dI1yA7cRfCvK8h406OB', 18 | // 折扣价 19 | '._1EKGZBnKFWOr3RqVdnLMRN', 20 | '._3fFFsvII7Y2KXNLDk_krOW' 21 | ] 22 | 23 | const account = [ 24 | 'div.accountData.price a' 25 | ] 26 | 27 | const wishlist = [ 28 | // 右上角钱包 29 | 'div.Hxi-pnf9Xlw- > div._79DIT7RUQ5g-', 30 | // 当前价格 31 | 'div.ME2eMO7C1Tk- > div.DOnsaVcV0Is-', 32 | // 原价 33 | 'div.ME2eMO7C1Tk- > div.ywNldZ-YzEE-' 34 | ] 35 | 36 | const inventory = [ 37 | '#iteminfo1_item_market_actions div[id^="market_item_action_buyback_at_price_"]' 38 | ] 39 | 40 | // 购物车 41 | const cart = [ 42 | // 原价 43 | '.Panel.Focusable ._3-o3G9jt3lqcvbRXt8epsn.StoreOriginalPrice', 44 | // 折扣价 45 | '.Panel.Focusable .pk-LoKoNmmPK4GBiC9DR8', 46 | // 总额 47 | '._2WLaY5TxjBGVyuWe_6KS3N', 48 | ] 49 | // 购物车复核 50 | const cartCheckout = [ 51 | // 列表 52 | '#checkout_review_cart_area .checkout_review_item_price > .price', 53 | // 小计 54 | '#review_subtotal_value.price', 55 | // 合计 56 | '#review_total_value.price', 57 | ] 58 | 59 | const selectors = [ 60 | // 商店 61 | // 首页 62 | '.discount_original_price', 63 | '.discount_final_price', 64 | '.col.search_price.responsive_secondrow strike', 65 | // 头像旁边 66 | '#header_wallet_balance > span.tooltip', 67 | // 愿望单总价值 68 | '.esi-wishlist-stat > .num', 69 | // 新版卡片 70 | '.salepreviewwidgets_StoreOriginalPrice_1EKGZ', 71 | '.salepreviewwidgets_StoreSalePriceBox_Wh0L8', 72 | // 分类查看游戏 73 | '.contenthubshared_OriginalPrice_3hBh3', 74 | '.contenthubshared_FinalPrice_F_tGv', 75 | '.salepreviewwidgets_StoreSalePriceBox_Wh0L8:not(.salepreviewwidgets_StoreSalePrepurchaseLabel_Wxeyn)', 76 | 77 | // 市场 78 | // 总余额 79 | '#marketWalletBalanceAmount', 80 | // 列表 81 | 'span.normal_price[data-price]', 82 | 'span.sale_price', 83 | // 求购、求售统计 84 | '.market_commodity_orders_header_promote:nth-child(even)', 85 | // 求购、求售列表 86 | '.market_commodity_orders_table td:nth-child(odd)', 87 | // 详情列表 88 | '.market_table_value > span', 89 | '.jqplot-highlighter-tooltip', 90 | 91 | // 消费记录 92 | 'tr.wallet_table_row > td.wht_total', 93 | 'tr.wallet_table_row > td.wht_wallet_change.wallet_column', 94 | 'tr.wallet_table_row > td.wht_wallet_balance.wallet_column', 95 | // 捆绑包 96 | '.package_totals_row > .price:not(.bundle_discount)', 97 | '#package_savings_bar > .savings.bundle_savings', 98 | // 低于xxx 分类标题 99 | '.home_page_content_title a.btn_small_tall > span', 100 | ] 101 | selectors.push(...home) 102 | selectors.push(...category) 103 | selectors.push(...account) 104 | selectors.push(...wishlist) 105 | selectors.push(...inventory) 106 | selectors.push(...cart) 107 | selectors.push(...cartCheckout) 108 | return selectors 109 | } 110 | 111 | convert(elementSnap: ElementSnap, rate: number): boolean { 112 | // 提取货币代码和货币量 113 | // @ts-ignore match 方法已经检查过了,不可能为 null 114 | elementSnap.element.textContent = convertPriceContent(elementSnap.textContext, rate) 115 | return true 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/converter/TextNodeConverter.ts: -------------------------------------------------------------------------------- 1 | import {AbstractConverter} from './AbstractConverter' 2 | import {ElementSnap} from './ConverterManager' 3 | import {convertPriceContent} from './ConvertUtils' 4 | 5 | type parseTextNodeFn = (element: Element) => ChildNode 6 | 7 | export class TextNodeConverter extends AbstractConverter { 8 | 9 | 10 | // @ts-ignore 11 | parseFirstChildTextNodeFn: parseTextNodeFn = el => el.firstChild 12 | 13 | // 购物车 14 | cart = new Map([ 15 | // 卡牌获取进度 16 | ['.Panel.Focusable ._18eO4-XadW5jmTpgdATkSz', [el => el.childNodes[1]], 17 | ] 18 | ]) 19 | 20 | // @ts-ignore 21 | targets: Map = new Map([ 22 | ['.col.search_price.responsive_secondrow', 23 | [ 24 | // @ts-ignore 25 | el => el.firstChild.nextSibling.nextSibling.nextSibling, 26 | this.parseFirstChildTextNodeFn, 27 | ], 28 | ], 29 | ['#header_wallet_balance', [this.parseFirstChildTextNodeFn]], 30 | // iframe 31 | ['.game_purchase_price.price', [this.parseFirstChildTextNodeFn]], 32 | // 低于xxx 分类标题 33 | ['.home_page_content_title', [this.parseFirstChildTextNodeFn]], 34 | // dlc 中没有折扣 35 | ['.game_area_dlc_row > .game_area_dlc_price', [ 36 | el => el, 37 | this.parseFirstChildTextNodeFn 38 | ]], 39 | ...this.cart 40 | ]) 41 | 42 | getCssSelectors(): string[] { 43 | return [...this.targets.keys()] 44 | } 45 | 46 | convert(elementSnap: ElementSnap, rate: number): boolean { 47 | // 找到对应的元素 48 | // @ts-ignore 49 | const selector: string = elementSnap.selector 50 | this.targets.get(selector) 51 | 52 | // 拿到对应的 textNode 53 | const parseNodeFns: parseTextNodeFn[] = this.targets.get(selector) || [] 54 | if (!parseNodeFns) { 55 | return false 56 | } 57 | 58 | const textNode = this.safeParseNode(selector, elementSnap.element, parseNodeFns) 59 | if (!textNode) { 60 | return false 61 | } 62 | 63 | // 转换 64 | const content = textNode.nodeValue 65 | if (!content || content.trim().length === 0) { 66 | return false 67 | } 68 | textNode.nodeValue = convertPriceContent(content, rate) 69 | return true 70 | } 71 | 72 | safeParseNode(selector: string, el: Element, fns: parseTextNodeFn[]): ChildNode | null { 73 | for (let fn of fns) { 74 | try { 75 | const node = fn(el) 76 | if (node.nodeName === '#text' && node.nodeValue && node.nodeValue.length > 0) { 77 | return node 78 | } 79 | } catch (e) { 80 | console.debug('获取文本节点失败,但不确定该节点是否一定会出现。selector:' + selector) 81 | } 82 | } 83 | return null 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/county/CookieCountyInfoGetter.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {GM_cookie} from '$' 3 | 4 | /** 5 | * 从 cookie 中获取区域代码 6 | */ 7 | export class CookieCountyInfoGetter implements ICountyInfoGetter { 8 | name(): string { 9 | return 'cookie' 10 | } 11 | 12 | match(): boolean { 13 | return true 14 | } 15 | 16 | async getCountyCode(): Promise { 17 | return new Promise(async (resolve, reject) => { 18 | const cookies = await GM_cookie.list({name: 'steamCountry'}) 19 | if (cookies && cookies.length > 0) { 20 | const match = cookies[0].value.match(/^[a-zA-Z][a-zA-Z]/) 21 | if (match) { 22 | resolve(match[0]) 23 | } 24 | } 25 | reject() 26 | }) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/county/CountyCodeGetterManager.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {CookieCountyInfoGetter} from './CookieCountyInfoGetter' 3 | import {RequestStorePageCountyCodeGetter} from './RequestStorePageCountyCodeGetter' 4 | import {StorePageCountyCodeGetter} from './StorePageCountyCodeGetter' 5 | import {MarketPageCountyCodeGetter} from './MarketPageCountyCodeGetter' 6 | import {Logger} from '../utils/Logger' 7 | import {UserConfigCountyInfoGetter} from "./UserConfigCountyInfoGetter"; 8 | 9 | 10 | export class CountyCodeGetterManager { 11 | 12 | static readonly instance: CountyCodeGetterManager = new CountyCodeGetterManager() 13 | 14 | private readonly getters: ICountyInfoGetter[] 15 | 16 | private constructor() { 17 | 18 | this.getters = [ 19 | new UserConfigCountyInfoGetter(), 20 | new StorePageCountyCodeGetter(), 21 | new MarketPageCountyCodeGetter(), 22 | new RequestStorePageCountyCodeGetter(), 23 | new CookieCountyInfoGetter(), 24 | ] 25 | } 26 | 27 | match(): boolean { 28 | return true 29 | } 30 | 31 | async getCountyCode(): Promise { 32 | Logger.info('尝试获取区域代码') 33 | 34 | for (let getter of this.getters) { 35 | if (!getter.match()) { 36 | continue 37 | } 38 | 39 | Logger.debug(`尝试通过[${getter.name()}]获取区域代码`) 40 | try { 41 | const countyCode = await getter.getCountyCode() 42 | Logger.info(`通过[${getter.name()}]获取区域代码成功`) 43 | return countyCode 44 | } catch (e) { 45 | Logger.error(`通过[${getter.name()}]获取区域代码失败`) 46 | } 47 | } 48 | throw new Error('所有获取区域代码策略都获取失败') 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/county/CountyInfo.ts: -------------------------------------------------------------------------------- 1 | // https://zh.wikipedia.org/wiki/ISO_4217 2 | // https://zh.wikipedia.org/wiki/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81#%E6%AD%A3%E5%BC%8F%E5%88%86%E9%85%8D%E4%BB%A3%E7%A0%81 3 | import countyObjs from './county-data.json' 4 | import {Jsons} from '../utils/Jsons' 5 | 6 | export interface CountyInfo { 7 | code: string 8 | name: string 9 | nameEn: string 10 | currencyCode: string 11 | } 12 | 13 | export const countyInfos = Jsons.readJson(countyObjs, Array) 14 | export const countyCode2Info = new Map(countyInfos.map(v => [v.code, v])) 15 | -------------------------------------------------------------------------------- /src/county/ICountyInfoGetter.ts: -------------------------------------------------------------------------------- 1 | export interface ICountyInfoGetter { 2 | 3 | /** 4 | * 是否匹配 5 | */ 6 | match(): boolean 7 | 8 | /** 9 | * 名称 10 | */ 11 | name(): string 12 | 13 | /** 14 | * 获取区域代码 15 | */ 16 | getCountyCode(): Promise 17 | } 18 | -------------------------------------------------------------------------------- /src/county/MarketPageCountyCodeGetter.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {Logger} from '../utils/Logger' 3 | 4 | /** 5 | * 市场页面获取区域代码 6 | */ 7 | export class MarketPageCountyCodeGetter implements ICountyInfoGetter { 8 | name(): string { 9 | return '市场页面' 10 | } 11 | 12 | 13 | match(): boolean { 14 | return window.location.href.includes('steamcommunity.com') 15 | } 16 | 17 | getCountyCode(): Promise { 18 | return new Promise((resolve, reject) => { 19 | try { 20 | // @ts-ignore 21 | const code: string | undefined = g_strCountryCode 22 | if (code) return resolve(code) 23 | } catch (err: any) { 24 | Logger.error(err) 25 | } 26 | reject() 27 | }) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/county/RequestStorePageCountyCodeGetter.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {Logger} from '../utils/Logger' 3 | import {Http} from '../utils/Http' 4 | 5 | /** 6 | * 请求商店页面获取区域代码 7 | */ 8 | export class RequestStorePageCountyCodeGetter implements ICountyInfoGetter { 9 | name(): string { 10 | return '请求商店页面' 11 | } 12 | 13 | 14 | match(): boolean { 15 | return !window.location.href.includes('store.steampowered.com') 16 | } 17 | 18 | async getCountyCode(): Promise { 19 | return new Promise(async (resolve, reject) => { 20 | try { 21 | const storeHtml = await Http.get(String, 'https://store.steampowered.com/') 22 | const match = storeHtml.match(/(?<=GDynamicStore.Init\(.+')[A-Z][A-Z](?=',)/) 23 | if (match) { 24 | return resolve(match[0]) 25 | } 26 | } catch (err: any) { 27 | Logger.error(err) 28 | } 29 | reject() 30 | }) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/county/StorePageCountyCodeGetter.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {Logger} from '../utils/Logger' 3 | 4 | /** 5 | * 当前页面获取区域代码 6 | */ 7 | export class StorePageCountyCodeGetter implements ICountyInfoGetter { 8 | name(): string { 9 | return '商店页面' 10 | } 11 | 12 | 13 | match(): boolean { 14 | return window.location.href.includes('store.steampowered.com') 15 | } 16 | 17 | getCountyCode(): Promise { 18 | return new Promise((resolve, reject) => { 19 | try { 20 | // @ts-ignore 21 | let countyCode = GStoreItemData?.rgNavParams?.__page_default_obj?.countrycode 22 | if (countyCode) { 23 | resolve(countyCode) 24 | } 25 | } catch (e: any) { 26 | Logger.warn('读取商店页面区域代码变量失败: ' + e.message) 27 | } 28 | 29 | document.querySelectorAll('script').forEach(scriptEl => { 30 | const scriptInnerText = scriptEl.innerText 31 | if (scriptInnerText.includes('$J( InitMiniprofileHovers );') || scriptInnerText.includes(`$J( InitMiniprofileHovers( 'https%3A%2F%2Fstore.steampowered.com%2F' ) );`)) { 32 | const matcher = /(?<=')[A-Z]{2}(?!=')/g 33 | const match = scriptInnerText.match(matcher) 34 | if (match) { 35 | const countyCode = match.toString() 36 | resolve(countyCode) 37 | } 38 | } 39 | }) 40 | reject() 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/county/UserConfigCountyInfoGetter.ts: -------------------------------------------------------------------------------- 1 | import {ICountyInfoGetter} from './ICountyInfoGetter' 2 | import {Http} from '../utils/Http' 3 | import {unsafeWindow} from 'vite-plugin-monkey/dist/client' 4 | 5 | export class UserConfigCountyInfoGetter implements ICountyInfoGetter { 6 | 7 | async getCountyCode(): Promise { 8 | return new Promise(async (resolve, reject) => { 9 | 10 | // @ts-ignore 11 | const code = unsafeWindow.userConfig 12 | // @ts-ignore 13 | ? unsafeWindow.userConfig?.country_code 14 | : await this.getCountyCodeForDev() 15 | if (code) { 16 | resolve(code) 17 | } else { 18 | reject() 19 | } 20 | }) 21 | } 22 | 23 | match(): boolean { 24 | return true 25 | } 26 | 27 | name(): string { 28 | return 'window.UserConfig' 29 | } 30 | 31 | async getCountyCodeForDev() { 32 | // @ts-ignore 33 | const html = await Http.get(String, window.location.href) 34 | const match = html.match(/,"country_code":"([A-Z]{2})"/) 35 | if (match) { 36 | return match[1] 37 | } 38 | return undefined 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/county/county-data-all.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"code":"AL","name":"阿尔巴尼亚","nameEn":"Albania","currencyCode":"ALL"}, 3 | {"code":"DZ","name":"阿尔及利亚","nameEn":"Algeria","currencyCode":"DZD"}, 4 | {"code":"AF","name":"阿富汗","nameEn":"Afghanistan","currencyCode":"AFN"}, 5 | {"code":"AR","name":"阿根廷","nameEn":"Argentina","currencyCode":"USD"}, 6 | {"code":"AE","name":"阿联酋","nameEn":"United Arab Emirates","currencyCode":"AED"}, 7 | {"code":"AW","name":"阿鲁巴","nameEn":"Aruba","currencyCode":"AWG"}, 8 | {"code":"OM","name":"阿曼","nameEn":"Oman","currencyCode":"OMR"}, 9 | {"code":"AZ","name":"阿塞拜疆","nameEn":"Azerbaijan","currencyCode":"USD"}, 10 | {"code":"EG","name":"埃及","nameEn":"Egypt","currencyCode":"EGP"}, 11 | {"code":"ET","name":"埃塞俄比亚","nameEn":"Ethiopia","currencyCode":"ETB"}, 12 | {"code":"IE","name":"爱尔兰","nameEn":"Ireland","currencyCode":"EUR"}, 13 | {"code":"EE","name":"爱沙尼亚","nameEn":"Estonia","currencyCode":"EUR"}, 14 | {"code":"AD","name":"安道尔","nameEn":"Andorra","currencyCode":"EUR"}, 15 | {"code":"AO","name":"安哥拉","nameEn":"Angola","currencyCode":"AOA"}, 16 | {"code":"AI","name":"安圭拉","nameEn":"Anguilla","currencyCode":"XCD"}, 17 | {"code":"AG","name":"安提瓜和巴布达","nameEn":"Antigua and Barbuda","currencyCode":"XCD"}, 18 | {"code":"AT","name":"奥地利","nameEn":"Austria","currencyCode":"EUR"}, 19 | {"code":"AU","name":"澳大利亚","nameEn":"Australia","currencyCode":"AUD"}, 20 | {"code":"MO","name":"澳门","nameEn":"Macao","currencyCode":"MOP"}, 21 | {"code":"BB","name":"巴巴多斯","nameEn":"Barbados","currencyCode":"BBD"}, 22 | {"code":"PG","name":"巴布亚新几内亚","nameEn":"Papua New Guinea","currencyCode":"PGK"}, 23 | {"code":"BS","name":"巴哈马","nameEn":"Bahamas","currencyCode":"BSD"}, 24 | {"code":"PK","name":"巴基斯坦","nameEn":"Pakistan","currencyCode":"USD"}, 25 | {"code":"PY","name":"巴拉圭","nameEn":"Paraguay","currencyCode":"PYG"}, 26 | {"code":"PS","name":"巴勒斯坦","nameEn":"Palestine, State of","currencyCode":"ILS"}, 27 | {"code":"BH","name":"巴林","nameEn":"Bahrain","currencyCode":"USD"}, 28 | {"code":"PA","name":"巴拿马","nameEn":"Panama","currencyCode":"PAB"}, 29 | {"code":"BR","name":"巴西","nameEn":"Brazil","currencyCode":"BRL"}, 30 | {"code":"BY","name":"白俄罗斯","nameEn":"Belarus","currencyCode":"BYN"}, 31 | {"code":"BM","name":"百慕大","nameEn":"Bermuda","currencyCode":"BMD"}, 32 | {"code":"BG","name":"保加利亚","nameEn":"Bulgaria","currencyCode":"BGN"}, 33 | {"code":"MP","name":"北马里亚纳群岛","nameEn":"Northern Mariana Islands","currencyCode":"USD"}, 34 | {"code":"BJ","name":"贝宁","nameEn":"Benin","currencyCode":"XOF"}, 35 | {"code":"BE","name":"比利时","nameEn":"Belgium","currencyCode":"EUR"}, 36 | {"code":"IS","name":"冰岛","nameEn":"Iceland","currencyCode":"ISK"}, 37 | {"code":"PR","name":"波多黎各","nameEn":"Puerto Rico","currencyCode":"USD"}, 38 | {"code":"BA","name":"波黑","nameEn":"Bosnia and Herzegovina","currencyCode":"BAM"}, 39 | {"code":"PL","name":"波兰","nameEn":"Poland","currencyCode":"PLN"}, 40 | {"code":"BO","name":"玻利维亚","nameEn":"Bolivia, Plurinational State of","currencyCode":"BOB"}, 41 | {"code":"BZ","name":"伯利兹","nameEn":"Belize","currencyCode":"BZD"}, 42 | {"code":"BW","name":"博茨瓦纳","nameEn":"Botswana","currencyCode":"BWP"}, 43 | {"code":"BT","name":"不丹","nameEn":"Bhutan","currencyCode":"BTN"}, 44 | {"code":"BF","name":"布基纳法索","nameEn":"Burkina Faso","currencyCode":"XOF"}, 45 | {"code":"BI","name":"布隆迪","nameEn":"Burundi","currencyCode":"BIF"}, 46 | {"code":"BV","name":"布韦岛","nameEn":"Bouvet Island","currencyCode":"NOK"}, 47 | {"code":"KP","name":"朝鲜","nameEn":"Korea, Democratic People's Republic of","currencyCode":"KPW"}, 48 | {"code":"GQ","name":"赤道几内亚","nameEn":"Equatorial Guinea","currencyCode":"XAF"}, 49 | {"code":"DK","name":"丹麦","nameEn":"Denmark","currencyCode":"DKK"}, 50 | {"code":"DE","name":"德国","nameEn":"Germany","currencyCode":"EUR"}, 51 | {"code":"TL","name":"东帝汶","nameEn":"Timor-Leste","currencyCode":"USD"}, 52 | {"code":"TG","name":"多哥","nameEn":"Togo","currencyCode":"XOF"}, 53 | {"code":"DO","name":"多米尼加","nameEn":"Dominican Republic","currencyCode":"DOP"}, 54 | {"code":"DM","name":"多米尼克","nameEn":"Dominica","currencyCode":"XCD"}, 55 | {"code":"RU","name":"俄罗斯","nameEn":"Russian Federation","currencyCode":"RUB"}, 56 | {"code":"EC","name":"厄瓜多尔","nameEn":"Ecuador","currencyCode":"USD"}, 57 | {"code":"ER","name":"厄立特里亚","nameEn":"Eritrea","currencyCode":"ERN"}, 58 | {"code":"FR","name":"法国","nameEn":"France","currencyCode":"EUR"}, 59 | {"code":"FO","name":"法罗群岛","nameEn":"Faroe Islands","currencyCode":"DKK"}, 60 | {"code":"PF","name":"法属波利尼西亚","nameEn":"French Polynesia","currencyCode":"XPF"}, 61 | {"code":"VA","name":"梵蒂冈","nameEn":"Holy See","currencyCode":"EUR"}, 62 | {"code":"PH","name":"菲律宾","nameEn":"Philippines","currencyCode":"PHP"}, 63 | {"code":"FJ","name":"斐济","nameEn":"Fiji","currencyCode":"FJD"}, 64 | {"code":"FI","name":"芬兰","nameEn":"Finland","currencyCode":"EUR"}, 65 | {"code":"CV","name":"佛得角","nameEn":"Cabo Verde","currencyCode":"CVE"}, 66 | {"code":"FK","name":"福克兰群岛","nameEn":"Falkland Islands (Malvinas)","currencyCode":"FKP"}, 67 | {"code":"GM","name":"冈比亚","nameEn":"Gambia","currencyCode":"GMD"}, 68 | {"code":"CG","name":"刚果共和国","nameEn":"Congo","currencyCode":"XAF"}, 69 | {"code":"CD","name":"刚果民主共和国","nameEn":"Congo, the Democratic Republic of the","currencyCode":"CDF"}, 70 | {"code":"CO","name":"哥伦比亚","nameEn":"Colombia","currencyCode":"COP"}, 71 | {"code":"CR","name":"哥斯达黎加","nameEn":"Costa Rica","currencyCode":"CRC"}, 72 | {"code":"GD","name":"格林纳达","nameEn":"Grenada","currencyCode":"XCD"}, 73 | {"code":"GL","name":"格陵兰","nameEn":"Greenland","currencyCode":"DKK"}, 74 | {"code":"GE","name":"格鲁吉亚","nameEn":"Georgia","currencyCode":"GEL"}, 75 | {"code":"GG","name":"根西","nameEn":"Guernsey","currencyCode":"GBP"}, 76 | {"code":"CU","name":"古巴","nameEn":"Cuba","currencyCode":"CUP"}, 77 | {"code":"GP","name":"瓜德罗普","nameEn":"Guadeloupe","currencyCode":"EUR"}, 78 | {"code":"GU","name":"关岛","nameEn":"Guam","currencyCode":"USD"}, 79 | {"code":"GY","name":"圭亚那","nameEn":"Guyana","currencyCode":"GYD"}, 80 | {"code":"KZ","name":"哈萨克斯坦","nameEn":"Kazakhstan","currencyCode":"KZT"}, 81 | {"code":"HT","name":"海地","nameEn":"Haiti","currencyCode":"HTG"}, 82 | {"code":"KR","name":"韩国","nameEn":"Korea, Republic of","currencyCode":"KRW"}, 83 | {"code":"NL","name":"荷兰","nameEn":"Netherlands","currencyCode":"EUR"}, 84 | {"code":"SX","name":"荷属圣马丁","nameEn":"Sint Maarten (Dutch part)","currencyCode":"ANG"}, 85 | {"code":"HM","name":"赫德岛和麦克唐纳群岛","nameEn":"Heard Island and McDonald Islands","currencyCode":"AUD"}, 86 | {"code":"ME","name":"黑山","nameEn":"Montenegro","currencyCode":"EUR"}, 87 | {"code":"HN","name":"洪都拉斯","nameEn":"Honduras","currencyCode":"HNL"}, 88 | {"code":"KI","name":"基里巴斯","nameEn":"Kiribati","currencyCode":"AUD"}, 89 | {"code":"DJ","name":"吉布提","nameEn":"Djibouti","currencyCode":"DJF"}, 90 | {"code":"KG","name":"吉尔吉斯斯坦","nameEn":"Kyrgyzstan","currencyCode":"KGS"}, 91 | {"code":"GN","name":"几内亚","nameEn":"Guinea","currencyCode":"GNF"}, 92 | {"code":"GW","name":"几内亚比绍","nameEn":"Guinea-Bissau","currencyCode":"XOF"}, 93 | {"code":"CA","name":"加拿大","nameEn":"Canada","currencyCode":"CAD"}, 94 | {"code":"GH","name":"加纳","nameEn":"Ghana","currencyCode":"GHS"}, 95 | {"code":"GA","name":"加蓬","nameEn":"Gabon","currencyCode":"XAF"}, 96 | {"code":"KH","name":"柬埔寨","nameEn":"Cambodia","currencyCode":"KHR"}, 97 | {"code":"CZ","name":"捷克","nameEn":"Czechia","currencyCode":"CZK"}, 98 | {"code":"ZW","name":"津巴布韦","nameEn":"Zimbabwe","currencyCode":"ZWL"}, 99 | {"code":"CM","name":"喀麦隆","nameEn":"Cameroon","currencyCode":"XAF"}, 100 | {"code":"QA","name":"卡塔尔","nameEn":"Qatar","currencyCode":"QAR"}, 101 | {"code":"KY","name":"开曼群岛","nameEn":"Cayman Islands","currencyCode":"KYD"}, 102 | {"code":"CC","name":"科科斯(基林)群岛","nameEn":"Cocos (Keeling) Islands","currencyCode":"AUD"}, 103 | {"code":"KM","name":"科摩罗","nameEn":"Comoros","currencyCode":"KMF"}, 104 | {"code":"CI","name":"科特迪瓦","nameEn":"Côte d'Ivoire","currencyCode":"XOF"}, 105 | {"code":"KW","name":"科威特","nameEn":"Kuwait","currencyCode":"KWD"}, 106 | {"code":"HR","name":"克罗地亚","nameEn":"Croatia","currencyCode":"HRK"}, 107 | {"code":"KE","name":"肯尼亚","nameEn":"Kenya","currencyCode":"KES"}, 108 | {"code":"CK","name":"库克群岛","nameEn":"Cook Islands","currencyCode":"NZD"}, 109 | {"code":"CW","name":"库拉索","nameEn":"Curaçao","currencyCode":"ANG"}, 110 | {"code":"LV","name":"拉脱维亚","nameEn":"Latvia","currencyCode":"EUR"}, 111 | {"code":"LS","name":"莱索托","nameEn":"Lesotho","currencyCode":"LSL"}, 112 | {"code":"LA","name":"老挝","nameEn":"Lao People's Democratic Republic","currencyCode":"LAK"}, 113 | {"code":"LB","name":"黎巴嫩","nameEn":"Lebanon","currencyCode":"LBP"}, 114 | {"code":"LT","name":"立陶宛","nameEn":"Lithuania","currencyCode":"EUR"}, 115 | {"code":"LR","name":"利比里亚","nameEn":"Liberia","currencyCode":"LRD"}, 116 | {"code":"LY","name":"利比亚","nameEn":"Libya","currencyCode":"LYD"}, 117 | {"code":"LI","name":"列支敦士登","nameEn":"Liechtenstein","currencyCode":"CHF"}, 118 | {"code":"RE","name":"留尼汪","nameEn":"Réunion","currencyCode":"EUR"}, 119 | {"code":"LU","name":"卢森堡","nameEn":"Luxembourg","currencyCode":"EUR"}, 120 | {"code":"RW","name":"卢旺达","nameEn":"Rwanda","currencyCode":"RWF"}, 121 | {"code":"RO","name":"罗马尼亚","nameEn":"Romania","currencyCode":"RON"}, 122 | {"code":"MG","name":"马达加斯加","nameEn":"Madagascar","currencyCode":"MGA"}, 123 | {"code":"IM","name":"马恩岛","nameEn":"Isle of Man","currencyCode":"GBP"}, 124 | {"code":"MV","name":"马尔代夫","nameEn":"Maldives","currencyCode":"MVR"}, 125 | {"code":"MT","name":"马耳他","nameEn":"Malta","currencyCode":"EUR"}, 126 | {"code":"MW","name":"马拉维","nameEn":"Malawi","currencyCode":"MWK"}, 127 | {"code":"MY","name":"马来西亚","nameEn":"Malaysia","currencyCode":"MYR"}, 128 | {"code":"ML","name":"马里","nameEn":"Mali","currencyCode":"XOF"}, 129 | {"code":"MH","name":"马绍尔群岛","nameEn":"Marshall Islands","currencyCode":"USD"}, 130 | {"code":"MQ","name":"马提尼克","nameEn":"Martinique","currencyCode":"EUR"}, 131 | {"code":"YT","name":"马约特","nameEn":"Mayotte","currencyCode":"EUR"}, 132 | {"code":"MU","name":"毛里求斯","nameEn":"Mauritius","currencyCode":"MUR"}, 133 | {"code":"MR","name":"毛里塔尼亚","nameEn":"Mauritania","currencyCode":"MRU"}, 134 | {"code":"US","name":"美国","nameEn":"United States of America","currencyCode":"USD"}, 135 | {"code":"AS","name":"美属萨摩亚","nameEn":"American Samoa","currencyCode":"USD"}, 136 | {"code":"VI","name":"美属维尔京群岛","nameEn":"Virgin Islands, U.S.","currencyCode":"USD"}, 137 | {"code":"MN","name":"蒙古","nameEn":"Mongolia","currencyCode":"MNT"}, 138 | {"code":"MS","name":"蒙特塞拉特","nameEn":"Montserrat","currencyCode":"XCD"}, 139 | {"code":"BD","name":"孟加拉国","nameEn":"Bangladesh","currencyCode":"BDT"}, 140 | {"code":"PE","name":"秘鲁","nameEn":"Peru","currencyCode":"PEN"}, 141 | {"code":"FM","name":"密克罗尼西亚联邦","nameEn":"Micronesia, Federated States of","currencyCode":"USD"}, 142 | {"code":"MM","name":"缅甸","nameEn":"Myanmar","currencyCode":"MMK"}, 143 | {"code":"MD","name":"摩尔多瓦","nameEn":"Moldova, Republic of","currencyCode":"MDL"}, 144 | {"code":"MA","name":"摩洛哥","nameEn":"Morocco","currencyCode":"MAD"}, 145 | {"code":"MC","name":"摩纳哥","nameEn":"Monaco","currencyCode":"EUR"}, 146 | {"code":"MZ","name":"莫桑比克","nameEn":"Mozambique","currencyCode":"MZN"}, 147 | {"code":"MX","name":"墨西哥","nameEn":"Mexico","currencyCode":"MXN"}, 148 | {"code":"NA","name":"纳米比亚","nameEn":"Namibia","currencyCode":"NAD"}, 149 | {"code":"ZA","name":"南非","nameEn":"South Africa","currencyCode":"USD"}, 150 | {"code":"GS","name":"南乔治亚和南桑威奇群岛","nameEn":"South Georgia and the South Sandwich Islands","currencyCode":"GBP"}, 151 | {"code":"SS","name":"南苏丹","nameEn":"South Sudan","currencyCode":"SSP"}, 152 | {"code":"NR","name":"瑙鲁","nameEn":"Nauru","currencyCode":"AUD"}, 153 | {"code":"NI","name":"尼加拉瓜","nameEn":"Nicaragua","currencyCode":"NIO"}, 154 | {"code":"NP","name":"尼泊尔","nameEn":"Nepal","currencyCode":"NPR"}, 155 | {"code":"NG","name":"尼日利亚","nameEn":"Nigeria","currencyCode":"NGN"}, 156 | {"code":"NU","name":"纽埃","nameEn":"Niue","currencyCode":"NZD"}, 157 | {"code":"NO","name":"挪威","nameEn":"Norway","currencyCode":"NOK"}, 158 | {"code":"NF","name":"诺福克岛","nameEn":"Norfolk Island","currencyCode":"AUD"}, 159 | {"code":"PW","name":"帕劳","nameEn":"Palau","currencyCode":"USD"}, 160 | {"code":"PN","name":"皮特凯恩群岛","nameEn":"Pitcairn","currencyCode":"NZD"}, 161 | {"code":"PT","name":"葡萄牙","nameEn":"Portugal","currencyCode":"EUR"}, 162 | {"code":"JP","name":"日本","nameEn":"Japan","currencyCode":"JPY"}, 163 | {"code":"SE","name":"瑞典","nameEn":"Sweden","currencyCode":"SEK"}, 164 | {"code":"CH","name":"瑞士","nameEn":"Switzerland","currencyCode":"CHF"}, 165 | {"code":"SV","name":"萨尔瓦多","nameEn":"El Salvador","currencyCode":"USD"}, 166 | {"code":"WS","name":"萨摩亚","nameEn":"Samoa","currencyCode":"WST"}, 167 | {"code":"RS","name":"塞尔维亚","nameEn":"Serbia","currencyCode":"RSD"}, 168 | {"code":"SL","name":"塞拉利昂","nameEn":"Sierra Leone","currencyCode":"SLL"}, 169 | {"code":"SN","name":"塞内加尔","nameEn":"Senegal","currencyCode":"XOF"}, 170 | {"code":"CY","name":"塞浦路斯","nameEn":"Cyprus","currencyCode":"EUR"}, 171 | {"code":"SC","name":"塞舌尔","nameEn":"Seychelles","currencyCode":"SCR"}, 172 | {"code":"SA","name":"沙特阿拉伯","nameEn":"Saudi Arabia","currencyCode":"SAR"}, 173 | {"code":"BL","name":"圣巴泰勒米","nameEn":"Saint Barthélemy","currencyCode":"EUR"}, 174 | {"code":"CX","name":"圣诞岛","nameEn":"Christmas Island","currencyCode":"AUD"}, 175 | {"code":"ST","name":"圣多美和普林西比","nameEn":"Sao Tome and Principe","currencyCode":"STN"}, 176 | {"code":"SH","name":"圣赫勒拿、阿森松和特里斯坦-达库尼亚","nameEn":"Saint Helena, Ascension and Tristan da Cunha","currencyCode":"SHP"}, 177 | {"code":"KN","name":"圣基茨和尼维斯","nameEn":"Saint Kitts and Nevis","currencyCode":"XCD"}, 178 | {"code":"LC","name":"圣卢西亚","nameEn":"Saint Lucia","currencyCode":"XCD"}, 179 | {"code":"SM","name":"圣马力诺","nameEn":"San Marino","currencyCode":"EUR"}, 180 | {"code":"PM","name":"圣皮埃尔和密克隆","nameEn":"Saint Pierre and Miquelon","currencyCode":"EUR"}, 181 | {"code":"VC","name":"圣文森特和格林纳丁斯","nameEn":"Saint Vincent and the Grenadines","currencyCode":"XCD"}, 182 | {"code":"LK","name":"斯里兰卡","nameEn":"Sri Lanka","currencyCode":"USD"}, 183 | {"code":"SK","name":"斯洛伐克","nameEn":"Slovakia","currencyCode":"EUR"}, 184 | {"code":"SI","name":"斯洛文尼亚","nameEn":"Slovenia","currencyCode":"EUR"}, 185 | {"code":"SJ","name":"斯瓦尔巴和扬马延","nameEn":"Svalbard and Jan Mayen","currencyCode":"NOK"}, 186 | {"code":"SZ","name":"斯威士兰","nameEn":"Eswatini","currencyCode":"SZL"}, 187 | {"code":"SD","name":"苏丹","nameEn":"Sudan","currencyCode":"SDG"}, 188 | {"code":"SR","name":"苏里南","nameEn":"Suriname","currencyCode":"SRD"}, 189 | {"code":"SB","name":"所罗门群岛","nameEn":"Solomon Islands","currencyCode":"SBD"}, 190 | {"code":"SO","name":"索马里","nameEn":"Somalia","currencyCode":"SOS"}, 191 | {"code":"TJ","name":"塔吉克斯坦","nameEn":"Tajikistan","currencyCode":"TJS"}, 192 | {"code":"TH","name":"泰国","nameEn":"Thailand","currencyCode":"THB"}, 193 | {"code":"TZ","name":"坦桑尼亚","nameEn":"Tanzania, United Republic of","currencyCode":"TZS"}, 194 | {"code":"TO","name":"汤加","nameEn":"Tonga","currencyCode":"TOP"}, 195 | {"code":"TC","name":"特克斯和凯科斯群岛","nameEn":"Turks and Caicos Islands","currencyCode":"USD"}, 196 | {"code":"TT","name":"特立尼达和多巴哥","nameEn":"Trinidad and Tobago","currencyCode":"TTD"}, 197 | {"code":"TN","name":"突尼斯","nameEn":"Tunisia","currencyCode":"TND"}, 198 | {"code":"TV","name":"图瓦卢","nameEn":"Tuvalu","currencyCode":"AUD"}, 199 | {"code":"TR","name":"土耳其","nameEn":"Turkey","currencyCode":"USD"}, 200 | {"code":"TM","name":"土库曼斯坦","nameEn":"Turkmenistan","currencyCode":"TMT"}, 201 | {"code":"TK","name":"托克劳","nameEn":"Tokelau","currencyCode":"NZD"}, 202 | {"code":"WF","name":"瓦利斯和富图纳","nameEn":"Wallis and Futuna","currencyCode":"XPF"}, 203 | {"code":"VU","name":"瓦努阿图","nameEn":"Vanuatu","currencyCode":"VUV"}, 204 | {"code":"GT","name":"危地马拉","nameEn":"Guatemala","currencyCode":"GTQ"}, 205 | {"code":"VE","name":"委内瑞拉","nameEn":"Venezuela, Bolivarian Republic of","currencyCode":"VES"}, 206 | {"code":"BN","name":"文莱","nameEn":"Brunei Darussalam","currencyCode":"BND"}, 207 | {"code":"UG","name":"乌干达","nameEn":"Uganda","currencyCode":"UGX"}, 208 | {"code":"UA","name":"乌克兰","nameEn":"Ukraine","currencyCode":"UAH"}, 209 | {"code":"UY","name":"乌拉圭","nameEn":"Uruguay","currencyCode":"UYU"}, 210 | {"code":"UZ","name":"乌兹别克斯坦","nameEn":"Uzbekistan","currencyCode":"UZS"}, 211 | {"code":"ES","name":"西班牙","nameEn":"Spain","currencyCode":"EUR"}, 212 | {"code":"GR","name":"希腊","nameEn":"Greece","currencyCode":"EUR"}, 213 | {"code":"SG","name":"新加坡","nameEn":"Singapore","currencyCode":"SGD"}, 214 | {"code":"NC","name":"新喀里多尼亚","nameEn":"New Caledonia","currencyCode":"XPF"}, 215 | {"code":"NZ","name":"新西兰","nameEn":"New Zealand","currencyCode":"NZD"}, 216 | {"code":"HU","name":"匈牙利","nameEn":"Hungary","currencyCode":"HUF"}, 217 | {"code":"SY","name":"叙利亚","nameEn":"Syrian Arab Republic","currencyCode":"SYP"}, 218 | {"code":"JM","name":"牙买加","nameEn":"Jamaica","currencyCode":"JMD"}, 219 | {"code":"AM","name":"亚美尼亚","nameEn":"Armenia","currencyCode":"AMD"}, 220 | {"code":"YE","name":"也门","nameEn":"Yemen","currencyCode":"YER"}, 221 | {"code":"IQ","name":"伊拉克","nameEn":"Iraq","currencyCode":"IQD"}, 222 | {"code":"IR","name":"伊朗","nameEn":"Iran, Islamic Republic of","currencyCode":"IRR"}, 223 | {"code":"IL","name":"以色列","nameEn":"Israel","currencyCode":"ILS"}, 224 | {"code":"IT","name":"意大利","nameEn":"Italy","currencyCode":"EUR"}, 225 | {"code":"IN","name":"印度","nameEn":"India","currencyCode":"INR"}, 226 | {"code":"ID","name":"印度尼西亚","nameEn":"Indonesia","currencyCode":"IDR"}, 227 | {"code":"GB","name":"英国","nameEn":"United Kingdom of Great Britain and Northern Ireland","currencyCode":"GBP"}, 228 | {"code":"VG","name":"英属维尔京群岛","nameEn":"Virgin Islands, British","currencyCode":"USD"}, 229 | {"code":"IO","name":"英属印度洋领地","nameEn":"British Indian Ocean Territory","currencyCode":"GBP"}, 230 | {"code":"IO","name":"英属印度洋领地","nameEn":"British Indian Ocean Territory","currencyCode":"USD"}, 231 | {"code":"JO","name":"约旦","nameEn":"Jordan","currencyCode":"JOD"}, 232 | {"code":"VN","name":"越南","nameEn":"Viet Nam","currencyCode":"VND"}, 233 | {"code":"ZM","name":"赞比亚","nameEn":"Zambia","currencyCode":"ZMW"}, 234 | {"code":"JE","name":"泽西","nameEn":"Jersey","currencyCode":"GBP"}, 235 | {"code":"TD","name":"乍得","nameEn":"Chad","currencyCode":"XAF"}, 236 | {"code":"GI","name":"直布罗陀","nameEn":"Gibraltar","currencyCode":"GIP"}, 237 | {"code":"CL","name":"智利","nameEn":"Chile","currencyCode":"CLF"}, 238 | {"code":"CL","name":"智利","nameEn":"Chile","currencyCode":"CLP"}, 239 | {"code":"CF","name":"中非","nameEn":"Central African Republic","currencyCode":"XAF"}, 240 | {"code":"CN","name":"中国","nameEn":"China","currencyCode":"CNY"}, 241 | {"code":"TW","name":"中国台湾","nameEn":"Taiwan, Province of China","currencyCode":"TWD"}, 242 | {"code":"HK","name":"中国香港","nameEn":"Hong Kong","currencyCode":"HKD"} 243 | ] 244 | -------------------------------------------------------------------------------- /src/county/county-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "code" : "AE", "currencyCode" : "AED", "name" : "阿联酋" , "nameEn" : "United Arab Emirates" }, 3 | { "code" : "AU", "currencyCode" : "AUD", "name" : "澳大利亚" , "nameEn" : "Australia" }, 4 | { "code" : "BR", "currencyCode" : "BRL", "name" : "巴西" , "nameEn" : "Brazil" }, 5 | { "code" : "CA", "currencyCode" : "CAD", "name" : "加拿大" , "nameEn" : "Canada" }, 6 | { "code" : "CH", "currencyCode" : "CHF", "name" : "瑞士" , "nameEn" : "Switzerland" }, 7 | { "code" : "CL", "currencyCode" : "CLP", "name" : "智利" , "nameEn" : "Chile" }, 8 | { "code" : "CN", "currencyCode" : "CNY", "name" : "中国" , "nameEn" : "China" }, 9 | { "code" : "CO", "currencyCode" : "COP", "name" : "哥伦比亚" , "nameEn" : "Colombia" }, 10 | { "code" : "CR", "currencyCode" : "CRC", "name" : "哥斯达黎加", "nameEn" : "Costa Rica" }, 11 | { "code" : "AD", "currencyCode" : "EUR", "name" : "安道尔" , "nameEn" : "Andorra" }, 12 | { "code" : "DE", "currencyCode" : "EUR", "name" : "德国" , "nameEn" : "Germany" }, 13 | { "code" : "EE", "currencyCode" : "EUR", "name" : "爱沙尼亚" , "nameEn" : "Estonia" }, 14 | { "code" : "ES", "currencyCode" : "EUR", "name" : "西班牙" , "nameEn" : "Spain" }, 15 | { "code" : "FR", "currencyCode" : "EUR", "name" : "法国" , "nameEn" : "France" }, 16 | { "code" : "IT", "currencyCode" : "EUR", "name" : "意大利" , "nameEn" : "Italy" }, 17 | { "code" : "LT", "currencyCode" : "EUR", "name" : "立陶宛" , "nameEn" : "Lithuania" }, 18 | { "code" : "LU", "currencyCode" : "EUR", "name" : "卢森堡" , "nameEn" : "Luxembourg" }, 19 | { "code" : "LV", "currencyCode" : "EUR", "name" : "拉脱维亚" , "nameEn" : "Latvia" }, 20 | { "code" : "MC", "currencyCode" : "EUR", "name" : "摩纳哥" , "nameEn" : "Monaco" }, 21 | { "code" : "ME", "currencyCode" : "EUR", "name" : "黑山" , "nameEn" : "Montenegro" }, 22 | { "code" : "SI", "currencyCode" : "EUR", "name" : "斯洛文尼亚", "nameEn" : "Slovenia" }, 23 | { "code" : "SK", "currencyCode" : "EUR", "name" : "斯洛伐克" , "nameEn" : "Slovakia" }, 24 | { "code" : "SM", "currencyCode" : "EUR", "name" : "圣马力诺" , "nameEn" : "San Marino" }, 25 | { "code" : "VA", "currencyCode" : "EUR", "name" : "梵蒂冈" , "nameEn" : "Holy See" }, 26 | { "code" : "GB", "currencyCode" : "GBP", "name" : "英国" , "nameEn" : "United Kingdom" }, 27 | { "code" : "HK", "currencyCode" : "HKD", "name" : "中国香港" , "nameEn" : "Hong Kong" }, 28 | { "code" : "ID", "currencyCode" : "IDR", "name" : "印度尼西亚", "nameEn" : "Indonesia" }, 29 | { "code" : "IL", "currencyCode" : "ILS", "name" : "以色列" , "nameEn" : "Israel" }, 30 | { "code" : "IN", "currencyCode" : "INR", "name" : "印度" , "nameEn" : "India" }, 31 | { "code" : "JP", "currencyCode" : "JPY", "name" : "日本" , "nameEn" : "Japan" }, 32 | { "code" : "KR", "currencyCode" : "KRW", "name" : "韩国" , "nameEn" : "South Korea" }, 33 | { "code" : "KW", "currencyCode" : "KWD", "name" : "科威特" , "nameEn" : "Kuwait" }, 34 | { "code" : "KZ", "currencyCode" : "KZT", "name" : "哈萨克斯坦", "nameEn" : "Kazakhstan" }, 35 | { "code" : "MX", "currencyCode" : "MXN", "name" : "墨西哥" , "nameEn" : "Mexico" }, 36 | { "code" : "MY", "currencyCode" : "MYR", "name" : "马来西亚" , "nameEn" : "Malaysia" }, 37 | { "code" : "NO", "currencyCode" : "NOK", "name" : "挪威" , "nameEn" : "Norway" }, 38 | { "code" : "NZ", "currencyCode" : "NZD", "name" : "新西兰" , "nameEn" : "New Zealand" }, 39 | { "code" : "PE", "currencyCode" : "PEN", "name" : "秘鲁" , "nameEn" : "Peru" }, 40 | { "code" : "PH", "currencyCode" : "PHP", "name" : "菲律宾" , "nameEn" : "Philippines" }, 41 | { "code" : "PL", "currencyCode" : "PLN", "name" : "波兰" , "nameEn" : "Poland" }, 42 | { "code" : "QA", "currencyCode" : "QAR", "name" : "卡塔尔" , "nameEn" : "Qatar" }, 43 | { "code" : "RU", "currencyCode" : "RUB", "name" : "俄罗斯" , "nameEn" : "Russia" }, 44 | { "code" : "SA", "currencyCode" : "SAR", "name" : "沙特阿拉伯", "nameEn" : "Saudi Arabia" }, 45 | { "code" : "SG", "currencyCode" : "SGD", "name" : "新加坡" , "nameEn" : "Singapore" }, 46 | { "code" : "TH", "currencyCode" : "THB", "name" : "泰国" , "nameEn" : "Thailand" }, 47 | { "code" : "TW", "currencyCode" : "TWD", "name" : "中国台湾" , "nameEn" : "Taiwan" }, 48 | { "code" : "UA", "currencyCode" : "UAH", "name" : "乌克兰" , "nameEn" : "Ukraine" }, 49 | { "code" : "AR", "currencyCode" : "USD", "name" : "阿根廷" , "nameEn" : "Argentina" }, 50 | { "code" : "AZ", "currencyCode" : "USD", "name" : "阿塞拜疆" , "nameEn" : "Azerbaijan" }, 51 | { "code" : "PK", "currencyCode" : "USD", "name" : "巴基斯坦" , "nameEn" : "Pakistan" }, 52 | { "code" : "TR", "currencyCode" : "USD", "name" : "土耳其" , "nameEn" : "Turkey" }, 53 | { "code" : "US", "currencyCode" : "USD", "name" : "美国" , "nameEn" : "United States" }, 54 | { "code" : "ZA", "currencyCode" : "USD", "name" : "南非" , "nameEn" : "South Africa" }, 55 | { "code" : "UY", "currencyCode" : "UYU", "name" : "乌拉圭" , "nameEn" : "Uruguay" }, 56 | { "code" : "VN", "currencyCode" : "VND", "name" : "越南" , "nameEn" : "Vietnam" } 57 | ] 58 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // mdui 2 | import 'mdui/mdui.css' 3 | import 'mdui' 4 | import './style/home.less' 5 | import './style/search.less' 6 | import './style/market.less' 7 | 8 | import {main} from './RealMain' 9 | import {countyCode2Info} from './county/CountyInfo' 10 | import {SettingManager} from './setting/SettingManager' 11 | import {createApp} from 'vue' 12 | // @ts-ignore 13 | import App from './App.vue' 14 | import {unsafeWindow} from 'vite-plugin-monkey/dist/client' 15 | import {SpcContext} from './SpcContext' 16 | import {CountyCodeGetterManager} from './county/CountyCodeGetterManager' 17 | import {SpcManager} from './SpcManager' 18 | import {Logger, setLogLevel} from './utils/Logger' 19 | import {GmUtils} from './utils/GmUtils' 20 | import {IM_MENU_SETTING} from './constant/Constant' 21 | import {ReactUtils} from './utils/ReactUtils' 22 | 23 | (async () => { 24 | if (ReactUtils.useReact()) { 25 | await ReactUtils.waitForReactInit(async (root, reactProp) => { 26 | console.log('React is ready!', {root, reactProp}) 27 | await initContext() 28 | initApp() 29 | registerMenu() 30 | await main() 31 | }) 32 | } else { 33 | console.log('React is not detected!') 34 | await initContext() 35 | initApp() 36 | registerMenu() 37 | await main() 38 | } 39 | })() 40 | 41 | function initApp() { 42 | createApp(App).mount( 43 | (() => { 44 | const app = document.createElement('div') 45 | app.setAttribute('id', 'spc-menu') 46 | document.body.append(app) 47 | return app 48 | })(), 49 | ) 50 | } 51 | 52 | function registerMenu() { 53 | GmUtils.registerMenuCommand(IM_MENU_SETTING) 54 | } 55 | 56 | async function initContext() { 57 | const setting = SettingManager.instance.setting 58 | setLogLevel(setting.logLevel) 59 | 60 | let targetCountyInfo = countyCode2Info.get(setting.countyCode) 61 | if (!targetCountyInfo) { 62 | Logger.warn(`获取转换后的国家(${setting.countyCode})信息失败,默认为美国:` + setting.countyCode) 63 | targetCountyInfo = countyCode2Info.get('US') 64 | } 65 | Logger.info('目标区域:', targetCountyInfo) 66 | 67 | const currCountyCode = await CountyCodeGetterManager.instance.getCountyCode() 68 | const currCountInfo = countyCode2Info.get(currCountyCode) 69 | if (!currCountyCode) { 70 | throw Error('缺少当前国家的信息映射:county: ' + currCountyCode) 71 | } 72 | Logger.info('当前区域:', currCountInfo) 73 | 74 | // @ts-ignore 75 | unsafeWindow.SpcManager = SpcManager.instance 76 | // @ts-ignore 77 | unsafeWindow.spcContext = new SpcContext(setting, targetCountyInfo, currCountInfo) 78 | } 79 | -------------------------------------------------------------------------------- /src/rate/AugmentedSteamRateApi.ts: -------------------------------------------------------------------------------- 1 | import {IRateApi} from './IRateApi' 2 | import {Http} from '../utils/Http' 3 | import {Logger} from '../utils/Logger' 4 | import {Strings} from '../utils/Strings' 5 | import {SpcContext} from '../SpcContext' 6 | 7 | 8 | export class AugmentedSteamRateApi implements IRateApi { 9 | 10 | getName(): string { 11 | return 'AugmentedSteamRateApi' 12 | } 13 | 14 | async getRate(): Promise { 15 | const context = SpcContext.getContext() 16 | Logger.info(Strings.format('通过 AugmentedSteam 获取汇率 %s(%s) -> %s(%s)...', 17 | context.currentCountyInfo.currencyCode, 18 | context.currentCountyInfo.name, 19 | context.targetCountyInfo.currencyCode, 20 | context.targetCountyInfo.name)) 21 | const url = `https://api.augmentedsteam.com/rates/v1?to=${context.currentCountyInfo.currencyCode}` 22 | let rate: number | void | null = await Http.get(Map, url) 23 | .then(res => res.get(context.targetCountyInfo.currencyCode)![context.currentCountyInfo.currencyCode]) 24 | .catch(err => Logger.error('通过 AugmentedSteam 获取汇率失败', err)) 25 | if (rate) { 26 | return rate 27 | } 28 | throw new Error(`通过 ${this.getName()} 获取汇率失败。`) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/rate/IRateApi.ts: -------------------------------------------------------------------------------- 1 | export interface IRateApi { 2 | 3 | /** 4 | * 获取实现的名称 5 | */ 6 | getName(): string 7 | 8 | /** 9 | * 获取汇率 10 | * Currency -> rate 11 | */ 12 | getRate(): Promise 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/rate/RateCaches.ts: -------------------------------------------------------------------------------- 1 | export class RateCache { 2 | 3 | constructor(from: string, to: string, rate?: number, createdAt?: number) { 4 | this.from = from 5 | this.to = to 6 | this.createdAt = createdAt || 0 7 | this.rate = rate || 0 8 | } 9 | 10 | from: string 11 | 12 | to: string 13 | 14 | createdAt: number 15 | 16 | rate: number 17 | } 18 | 19 | export class RateCaches { 20 | 21 | caches: Map = new Map() 22 | 23 | getCache(from: string, to: string): RateCache | undefined { 24 | return this.caches.get(this.buildCacheKey(from, to)) 25 | } 26 | 27 | setCache(cache: RateCache) { 28 | this.caches.set(this.buildCacheKey(cache.from, cache.to), cache) 29 | } 30 | 31 | private buildCacheKey(from: string, to: string): string { 32 | return `${from}:${to}` 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/rate/RateManager.ts: -------------------------------------------------------------------------------- 1 | import {IRateApi} from './IRateApi' 2 | import {RateCache, RateCaches} from './RateCaches' 3 | import {STORAGE_KEY_RATE_CACHES} from '../constant/Constant' 4 | import {AugmentedSteamRateApi} from './AugmentedSteamRateApi' 5 | import {Logger} from '../utils/Logger' 6 | import {SpcContext} from '../SpcContext' 7 | import {GmUtils} from '../utils/GmUtils' 8 | 9 | export class RateManager implements IRateApi { 10 | 11 | public static instance: RateManager = new RateManager() 12 | private readonly rateApis: IRateApi[] 13 | private rateCaches: RateCaches = new RateCaches() 14 | 15 | private constructor() { 16 | this.rateApis = [ 17 | new AugmentedSteamRateApi() 18 | ] 19 | } 20 | 21 | getName(): string { 22 | return 'RateManager' 23 | } 24 | 25 | private async getRate4Remote(): Promise { 26 | Logger.info('远程获取汇率...') 27 | let rate: number | undefined 28 | for (let rateApi of this.rateApis) { 29 | try { 30 | rate = await rateApi.getRate() 31 | } catch (e) { 32 | Logger.error(`使用实现(${rateApi.getName()})获取汇率失败`) 33 | } 34 | if (rate) { 35 | return rate 36 | } 37 | } 38 | throw Error('所有汇率获取实现获取汇率均失败') 39 | } 40 | 41 | public async getRate(): Promise { 42 | const context = SpcContext.getContext() 43 | if (context.setting.useCustomRate) { 44 | Logger.info('使用自定义汇率') 45 | return context.setting.customRate 46 | } 47 | 48 | this.rateCaches = this.loadRateCache() 49 | let cache = this.rateCaches.getCache(context.currentCountyInfo.code, context.targetCountyInfo.code) 50 | // 过期需要重新获取 51 | const now = new Date().getTime() 52 | const expired = context.setting.rateCacheExpired 53 | if (!cache || !cache.rate || now > cache.createdAt + expired) { 54 | // 两小时过期时间 55 | Logger.info(`本地缓存已过期`) 56 | cache = new RateCache(context.currentCountyInfo.code, context.targetCountyInfo.code) 57 | cache.rate = await this.getRate4Remote() 58 | cache.createdAt = new Date().getTime() 59 | this.rateCaches.setCache(cache) 60 | this.saveRateCache() 61 | } 62 | return cache.rate 63 | } 64 | 65 | private loadRateCache(): RateCaches { 66 | const setting = SpcContext.getContext().setting 67 | if (setting.oldVersion !== setting.currVersion) { 68 | Logger.info(`脚本版本发生变化需要刷新汇率缓存`) 69 | this.clear() 70 | return new RateCaches() 71 | } 72 | 73 | Logger.info(`读取汇率缓存`) 74 | return GmUtils.getValue(RateCaches, STORAGE_KEY_RATE_CACHES, new RateCaches()) 75 | } 76 | 77 | private saveRateCache() { 78 | Logger.info('保存汇率缓存', this.rateCaches) 79 | GmUtils.setValue(STORAGE_KEY_RATE_CACHES, this.rateCaches) 80 | } 81 | 82 | public clear() { 83 | GmUtils.deleteValue(STORAGE_KEY_RATE_CACHES) 84 | } 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/setting/Setting.ts: -------------------------------------------------------------------------------- 1 | import {LogLabel} from '../utils/Logger' 2 | 3 | export class Setting { 4 | /** 5 | * 目标国家代码,默认CN 6 | */ 7 | countyCode: string = 'CN' 8 | 9 | /** 10 | * 目标货币符号,默认 ¥ 11 | */ 12 | currencySymbol: string = '¥' 13 | 14 | /** 15 | * 符号位置在首 16 | */ 17 | currencySymbolBeforeValue: boolean = true 18 | 19 | /** 20 | * 汇率获取时间,默认1小时 21 | */ 22 | rateCacheExpired: number = 1000 * 60 * 60 23 | 24 | /** 25 | * 使用自定义汇率 26 | */ 27 | useCustomRate: boolean = false 28 | 29 | /** 30 | * 自定义汇率 31 | */ 32 | customRate: number = 1 33 | 34 | /** 35 | * 前一个版本 36 | */ 37 | oldVersion: string = '0.0.0' 38 | 39 | /** 40 | * 当前版本 41 | */ 42 | currVersion: string = '0.0.0' 43 | 44 | /** 45 | * 日志级别 46 | */ 47 | logLevel: LogLabel = 'info' 48 | } 49 | -------------------------------------------------------------------------------- /src/setting/SettingManager.ts: -------------------------------------------------------------------------------- 1 | import {Setting} from './Setting' 2 | import {STORAGE_KEY_SETTING} from '../constant/Constant' 3 | import {countyCode2Info} from '../county/CountyInfo' 4 | import {GM_info} from '$' 5 | import {Logger} from '../utils/Logger' 6 | import {Strings} from '../utils/Strings' 7 | import {GmUtils} from '../utils/GmUtils' 8 | 9 | export class SettingManager { 10 | public static instance: SettingManager = new SettingManager() 11 | setting: Setting 12 | 13 | private constructor() { 14 | this.setting = this.loadSetting() 15 | } 16 | 17 | private loadSetting(): Setting { 18 | const setting = GmUtils.getValue(Setting, STORAGE_KEY_SETTING, new Setting()) 19 | setting.oldVersion = setting.currVersion 20 | setting.currVersion = GM_info.script.version 21 | 22 | if (setting.oldVersion === setting.currVersion) { 23 | Logger.info('读取设置', setting) 24 | } else { 25 | Logger.debug(Strings.format(`版本更新重置设置:%s -> %s`, setting.oldVersion, setting.currVersion)) 26 | this.saveSetting(setting) 27 | } 28 | 29 | return setting 30 | } 31 | 32 | /** 33 | * 保存设置 34 | * @param setting 设置 35 | */ 36 | public saveSetting(setting: Setting) { 37 | Logger.info('保存设置', setting) 38 | this.setting = setting 39 | GmUtils.setValue(STORAGE_KEY_SETTING, setting) 40 | } 41 | 42 | public setCountyCode(countyCode: string) { 43 | const county = countyCode2Info.get(countyCode) 44 | if (!county) { 45 | throw Error(`国家代码不存在:${countyCode}`) 46 | } 47 | this.setting.countyCode = countyCode 48 | this.saveSetting(this.setting) 49 | } 50 | 51 | public setCurrencySymbol(currencySymbol: string) { 52 | this.setting.currencySymbol = currencySymbol 53 | this.saveSetting(this.setting) 54 | } 55 | 56 | public setCurrencySymbolBeforeValue(isCurrencySymbolBeforeValue: boolean) { 57 | this.setting.currencySymbolBeforeValue = isCurrencySymbolBeforeValue 58 | this.saveSetting(this.setting) 59 | } 60 | 61 | public reset() { 62 | this.saveSetting(new Setting()) 63 | } 64 | 65 | public setRateCacheExpired(rateCacheExpired: number) { 66 | this.setting.rateCacheExpired = rateCacheExpired 67 | this.saveSetting(this.setting) 68 | } 69 | 70 | public setUseCustomRate(isUseCustomRate: boolean) { 71 | this.setting.useCustomRate = isUseCustomRate 72 | this.saveSetting(this.setting) 73 | } 74 | 75 | public setCustomRate(customRate: number) { 76 | this.setting.customRate = customRate 77 | this.saveSetting(this.setting) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/style/home.less: -------------------------------------------------------------------------------- 1 | // 首页列表溢出 2 | div.home_tabs_content .tab_item_discount { 3 | width: 185px; 4 | } 5 | -------------------------------------------------------------------------------- /src/style/market.less: -------------------------------------------------------------------------------- 1 | #popularItemsTable { 2 | @price-width: 90px; 3 | @adapter-price-width: 40px; 4 | @adjusted-price-width: @price-width+@adapter-price-width; 5 | 6 | // 标题 7 | .market_listing_table_header { 8 | .market_listing_their_price { 9 | width: @adjusted-price-width; 10 | } 11 | } 12 | 13 | // 价格值 14 | .market_listing_row_link > .market_listing_row { 15 | .market_listing_right_cell.market_listing_their_price { 16 | width: @adjusted-price-width; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/style/search.less: -------------------------------------------------------------------------------- 1 | .search_result_row { 2 | // 配置项 3 | @adapter-released-width: 20px; 4 | @adapter-price-width: 15px; 5 | @adapter-name-width: 0px; 6 | // 原始值 7 | @released-width: 85px; 8 | @name-width: 275px; 9 | @price-width: 150px; 10 | // 调整后 11 | @adjusted-released-width: @released-width + @adapter-released-width; 12 | @adjusted-price-width: @price-width + @adapter-price-width; 13 | @adjusted-name-width: (@name-width + @released-width + @price-width) - (@adjusted-released-width + @adjusted-price-width); 14 | 15 | // 列表游戏名称 16 | & .col.search_name { 17 | width: @adjusted-name-width; 18 | } 19 | 20 | // 列表发售日期 21 | & .col.search_released { 22 | width: @adjusted-released-width; 23 | } 24 | 25 | // 列表价格 26 | & .col.search_discount_and_price .discount_block { 27 | width: @adjusted-price-width; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/style/style.less: -------------------------------------------------------------------------------- 1 | //body { background: #1b2838 !important; } 2 | 3 | #spc-menu { 4 | @top: 0px; 5 | position: absolute; 6 | top: @top; 7 | width: 100px; 8 | height: 100px; 9 | z-index: 99999; 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | .tab_item_discount { 15 | min-width: 113px !important; 16 | width: unset; 17 | } 18 | 19 | .discount_final_price { 20 | display: inline-block !important; 21 | } 22 | 23 | /*商店搜索列表*/ 24 | .search_result_row .col.search_price { 25 | width: 175px; 26 | } 27 | 28 | .search_result_row .col.search_name { 29 | width: 200px; 30 | } 31 | 32 | /*市场列表*/ 33 | .market_listing_their_price { 34 | width: 160px; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/GmUtils.ts: -------------------------------------------------------------------------------- 1 | import {GM_deleteValue, GM_getValue, GM_registerMenuCommand, GM_setValue} from '$' 2 | import {GM_addValueChangeListener} from 'vite-plugin-monkey/dist/client' 3 | import {Logger} from './Logger' 4 | import {ClassConstructor, Jsons} from './Jsons' 5 | 6 | export class GmUtils { 7 | public static getValue(cls: ClassConstructor, key: string, defaultValue: T): T { 8 | const value = GM_getValue(key) 9 | return value ? Jsons.readString(value as string, cls) : defaultValue 10 | } 11 | 12 | public static setValue(key: string, value: any): void { 13 | GM_setValue(key, Jsons.toString(value)) 14 | } 15 | 16 | public static deleteValue(key: string) { 17 | GM_deleteValue(key) 18 | } 19 | 20 | public static registerMenuCommand(caption: string, 21 | onClick?: (event: T) => void, 22 | accessKey?: string | undefined) { 23 | const key = `GM_registerMenuCommand@${caption}` 24 | GM_registerMenuCommand( 25 | caption, 26 | event => { 27 | this.setValue(key, true) 28 | Logger.debug('点击菜单:' + caption) 29 | this.setValue(key, false) 30 | if (onClick) { 31 | onClick(event as T) 32 | } 33 | }, 34 | accessKey) 35 | } 36 | 37 | public static addMenuClickEventListener(caption: string, 38 | onClick: () => void) { 39 | const key = `GM_registerMenuCommand@${caption}` 40 | // @ts-ignore 41 | GM_addValueChangeListener(key, (name, oldValue, newValue) => { 42 | if (newValue) { 43 | onClick() 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/Http.ts: -------------------------------------------------------------------------------- 1 | import {GM_xmlhttpRequest, GmXhrRequest} from '$' 2 | import {ClassConstructor, Jsons} from './Jsons' 3 | 4 | export class Http { 5 | 6 | static get(cls: ClassConstructor, url: string, details?: GmXhrRequest): Promise { 7 | if (!details) { 8 | details = {url} 9 | } 10 | details.method = 'GET' 11 | return this.request(cls, details) 12 | } 13 | 14 | static post(url: string, cls: ClassConstructor, details?: GmXhrRequest): Promise { 15 | if (!details) { 16 | details = {url} 17 | } 18 | details.method = 'POST' 19 | return this.request(cls, details) 20 | } 21 | 22 | private static request(cls: ClassConstructor, details: GmXhrRequest): Promise { 23 | return new Promise((resolve, reject) => { 24 | details.onload = response => { 25 | if (cls.name === String.name) { 26 | resolve(response.response as T) 27 | } else { 28 | const json = JSON.parse(response.response) 29 | // @ts-ignore 30 | resolve(Jsons.readJson(json, cls)) 31 | } 32 | } 33 | details.onerror = error => reject(error) 34 | GM_xmlhttpRequest(details) 35 | }) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/Jsons.ts: -------------------------------------------------------------------------------- 1 | export type ClassConstructor = new () => T 2 | 3 | export class Jsons { 4 | /** 5 | * 将对象转换为普通 JSON 对象 6 | */ 7 | public static toJson(obj: T): Record { 8 | return {...obj} as Record 9 | } 10 | 11 | /** 12 | * 将对象转换为 JSON 字符串 13 | */ 14 | public static toString(obj: any): string { 15 | return JSON.stringify(this.toJson(obj)) 16 | } 17 | 18 | /** 19 | * 将普通 JSON 对象解析为指定类型,支持嵌套处理,包括 Map 和 Record 中的 class 20 | */ 21 | public static readJson(json: Record, cls?: ClassConstructor): T { 22 | if (!cls) { 23 | // 如果没有提供构造器,直接返回 JSON 数据(适用于简单的对象类型) 24 | if (typeof json !== 'object' || json === null) { 25 | throw new Error('Invalid JSON input') 26 | } 27 | return json as T 28 | } 29 | 30 | // 创建目标类的实例 31 | const instance = new cls() 32 | 33 | // 直接处理 Map 类型 34 | if (instance instanceof Map) { 35 | return this.handleMap(json, instance) as T 36 | } 37 | 38 | for (const key of Reflect.ownKeys(json) as (keyof T)[]) { 39 | const value = json[key] 40 | 41 | if (value === null || value === undefined) { 42 | // 跳过空值 43 | (instance as any)[key] = value 44 | continue 45 | } 46 | 47 | const fieldValue = (instance as any)[key] 48 | 49 | if (fieldValue !== null && typeof fieldValue === 'object' && !(fieldValue instanceof Array)) { 50 | if (fieldValue instanceof Map) { 51 | // 处理 Map 类型 52 | (instance as any)[key] = this.handleMap(value, fieldValue) 53 | } else if (fieldValue instanceof Object) { 54 | // 处理普通对象 55 | (instance as any)[key] = this.readJson(value, fieldValue.constructor as any) 56 | } 57 | } else if (Array.isArray(fieldValue)) { 58 | // 如果是数组,递归处理数组中的对象 59 | (instance as any)[key] = value.map((item: any) => 60 | typeof item === 'object' ? this.readJson(item, fieldValue[0]?.constructor as any) : item 61 | ) 62 | } else { 63 | // 普通字段直接赋值 64 | (instance as any)[key] = value 65 | } 66 | } 67 | 68 | return instance 69 | } 70 | 71 | /** 72 | * 处理 Map 类型的转换,其中 V 可以是一个 class 73 | */ 74 | private static handleMap( 75 | value: Record, 76 | mapInstance: Map 77 | ): Map { 78 | const map = new Map() 79 | if (value && typeof value === 'object') { 80 | for (const key of Object.keys(value) as K[]) { 81 | const mapValue = value[key] 82 | if (mapValue === null || mapValue === undefined) { 83 | map.set(key, mapValue) 84 | continue 85 | } 86 | 87 | const existingValue = mapInstance.get(key) 88 | if (this.isObject(mapValue) && existingValue) { 89 | // 递归处理 Map 中的值 90 | map.set(key, this.readJson(mapValue, existingValue.constructor as any)) 91 | } else { 92 | map.set(key, mapValue) 93 | } 94 | } 95 | } 96 | return map 97 | } 98 | 99 | private static isObject(value: any): value is object { 100 | return value !== null && typeof value === 'object' 101 | } 102 | 103 | /** 104 | * 将 JSON 字符串解析为指定类型,支持嵌套处理,包括 Map 和 Record 中的 class 105 | */ 106 | public static readString(jsonString: string, cls?: new () => T): T { 107 | const json = JSON.parse(jsonString) 108 | return this.readJson(json, cls) 109 | 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | interface LogLevelDefinition { 2 | index: number; 3 | color: string; 4 | label: string; 5 | bindMethod: typeof console.info | typeof console.warn | typeof console.error; 6 | } 7 | 8 | function initializeLogTitle(): string { 9 | const isInIFrame = window.parent !== window || window.frames.length > 0 10 | return isInIFrame ? `steam-price-convertor iframe(${new Date().getMilliseconds()})` : 'steam-price-convertor' 11 | } 12 | 13 | function createLogStyle(color: string): string { 14 | return ` 15 | background: ${color}; 16 | color: white; 17 | padding: 1px 3px; 18 | border-radius: 2px; 19 | ` 20 | } 21 | 22 | function composeLogHint(): string { 23 | return `%c${initializeLogTitle()}` 24 | } 25 | 26 | export type LogLabel = 'debug' | 'info' | 'warn' | 'error' | 'off' 27 | 28 | export const LogDefinitions: Record = { 29 | debug: { 30 | index: 0, 31 | color: '#009688', 32 | label: 'debug', 33 | bindMethod: console.info 34 | }, 35 | info: { 36 | index: 1, 37 | color: '#2196f3', 38 | label: 'info', 39 | bindMethod: console.info 40 | }, 41 | warn: { 42 | index: 2, 43 | color: '#ffc107', 44 | label: 'warn', 45 | bindMethod: console.warn 46 | }, 47 | error: { 48 | index: 3, 49 | color: '#e91e63', 50 | label: 'error', 51 | bindMethod: console.error 52 | }, 53 | off: { 54 | index: 4, 55 | color: '', 56 | label: 'off', 57 | bindMethod: () => { 58 | } 59 | } 60 | } 61 | 62 | export const Logger: Record< 63 | 'debug' | 'info' | 'warn' | 'error', 64 | typeof console.debug | typeof console.info | typeof console.warn | typeof console.error 65 | > = { 66 | debug: noopLog.bind(null), 67 | info: noopLog.bind(null), 68 | warn: noopLog.bind(null), 69 | error: noopLog.bind(null), 70 | } 71 | 72 | function noopLog() { 73 | } 74 | 75 | let currLogLevel: LogLevelDefinition = LogDefinitions.info 76 | 77 | function refreshBinding() { 78 | const hint = composeLogHint() 79 | Object.entries(LogDefinitions).forEach(([label, def]) => { 80 | if (def.index >= currLogLevel.index) { 81 | const logStyle = createLogStyle(def.color) 82 | Logger[label.toLowerCase() as keyof typeof Logger] = def.bindMethod.bind(console, hint, logStyle) 83 | } else { 84 | Logger[label.toLowerCase() as keyof typeof Logger] = noopLog.bind(null) 85 | } 86 | }) 87 | } 88 | 89 | export function setLogLevel(levelLabel: keyof typeof LogDefinitions) { 90 | const newLevel = LogDefinitions[levelLabel] 91 | if (newLevel) { 92 | currLogLevel = newLevel 93 | refreshBinding() 94 | } else { 95 | console.error(`Invalid log level: ${levelLabel}`) 96 | } 97 | } 98 | 99 | refreshBinding() 100 | -------------------------------------------------------------------------------- /src/utils/ReactUtils.ts: -------------------------------------------------------------------------------- 1 | export type ReactInitCallback = (root: HTMLElement, reactProp: string) => Promise | void; 2 | 3 | export class ReactUtils { 4 | 5 | static waitForReactInit( 6 | callback: ReactInitCallback, 7 | checkInterval: number = 500, 8 | timeout: number = 10000 9 | ): Promise { 10 | return new Promise((resolve, reject) => { 11 | const start = Date.now() 12 | 13 | const interval = setInterval(() => { 14 | // 从 HTML 根节点开始检查 15 | const root = document.documentElement 16 | for (const prop in root) { 17 | if (prop.startsWith('__react')) { 18 | clearInterval(interval) 19 | console.log(`React initialized with property: ${prop}`) 20 | // 如果回调是异步的,等待其完成 21 | Promise.resolve(callback(root, prop)) 22 | .then(() => resolve()) 23 | .catch(reject) 24 | return 25 | } 26 | } 27 | 28 | if (Date.now() - start > timeout) { 29 | clearInterval(interval) 30 | reject(new Error('React initialization timeout exceeded.')) 31 | } 32 | }, checkInterval) 33 | }) 34 | } 35 | 36 | static useReact() { 37 | return !!document.querySelector('div[data-react-nav-root]') 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/Strings.ts: -------------------------------------------------------------------------------- 1 | export class Strings { 2 | 3 | static format(format: string, ...args: any[]): string { 4 | args = args || [] 5 | let message = format 6 | for (let arg of args) { 7 | message = message.replace('%s', arg) 8 | } 9 | return message 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | //// 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "useDefineForClassFields": true, 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "lib": [ 8 | "ESNext", 9 | "DOM" 10 | ], 11 | "moduleResolution": "Node", 12 | "strict": true, 13 | "sourceMap": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "esModuleInterop": true, 17 | "noEmit": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "skipLibCheck": true 22 | }, 23 | "include": [ 24 | "src/**/*.ts", 25 | "src/**/*.d.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue" 28 | ], 29 | "references": [ 30 | { 31 | "path": "./tsconfig.node.json" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import monkey, {cdn, util} from 'vite-plugin-monkey' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | vue({ 9 | template: { 10 | compilerOptions: { 11 | // 所有以 mdui- 开头的标签名都是 mdui 组件 12 | isCustomElement: (tag) => /^mdui-/.test(tag) 13 | } 14 | } 15 | }), 16 | monkey({ 17 | entry: 'src/main.ts', 18 | userscript: { 19 | name: 'steam价格转换', 20 | author: 'marioplus', 21 | description: 'steam商店中的价格转换为人民币', 22 | version: '2.5.4', 23 | icon: 'https://vitejs.dev/logo.svg', 24 | namespace: 'https://github.com/marioplus/steam-price-converter', 25 | homepage: 'https://github.com/marioplus', 26 | license: 'GPL-3.0-or-later', 27 | match: [ 28 | 'https://store.steampowered.com/*', 29 | 'https://steamcommunity.com/*', 30 | 'https://checkout.steampowered.com/checkout/*', 31 | ], 32 | connect: [ 33 | 'api.augmentedsteam.com', 34 | 'store.steampowered.com', 35 | 'cdn.jsdelivr.net' 36 | ] 37 | }, 38 | build: { 39 | externalGlobals: { 40 | mdui: cdn.jsdelivr('mdui', 'mdui.global.min.js'), 41 | vue: cdn.jsdelivr('Vue', 'dist/vue.global.prod.js'), 42 | }, 43 | externalResource: { 44 | 'mdui/mdui.css': cdn.jsdelivr(), 45 | } 46 | }, 47 | }), 48 | ] 49 | }) 50 | --------------------------------------------------------------------------------