您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增强购买Steampy密钥的体验,增加筛选功能,支持鼠标中键打开Steam页面。
// ==UserScript== // @name SteamPy Plus // @name:zh-CN SteamPy Plus // @name:en SteamPy Plus // @namespace http://github.com/blue-bird1/tampermonkey-script // @version 4.10 // @description 增强购买Steampy密钥的体验,增加筛选功能,支持鼠标中键打开Steam页面。 // @description:en Enhance the experience of purchasing Steampy keys, add filter functionality, and support opening Steam pages with the middle mouse button. // 英文描述(可选,补充默认描述的英文版本) // @author 豆包 (Doubao) // @match https://steampy.com/* // @grant GM_setValue // @grant GM_getValue // @icon https://steampy.com/logo.ico // @require https://scriptcat.org/lib/637/1.4.8/ajaxHooker.js#sha256=dTF50feumqJW36kBpbf6+LguSLAtLr7CEs3oPmyfbiM= // @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U= // @run-at document-start // @license MIT // ==/UserScript== /*global elmGetter,ajaxHooker*/ (function () { 'use strict'; // 状态管理 const StateManager = { saveState(state) { GM_setValue('steamPriceFilterState', JSON.stringify(state)); }, loadState() { const saved = GM_getValue('steamPriceFilterState', null); return saved ? JSON.parse(saved) : { minPrice: 0, maxPrice: 9999, isActive: false }; } }; let filterState = StateManager.loadState(); // 修复后的工具函数:提取游戏ID(优先读取data-src) function getSteamAppId(gameBlock) { const iconImg = gameBlock.querySelector('.cdkGameIcon'); if (!iconImg) return null; // 优先读取真实图片地址(data-src),再兼容src const imgUrl = iconImg.dataset.src || iconImg.src; // 从图片地址中匹配游戏ID(例如从steam/apps/1651560/中提取1651560) const match = imgUrl.match(/steam\/apps\/(\d+)\/header/); return match ? match[1] : null; } // 游戏数据存储 const TempDataStore = { steamGameData: null, setGameData(data) { this.steamGameData = data; }, getGameData() { return this.steamGameData || { result: { content: [] } }; }, getRatingByAppId(appId) { const gameList = this.getGameData().result.content; const targetGame = gameList.find(game => game.appId === appId); return targetGame?.rating || 0; } }; // 接口拦截 ajaxHooker.hook(request => { if (request.url.includes('/xboot/steamGame/keyHot')) { request.response = (res) => { try { const originalData = JSON.parse(res.responseText); TempDataStore.setGameData(originalData); res.responseText = JSON.stringify(originalData); } catch (e) { console.error('接口数据处理失败:', e); } }; } return request; }); // 单个游戏评分更新(使用Steam风格文本描述) function updateGameRating(gameBlock) { if (!gameBlock) return; const appId = getSteamAppId(gameBlock); const gameHead = gameBlock.querySelector('.gameHead'); // 只有存在有效ID时才处理评分 if (appId && gameHead) { const rating = TempDataStore.getRatingByAppId(appId); const ratingEl = gameHead.querySelector('.gameRating'); // 有评分数据 if (rating > 0) { // 计算百分比并映射到Steam评分等级 const ratingPercent = Math.round(rating * 100); let ratingText, ratingClass; // Steam风格评分标准 if (ratingPercent >= 90) { ratingText = "好评如潮"; ratingClass = "overwhelmingly-positive"; } else if (ratingPercent >= 80) { ratingText = "特别好评"; ratingClass = "very-positive"; } else if (ratingPercent >= 70) { ratingText = "多半好评"; ratingClass = "positive"; } else if (ratingPercent >= 40) { ratingText = "褒贬不一"; ratingClass = "mixed"; } else if (ratingPercent >= 20) { ratingText = "多半差评"; ratingClass = "negative"; } else { ratingText = "特别差评"; ratingClass = "very-negative"; } if (ratingEl) { // 只在内容变化时更新 if (ratingEl.textContent !== ratingText) { ratingEl.textContent = ratingText; } // 更新评分等级类名 if (!ratingEl.classList.contains(ratingClass)) { ratingEl.classList.remove( 'overwhelmingly-positive', 'very-positive', 'positive', 'mixed', 'negative', 'very-negative' ); ratingEl.classList.add(ratingClass); } } else { // 创建新评分标签 const newRatingEl = document.createElement('div'); newRatingEl.className = `gameRating ${ratingClass}`; newRatingEl.textContent = ratingText; gameHead.appendChild(newRatingEl); } } // 无评分数据则移除标签 else if (ratingEl) { ratingEl.remove(); } } // 无ID时移除现有评分标签 else if (gameHead) { const ratingEl = gameHead.querySelector('.gameRating'); if (ratingEl) ratingEl.remove(); } } // 同步更新评分样式 function injectRatingStyle() { const existingStyle = document.getElementById('ratingStyle'); if (existingStyle) { existingStyle.remove(); } const style = document.createElement('style'); style.id = 'ratingStyle'; style.textContent = ` .gameHead .gameRating { padding: 0 8px !important; height: .3rem !important; position: absolute !important; top: 0 !important; left: 0 !important; color: #fff !important; text-align: center !important; line-height: .3rem !important; border-radius: .09rem 0 0 0 !important; font-size: .12rem !important; font-weight: bold !important; z-index: 10 !important; white-space: nowrap !important; } /* Steam风格评分颜色 */ .gameRating.overwhelmingly-positive { background: #4CAF50 !important; } /* 好评如潮 - 深绿 */ .gameRating.very-positive { background: #8BC34A !important; } /* 特别好评 - 中绿 */ .gameRating.positive { background: #CDDC39 !important; color: #333 !important; } /* 多半好评 - 浅绿 */ .gameRating.mixed { background: #FFC107 !important; color: #333 !important; } /* 褒贬不一 - 黄色 */ .gameRating.negative { background: #FF9800 !important; } /* 多半差评 - 橙色 */ .gameRating.very-negative { background: #F44336 !important; } /* 特别差评 - 红色 */ `; document.head.appendChild(style); } // 等待元素加载 function waitForElement(selector, callback, timeout = 10000) { const start = Date.now(); const timer = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(timer); callback(el); } else if (Date.now() - start > timeout) { clearInterval(timer); console.warn(`超时未找到元素: ${selector}`); insertFilterUI(); } }, 200); } // 筛选UI function createFilterUI() { const ui = document.createElement('div'); ui.id = 'priceFilterContainer'; ui.className = 'ml-5-rem c-point tagBtnTwo flex-row align-items-center'; ui.style.cssText = `font-family:Arial,sans-serif;font-size:13px;align-items:center;gap:8px;padding:8px;z-index:9999;position:relative;background:#f9f9f9;border-radius:4px;border:1px solid #eee;height:.25rem;`; const title = document.createElement('span'); title.className = 'tag-titleOne ml-3-rem'; title.textContent = '价格筛选'; title.style.fontWeight = 'bold'; ui.appendChild(title); const presets = [ { text: '0-20元', min: 0, max: 20 }, { text: '20元以上', min: 20, max: 9999 } ]; const presetContainer = document.createElement('div'); presetContainer.className = 'flex-row jc-space-flex-start align-items-center pr5-rem'; presetContainer.style.gap = '8px'; presets.forEach(p => { const btn = document.createElement('div'); btn.className = 'tagBtn'; btn.dataset.min = p.min; btn.dataset.max = p.max; btn.textContent = p.text; btn.style.cssText = `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #ddd;color:#666;background:transparent;transition:all 0.2s;`; if (filterState.isActive && filterState.minPrice === p.min && filterState.maxPrice === p.max) { btn.style.cssText = `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #409EFF;color:#fff;background:#409EFF;transition:all 0.2s;`; } btn.onclick = () => { filterState.minPrice = p.min; filterState.maxPrice = p.max; filterState.isActive = true; StateManager.saveState(filterState); syncInputValues(); applyFilter(); // 仅应用筛选,不更新评分 updatePresetHighlights(); }; presetContainer.appendChild(btn); }); ui.appendChild(presetContainer); const inputContainer = document.createElement('div'); inputContainer.className = 'flex-row align-items-center'; inputContainer.style.gap = '8px'; const minInp = document.createElement('input'); minInp.id = 'priceFilterMin'; minInp.type = 'number'; minInp.placeholder = '最低价'; minInp.min = 0; minInp.step = 0.01; minInp.style.cssText = `width:70px;height:28px;padding:0 8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:13px;`; minInp.addEventListener('input', (e) => { filterState.minPrice = parseFloat(e.target.value) || 0; filterState.isActive = true; StateManager.saveState(filterState); }); const maxInp = document.createElement('input'); maxInp.id = 'priceFilterMax'; maxInp.type = 'number'; maxInp.placeholder = '最高价'; maxInp.min = 0; maxInp.step = 0.01; maxInp.style.cssText = `width:70px;height:28px;padding:0 8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:13px;`; maxInp.addEventListener('input', (e) => { filterState.maxPrice = parseFloat(e.target.value) || 9999; filterState.isActive = true; StateManager.saveState(filterState); }); const filterBtn = document.createElement('button'); filterBtn.className = 'ivu-btn ivu-btn-default ivu-btn-sm'; filterBtn.textContent = '筛选'; filterBtn.style.cssText = `margin-left:4px;padding:4px 12px;cursor:pointer;background:#409EFF;color:white;border:1px solid #409EFF;border-radius:4px;`; filterBtn.onclick = () => { applyFilter(); // 仅应用筛选,不更新评分 updatePresetHighlights(false); }; inputContainer.append(minInp, document.createTextNode('-'), maxInp, filterBtn); ui.appendChild(inputContainer); return ui; } function insertFilterUI() { if (document.getElementById('priceFilterContainer')) return; const ui = createFilterUI(); const targetContainer = document.querySelector('.tag.flex-row.align-items-center'); if (targetContainer) { targetContainer.appendChild(ui); syncInputValues(); } if (filterState.isActive) { applyFilter(); } } function updatePresetHighlights(shouldHighlight = true) { document.querySelectorAll('.tagBtn[data-min]').forEach(btn => { const btnMin = parseFloat(btn.dataset.min); const btnMax = parseFloat(btn.dataset.max); const isMatch = filterState.isActive && filterState.minPrice === btnMin && filterState.maxPrice === btnMax; btn.style.cssText = shouldHighlight && isMatch ? `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #409EFF;color:#fff;background:#409EFF;transition:all 0.2s;` : `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #ddd;color:#666;background:transparent;transition:all 0.2s;`; }); } // 价格筛选核心逻辑 function syncInputValues() { const minInp = document.getElementById('priceFilterMin'); const maxInp = document.getElementById('priceFilterMax'); if (minInp && filterState.isActive) minInp.value = filterState.minPrice; if (maxInp && filterState.isActive) maxInp.value = filterState.maxPrice; } function getGamePrice(gameBlock) { const priceEl = gameBlock.querySelector('.gamePrice'); if (!priceEl) return 0; const priceText = priceEl.textContent.replace(/[¥元]/g, '').trim().toLowerCase(); return priceText === '免费' ? 0 : (parseFloat(priceText) || 0); } function processGame(gameBlock) { if (gameBlock.dataset.filterProcessed) return; gameBlock.dataset.filterProcessed = 'true'; gameBlock.addEventListener('mousedown', e => { if (e.button === 1 && !e.ctrlKey && !e.shiftKey) { const appId = getSteamAppId(gameBlock); if (appId) { e.preventDefault(); window.open(`https://store.steampowered.com/app/${appId}/`, '_blank'); } } }); applyFilterToGame(gameBlock); } // 应用筛选到单个游戏(仅在状态变化且变为可见时才更新评分) function applyFilterToGame(gameBlock, forceRatingUpdate = false) { if (!gameBlock) return false; const price = getGamePrice(gameBlock); const shouldShow = !filterState.isActive || (price >= filterState.minPrice && price <= filterState.maxPrice); const wasShowing = gameBlock.style.display !== 'none'; // 状态变化时才更新DOM if (shouldShow !== wasShowing) { gameBlock.style.display = shouldShow ? 'block' : 'none'; // 只有变为可见状态时才可能需要更新评分 if (shouldShow) { updateGameRating(gameBlock); } return true; } // 强制更新评分(用于ID变化的情况) if (forceRatingUpdate && shouldShow) { updateGameRating(gameBlock); } return false; } // 批量应用筛选(不主动更新评分) function applyFilter() { document.querySelectorAll('.gameblock:not([data-filter-processed])') .forEach(processGame); let hasChanges = false; document.querySelectorAll('.gameblock').forEach(gameBlock => { if (applyFilterToGame(gameBlock)) { hasChanges = true; } }); // 处理空状态显示 const visibleCount = 35 - document.querySelectorAll('.gameblock[style="display: none;"]').length; const emptyMsg = document.querySelector('.tc.mt-50-rem.pb-20-rem'); if (emptyMsg) { emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none'; } return hasChanges; } // 精准监控器:仅在游戏ID变化时更新评分 async function startContentMonitor() { const gameContainer = await elmGetter.get('.ivu-tabs-content'); if (!gameContainer) { console.warn('未找到游戏容器,1秒后重试'); setTimeout(startContentMonitor, 1000); return; } const observerConfig = { subtree: true, attributes: true, attributeFilter: ['src', 'style'], characterData: true }; const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { let gameBlock = null; let requiresRatingUpdate = false; // 游戏图标变化(ID可能改变)- 需要更新评分 if (mutation.type === 'attributes' && mutation.attributeName === 'src') { if (mutation.target.classList?.contains('cdkGameIcon')) { gameBlock = mutation.target.closest('.gameblock'); requiresRatingUpdate = true; // 图片变化意味着ID可能变化 } } // 价格文本变化 - 只影响筛选,不更新评分 else if (mutation.type === 'characterData') { const priceEl = mutation.target.parentElement; if (priceEl && priceEl.classList.contains('gamePrice')) { gameBlock = priceEl.closest('.gameblock'); requiresRatingUpdate = false; // 价格变化不影响评分 } } // 处理找到的游戏块 if (gameBlock) { if (requiresRatingUpdate) { // ID变化或变为可见,需要更新评分 applyFilterToGame(gameBlock, true); } else { // 仅应用筛选,不更新评分 applyFilterToGame(gameBlock, false); } } }); }); observer.observe(gameContainer, observerConfig); console.log('监控器已启动'); window.addEventListener('beforeunload', () => { observer.disconnect(); }); // 初始加载后执行一次 setTimeout(() => { applyFilter(); // 初始加载时对所有可见游戏更新评分 document.querySelectorAll('.gameblock') .forEach(gameBlock => updateGameRating(gameBlock)); }, 600); } // 路径处理 const TARGET_PATH = '/cdKey/cdKey'; let isInitialized = false; function isTargetPath() { return window.location.pathname.startsWith(TARGET_PATH); } function cleanUp() { if (!isInitialized) return; isInitialized = false; } function handlePathChange() { if (isTargetPath() && !isInitialized) { console.log("run script in path") init(); } else if (!isTargetPath() && isInitialized) { cleanUp(); } } async function init() { if (isInitialized) return; injectRatingStyle(); waitForElement('.tag.flex-row.align-items-center', insertFilterUI); await startContentMonitor(); isInitialized = true; } // 监听历史变化 let lastPath = location.pathname + location.search; const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { originalPushState.apply(this, args); const newPath = location.pathname + location.search; if (newPath !== lastPath) { lastPath = newPath; handlePathChange(); } }; history.replaceState = function (...args) { originalReplaceState.apply(this, args); const newPath = location.pathname + location.search; if (newPath !== lastPath) { lastPath = newPath; handlePathChange(); } }; window.addEventListener('popstate', handlePathChange); window.addEventListener('hashchange', handlePathChange); // 初始检查 if (isTargetPath()) { init(); } })();