在SteamPy网站上Steam游戏列表页面添加价格过滤功能,支持按自定义价格区间筛选游戏,自动监控价格变动并高亮显示,同时实现翻页时自动应用筛选条件,帮助快速找到符合预算的游戏。
目前為
// ==UserScript==
// @name SteamPy 游戏Key列表价格过滤器
// @name:en SteamPy Game Key List Price Filter
// @namespace http://github.com/blue-bird1/tampermonkey-script
// @version 3.9
// @description 在SteamPy网站上Steam游戏列表页面添加价格过滤功能,支持按自定义价格区间筛选游戏,自动监控价格变动并高亮显示,同时实现翻页时自动应用筛选条件,帮助快速找到符合预算的游戏。
// @description:en Add price filtering to Steam game lists on SteamPy. Supports custom price range filtering, automatically monitors price changes with highlights, and applies filters automatically when paginating to help find games within budget quickly.
// @description:ja SteamPyのSteamゲームリストに価格フィルター機能を追加します。カスタム価格帯でのフィルタリング、価格変動の自動監視とハイライト表示、ページネーション時の自動フィルター適用に対応し、予算内のゲームをすばやく見つけるのに役立ちます。
// @author 豆包 (Doubao)
// @match https://steampy.com/*
// @grant none
// @icon https://steampy.com/logo.ico
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 状态管理 - 使用localStorage持久化
const StateManager = {
saveState(state) {
try {
localStorage.setItem('steamPriceFilterState', JSON.stringify(state));
} catch (e) {
console.warn('保存筛选状态失败:', e);
}
},
loadState() {
try {
const saved = localStorage.getItem('steamPriceFilterState');
return saved ? JSON.parse(saved) : {
minPrice: 0,
maxPrice: 9999,
isActive: false
};
} catch (e) {
console.warn('加载筛选状态失败:', e);
return {
minPrice: 0,
maxPrice: 9999,
isActive: false
};
}
}
};
// 筛选状态 - 从本地加载
let filterState = StateManager.loadState();
// 基础工具函数
function getSteamAppId(gameBlock) {
const iconImg = gameBlock.querySelector('.cdkGameIcon');
return iconImg?.src ? iconImg.src.match(/steam\/apps\/(\d+)\/header/)?.[1] : null;
}
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;`;
// 标题
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-5元', min: 0, max: 5 },
{ text: '5-20元', min: 5, max: 20 },
{ text: '20-50元', min: 20, max: 50 },
{ text: '50元以上', min: 50, 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);
};
// 重置按钮
const resetBtn = document.createElement('button');
resetBtn.className = 'ivu-btn ivu-btn-default ivu-btn-sm';
resetBtn.textContent = '重置';
resetBtn.style.cssText = `margin-left:4px;padding:4px 12px;cursor:pointer;`;
resetBtn.onclick = () => {
filterState = { minPrice: 0, maxPrice: 9999, isActive: false };
StateManager.saveState(filterState);
minInp.value = '';
maxInp.value = '';
updatePresetHighlights(false);
applyFilter();
};
inputContainer.append(minInp, document.createTextNode('-'), maxInp, filterBtn, resetBtn);
ui.appendChild(inputContainer);
return ui;
}
// 插入UI
function insertFilterUI() {
if (document.getElementById('priceFilterContainer')) return;
const ui = createFilterUI();
const targetContainers = [
'.tag.flex-row.align-items-center',
'.main-content',
'.game-list',
'.games-container'
];
for (const selector of targetContainers) {
const container = document.querySelector(selector);
if (container && window.getComputedStyle(container).display !== 'none') {
selector === '.tag.flex-row.align-items-center'
? container.appendChild(ui)
: container.insertBefore(ui, container.firstChild);
syncInputValues();
console.log(`筛选UI已插入到: ${selector}`);
return;
}
}
document.body.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) {
if (!gameBlock) return;
const price = getGamePrice(gameBlock);
gameBlock.style.display = filterState.isActive
? (price >= filterState.minPrice && price <= filterState.maxPrice) ? 'block' : 'none'
: 'block';
}
// 统一的筛选应用函数
function applyFilter() {
document.querySelectorAll('.gameblock:not([data-filter-processed])')
.forEach(processGame);
document.querySelectorAll('.gameblock').forEach(applyFilterToGame);
const visibleCount = document.querySelectorAll('.gameblock[style="display: block;"]').length;
const emptyMsg = document.querySelector('.tc.mt-50-rem.pb-20-rem');
if (emptyMsg) {
emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
// -------------- 恢复原始价格监控逻辑 + 翻页检测 --------------
function startGameElementMonitor() {
// 找到稳定的根容器(使用原始选择器)
const stableRoot = document.querySelector('.ivu-tabs-content') ||
document.querySelector('.game-list') ||
document.body;
if (!stableRoot) {
console.warn('未找到稳定根容器,1秒后重试');
setTimeout(startGameElementMonitor, 1000);
return;
}
// 恢复原始监控配置(专注于内容和价格变化)
const observerConfig = {
childList: true, // 监控子节点变化
subtree: true, // 监控所有后代
characterData: true, // 监控文本内容变化
characterDataOldValue: true,
attributes: true, // 增加属性监控以检测翻页
attributeFilter: ['style', 'class', 'href'] // 监控可能的翻页属性变化
};
// 价格和内容变化监控器(恢复原始逻辑)
const priceObserver = new MutationObserver((mutations) => {
let contentChanged = false;
mutations.forEach(mutation => {
// 1. 处理价格文本变化(原始逻辑)
if (mutation.type === 'characterData') {
const priceElement = mutation.target.parentElement;
if (priceElement && priceElement.classList.contains('gamePrice')) {
const gameBlock = priceElement.closest('.gameblock');
if (gameBlock) {
setTimeout(() => {
applyFilterToGame(gameBlock);
}, 0);
const oldValue = mutation.oldValue?.trim() || '';
const newValue = mutation.target.data?.trim() || '';
console.log(`价格变化: ${oldValue} -> ${newValue}`);
}
}
}
// 2. 检测新游戏元素添加(用于翻页检测)
if (mutation.type === 'childList') {
const hasNewGames = Array.from(mutation.addedNodes).some(node =>
node.classList?.contains('gameblock') ||
node.querySelector?.('.gameblock')
);
// 检测翻页控件变化
const hasPaginationChange = Array.from(mutation.addedNodes).some(node =>
node.classList?.contains('pagination') ||
node.querySelector?.('.pagination')
);
if (hasNewGames || hasPaginationChange) {
contentChanged = true;
}
}
});
// 检查UI是否需要恢复
const gameListExists = document.querySelector('.gameblock') || document.querySelector('.game-list');
const uiNeedsRecovery = gameListExists && !document.getElementById('priceFilterContainer');
if (uiNeedsRecovery) {
insertFilterUI();
applyFilter();
} else if (contentChanged) {
// 内容变化(包括翻页)时重新应用筛选
applyFilter();
}
});
// 启动监控(使用原始的稳定根容器)
priceObserver.observe(stableRoot, observerConfig);
console.log('价格监控系统已启动,基于原始逻辑实现');
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
priceObserver.disconnect();
});
// 初始处理已加载的游戏
setTimeout(applyFilter, 500);
}
const TARGET_PATH = '/cdKey/cdKey';
let isInitialized = false;
// 检查当前路径是否匹配目标路径
function isTargetPath() {
return window.location.pathname === TARGET_PATH;
}
// 清理函数(当离开目标路径时)
function cleanUp() {
if (!isInitialized) return;
console.log('离开目标路径,清理脚本...');
// 这里可以添加需要清理的逻辑,如移除事件监听等
isInitialized = false;
}
// 处理路径变化
function handlePathChange() {
if (isTargetPath()) {
init();
} else {
cleanUp();
}
}
// 初始化函数
function init() {
if (isInitialized) return;
console.log('在目标路径下,初始化脚本...');
waitForElement('.tag.flex-row.align-items-center', insertFilterUI);
startGameElementMonitor();
isInitialized = true;
}
// 监听URL变化(适用于大多数SPA框架)
window.addEventListener('popstate', handlePathChange);
// 某些框架可能使用hash变化,也可以监听hashchange事件
window.addEventListener('hashchange', handlePathChange);
init();
})();