SteamDB Sales CNPrice Injector

Adds CNPrice column to SteamDB sales page with selectable currency conversions.

  1. // ==UserScript==
  2. // @name SteamDB Sales CNPrice Injector
  3. // @namespace https://liangying.eu.org/
  4. // @version 1.1.0
  5. // @description Adds CNPrice column to SteamDB sales page with selectable currency conversions.
  6. // @author LiangYing
  7. // @match https://steamdb.info/sales/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @connect store.steampowered.com
  12. // @connect api.exchangerate-api.com
  13. // @icon https://store.steampowered.com/favicon.ico
  14. // @license MIT
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const COLUMN_CLASS = 'compare-price-column';
  22. const PRICE_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24小时的缓存时间
  23.  
  24. // 货币符号映射
  25. const CURRENCY_SYMBOLS = {
  26. CNY: '¥',
  27. JPY: '¥',
  28. HKD: 'HK$',
  29. USD: '$',
  30. RUB: '₽',
  31. PHP: '₱',
  32. INR: '₹',
  33. KRW: '₩',
  34. CAD: 'C$'
  35. };
  36.  
  37. // 汇率对象 - 存储1 CNY兑换多少目标货币
  38. let exchangeRates = {
  39. CNY: 1,
  40. JPY: 16.5, // 1 CNY = 16.5 JPY
  41. HKD: 1.09, // 1 CNY = 1.09 HKD
  42. USD: 0.14, // 1 CNY = 0.14 USD
  43. RUB: 12.7, // 1 CNY = 12.7 RUB
  44. PHP: 7.74, // 1 CNY = 7.74 PHP
  45. INR: 11.52, // 1 CNY = 11.52 INR
  46. KRW: 185.87, // 1 CNY = 185.87 KRW
  47. CAD: 0.19 // 1 CNY = 0.19 CAD
  48. };
  49.  
  50. // 当前选择的货币
  51. let currentCurrency = null;
  52.  
  53. // 价格缓存
  54. const priceCache = {
  55. // 获取缓存的价格
  56. get: function (appId) {
  57. const cached = GM_getValue(`price_${appId}`);
  58. if (!cached) return null;
  59.  
  60. const { timestamp, data } = JSON.parse(cached);
  61. if (Date.now() - timestamp > PRICE_CACHE_DURATION) {
  62. GM_setValue(`price_${appId}`, '');
  63. return null;
  64. }
  65. return data;
  66. },
  67.  
  68. // 设置价格缓存
  69. set: function (appId, priceData) {
  70. const cacheData = {
  71. timestamp: Date.now(),
  72. data: priceData
  73. };
  74. GM_setValue(`price_${appId}`, JSON.stringify(cacheData));
  75. }
  76. };
  77.  
  78. // 创建UI元素
  79. function createUI() {
  80. // 尝试找到现有的容器
  81. const existingContainer = document.querySelector('.dt-layout-end');
  82.  
  83. if (existingContainer && !existingContainer.querySelector('.currency-selector')) {
  84. const rateSelect = document.createElement('select');
  85. rateSelect.className = 'currency-selector';
  86. rateSelect.innerHTML = `
  87. <option value="">-- LiangYing Exchange --</option>
  88. <option value="CNY">CNY (中国)</option>
  89. <option value="JPY">JPY (日本)</option>
  90. <option value="HKD">HKD (香港)</option>
  91. <option value="USD">USD (美国)</option>
  92. <option value="RUB">RUB (俄罗斯)</option>
  93. <option value="PHP">PHP (菲律宾)</option>
  94. <option value="INR">INR (印度)</option>
  95. <option value="KRW">KRW (韩国)</option>
  96. <option value="CAD">CAD (加拿大)</option>
  97. `;
  98.  
  99. rateSelect.style.marginLeft = '10px';
  100. rateSelect.style.padding = '5px';
  101. rateSelect.style.backgroundColor = '#1b2838';
  102. rateSelect.style.color = '#c6d4df';
  103. rateSelect.style.border = '1px solid #2a475e';
  104.  
  105. existingContainer.appendChild(rateSelect);
  106.  
  107. rateSelect.addEventListener('change', function () {
  108. currentCurrency = this.value;
  109. if (!currentCurrency) {
  110. removePriceColumn();
  111. return;
  112. }
  113.  
  114. updateExchangeRates(() => {
  115. ensurePriceColumn();
  116. refreshPrices();
  117. });
  118. });
  119. return;
  120. }
  121.  
  122. // 创建新的容器
  123. if (!document.querySelector('.currency-selector')) {
  124. const controlContainer = document.createElement('div');
  125. controlContainer.className = 'currency-selector-container';
  126. controlContainer.style.margin = '10px 0';
  127. controlContainer.style.textAlign = 'right';
  128.  
  129. const rateSelect = document.createElement('select');
  130. rateSelect.className = 'currency-selector';
  131. rateSelect.innerHTML = `
  132. <option value="">-- LiangYing Exchange --</option>
  133. <option value="CNY">CNY (中国)</option>
  134. <option value="JPY">JPY (日本)</option>
  135. <option value="HKD">HKD (香港)</option>
  136. <option value="USD">USD (美国)</option>
  137. <option value="RUB">RUB (俄罗斯)</option>
  138. <option value="PHP">PHP (菲律宾)</option>
  139. <option value="INR">INR (印度)</option>
  140. <option value="KRW">KRW (韩国)</option>
  141. <option value="CAD">CAD (加拿大)</option>
  142. `;
  143.  
  144. rateSelect.style.marginLeft = '10px';
  145. rateSelect.style.padding = '5px';
  146. rateSelect.style.backgroundColor = '#1b2838';
  147. rateSelect.style.color = '#c6d4df';
  148. rateSelect.style.border = '1px solid #2a475e';
  149.  
  150. controlContainer.appendChild(rateSelect);
  151.  
  152. // 插入UI元素
  153. const tableElement = document.querySelector('.table-sales') ||
  154. document.querySelector('.dataTable');
  155.  
  156. if (tableElement && tableElement.parentNode) {
  157. tableElement.parentNode.insertBefore(controlContainer, tableElement);
  158. } else {
  159. const tableContainer = document.querySelector('.table-container') ||
  160. document.querySelector('.table-responsive');
  161. if (tableContainer) {
  162. tableContainer.insertBefore(controlContainer, tableContainer.firstChild);
  163. }
  164. }
  165.  
  166. rateSelect.addEventListener('change', function () {
  167. currentCurrency = this.value;
  168. if (!currentCurrency) {
  169. removePriceColumn();
  170. return;
  171. }
  172.  
  173. updateExchangeRates(() => {
  174. ensurePriceColumn();
  175. refreshPrices();
  176. });
  177. });
  178. }
  179. }
  180.  
  181. // 更新汇率数据
  182. function updateExchangeRates(callback) {
  183. GM_xmlhttpRequest({
  184. method: 'GET',
  185. url: 'https://api.exchangerate-api.com/v4/latest/CNY',
  186. onload: function (response) {
  187. if (response.status === 200) {
  188. try {
  189. const data = JSON.parse(response.responseText);
  190. exchangeRates.JPY = data.rates.JPY;
  191. exchangeRates.HKD = data.rates.HKD;
  192. exchangeRates.USD = data.rates.USD;
  193. exchangeRates.RUB = data.rates.RUB;
  194. exchangeRates.PHP = data.rates.PHP;
  195. exchangeRates.INR = data.rates.INR;
  196. exchangeRates.KRW = data.rates.KRW;
  197. exchangeRates.CAD = data.rates.CAD;
  198.  
  199. if (callback) callback();
  200. } catch (error) {
  201. console.error('Failed to parse exchange rates, using defaults:', error);
  202. if (callback) callback();
  203. }
  204. } else {
  205. console.error('Failed to fetch exchange rates, using defaults:', response.status);
  206. if (callback) callback();
  207. }
  208. },
  209. onerror: function() {
  210. console.error('Failed to fetch exchange rates, using defaults');
  211. if (callback) callback();
  212. }
  213. });
  214. }
  215.  
  216. // 解析价格
  217. function parsePrice(priceStr) {
  218. return parseFloat(priceStr.replace(/[^0-9.]/g, '')) || 0;
  219. }
  220.  
  221. // 获取商店价格
  222. function fetchGamePrice(appId, callback, retryCount = 0) {
  223. const maxRetries = 3;
  224.  
  225. // 先检查缓存
  226. const cachedPrice = priceCache.get(appId);
  227. if (cachedPrice) {
  228. callback(cachedPrice);
  229. return;
  230. }
  231.  
  232. GM_xmlhttpRequest({
  233. method: 'GET',
  234. url: `https://store.steampowered.com/api/appdetails/?appids=${appId}&cc=cn`,
  235. timeout: 10000,
  236. onload: function (response) {
  237. try {
  238. const data = JSON.parse(response.responseText);
  239. if (data[appId]?.success) {
  240. const priceInfo = data[appId].data.price_overview;
  241. if (priceInfo) {
  242. priceCache.set(appId, priceInfo);
  243. }
  244. callback(priceInfo);
  245. } else {
  246. callback(null);
  247. }
  248. } catch (error) {
  249. if (retryCount < maxRetries) {
  250. setTimeout(() => {
  251. fetchGamePrice(appId, callback, retryCount + 1);
  252. }, 2000 * (retryCount + 1));
  253. } else {
  254. callback(null);
  255. }
  256. }
  257. },
  258. onerror: function () {
  259. if (retryCount < maxRetries) {
  260. setTimeout(() => {
  261. fetchGamePrice(appId, callback, retryCount + 1);
  262. }, 2000 * (retryCount + 1));
  263. } else {
  264. callback(null);
  265. }
  266. }
  267. });
  268. }
  269.  
  270. // 确保价格列存在
  271. function ensurePriceColumn() {
  272. if (!currentCurrency) return;
  273.  
  274. const header = document.querySelector('.table-sales thead tr, .dataTable thead tr');
  275. if (!header) return;
  276.  
  277. let priceHeader = header.querySelector(`.${COLUMN_CLASS}`);
  278. if (!priceHeader) {
  279. priceHeader = document.createElement('th');
  280. priceHeader.className = COLUMN_CLASS;
  281. header.appendChild(priceHeader);
  282. }
  283.  
  284. // 更新列标题
  285. const symbol = CURRENCY_SYMBOLS[currentCurrency] || currentCurrency;
  286. priceHeader.textContent = `${symbol} Price Comparison`;
  287. priceHeader.style.whiteSpace = 'nowrap';
  288.  
  289. const rows = document.querySelectorAll('.table-sales tbody tr, .dataTable tbody tr');
  290. rows.forEach(row => {
  291. if (!row.querySelector(`.${COLUMN_CLASS}`)) {
  292. const priceCell = document.createElement('td');
  293. priceCell.className = COLUMN_CLASS;
  294. row.appendChild(priceCell);
  295. }
  296. });
  297. }
  298.  
  299. // 移除价格列
  300. function removePriceColumn() {
  301. const header = document.querySelector('.table-sales thead tr, .dataTable thead tr');
  302. if (!header) return;
  303.  
  304. const priceHeader = header.querySelector(`.${COLUMN_CLASS}`);
  305. if (priceHeader) {
  306. priceHeader.remove();
  307. }
  308.  
  309. const rows = document.querySelectorAll('.table-sales tbody tr, .dataTable tbody tr');
  310. rows.forEach(row => {
  311. const priceCell = row.querySelector(`.${COLUMN_CLASS}`);
  312. if (priceCell) {
  313. priceCell.remove();
  314. }
  315. });
  316. }
  317.  
  318. // 更新单个游戏的价格显示
  319. function updateGamePrice(row) {
  320. if (!currentCurrency) return;
  321.  
  322. // 获取游戏ID
  323. const appId = row.dataset.appid;
  324. if (!appId) return;
  325.  
  326. // 找到价格单元格
  327. let priceCell = row.querySelector(`.${COLUMN_CLASS}`);
  328. if (!priceCell) {
  329. priceCell = document.createElement('td');
  330. priceCell.className = COLUMN_CLASS;
  331. row.appendChild(priceCell);
  332. }
  333.  
  334. // 如果已经有价格数据,则跳过
  335. if (priceCell.textContent && !priceCell.textContent.includes('Loading')) {
  336. return;
  337. }
  338.  
  339. priceCell.textContent = 'Loading...';
  340.  
  341. // 从SteamDB表格中获取目标货币价格(第5列)
  342. const targetCurrencyPriceElement = row.querySelector('td:nth-child(5)');
  343. const targetCurrencyPrice = targetCurrencyPriceElement ?
  344. parsePrice(targetCurrencyPriceElement.textContent) : 0;
  345.  
  346. if (!targetCurrencyPrice) {
  347. priceCell.textContent = 'N/A';
  348. return;
  349. }
  350.  
  351. fetchGamePrice(appId, (priceInfo) => {
  352. if (priceInfo) {
  353. // 中国区价格(人民币)
  354. const cnPrice = priceInfo.final / 100;
  355.  
  356. // 汇率:1 CNY = X 目标货币
  357. const exchangeRate = exchangeRates[currentCurrency];
  358.  
  359. // 转换后的目标货币价格
  360. const convertedPrice = cnPrice * exchangeRate;
  361.  
  362. // 计算比例:转换后价格 / SteamDB显示的目标货币价格
  363. const ratio = targetCurrencyPrice > 0 ?
  364. (convertedPrice / targetCurrencyPrice * 100).toFixed(2) : 'N/A';
  365.  
  366. // 设置颜色
  367. const color = (ratio < 100) ? '#5cff47' :
  368. (ratio > 100) ? '#ff4747' : '#ccc';
  369.  
  370. // 获取货币符号
  371. const targetSymbol = CURRENCY_SYMBOLS[currentCurrency] || currentCurrency;
  372. const cnySymbol = CURRENCY_SYMBOLS.CNY;
  373.  
  374. // 更新单元格内容
  375. priceCell.innerHTML = `
  376. <div>${cnySymbol}${cnPrice.toFixed(2)}</div>
  377. <div>${targetSymbol}${convertedPrice.toFixed(2)}</div>
  378. <div style="color: ${color}; font-weight: bold">${ratio}%</div>
  379. `;
  380. } else {
  381. priceCell.textContent = 'N/A';
  382. }
  383. });
  384. }
  385.  
  386. // 刷新所有价格
  387. function refreshPrices() {
  388. if (!currentCurrency) return;
  389.  
  390. ensurePriceColumn();
  391.  
  392. const rows = document.querySelectorAll('.table-sales tbody tr, .dataTable tbody tr');
  393. rows.forEach((row, index) => {
  394. setTimeout(() => {
  395. try {
  396. updateGamePrice(row);
  397. } catch (error) {
  398. console.error(`Error updating price for row ${index}:`, error);
  399. }
  400. }, index * 300);
  401. });
  402. }
  403.  
  404. // 监听表格变化
  405. function setupTableObserver() {
  406. const tableBody = document.querySelector('.table-sales tbody, .dataTable tbody');
  407. if (tableBody) {
  408. const tableObserver = new MutationObserver((mutations) => {
  409. for (const mutation of mutations) {
  410. if (mutation.type === 'childList' && currentCurrency) {
  411. ensurePriceColumn();
  412. const newRows = Array.from(mutation.addedNodes).filter(node =>
  413. node.nodeType === 1 && node.matches('tr')
  414. );
  415. newRows.forEach(updateGamePrice);
  416. }
  417. }
  418. });
  419.  
  420. tableObserver.observe(tableBody, {
  421. childList: true,
  422. subtree: true
  423. });
  424. }
  425.  
  426. // 监听分页变化
  427. const paginationContainer = document.querySelector('.pagination, .dataTables_paginate');
  428. if (paginationContainer) {
  429. const paginationObserver = new MutationObserver(() => {
  430. if (currentCurrency) {
  431. ensurePriceColumn();
  432. refreshPrices();
  433. }
  434. });
  435.  
  436. paginationObserver.observe(paginationContainer, {
  437. childList: true,
  438. subtree: true
  439. });
  440. }
  441.  
  442. // 筛选表单监听
  443. const filterForm = document.getElementById('js-filters');
  444. if (filterForm) {
  445. filterForm.addEventListener('submit', () => {
  446. setTimeout(() => {
  447. if (currentCurrency) {
  448. ensurePriceColumn();
  449. refreshPrices();
  450. }
  451. }, 500);
  452. });
  453. }
  454. }
  455.  
  456. // 初始化
  457. function init() {
  458. createUI();
  459. setupTableObserver();
  460. }
  461.  
  462. // 等待页面加载完成后初始化
  463. if (document.readyState === 'loading') {
  464. document.addEventListener('DOMContentLoaded', init);
  465. } else {
  466. init();
  467. }
  468. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址