在SteamPy网站上Steam游戏列表页面添加价格过滤功能,支持按自定义价格区间筛选游戏,自动监控价格变动并高亮显示,同时实现翻页时自动应用筛选条件,帮助快速找到符合预算的游戏。
目前為
// ==UserScript==
// @name SteamPy 游戏列表价格过滤器
// @name:en SteamPy Game List Price Filter
// @namespace http://github.com/blue-bird1/tampermonkey-script
// @version 4.0 // 版本号迭代,标记功能变更
// @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;height:.25rem;`;
// 标题
const title = document.createElement('span');
title.className = 'tag-titleOne ml-3-rem';
title.textContent = '价格筛选';
title.style.fontWeight = 'bold';
ui.appendChild(title);
// 预设按钮组(核心修改:删除2个按钮,调整剩余按钮范围以覆盖原区间)
const presets = [
{ text: '0-20元', min: 0, max: 20 }, // 覆盖原「0-5元」「5-20元」区间
{ text: '20元以上', min: 20, max: 9999 } // 覆盖原「20-50元」「50元以上」区间
];
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);
};
// 核心修改:移除重置按钮相关代码(不再append resetBtn)
inputContainer.append(minInp, document.createTextNode('-'), maxInp, filterBtn);
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 => {
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}`);
}
}
}
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;
}
}
});
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.startsWith(TARGET_PATH);
}
function cleanUp() {
if (!isInitialized) return;
console.log('离开目标路径,清理脚本...');
isInitialized = false;
}
function handlePathChange() {
console.log('切换到路径'+window.location.pathname);
if (isTargetPath()) {
init();
} else {
cleanUp();
}
}
function init() {
if (isInitialized) return;
console.log('在目标路径下,初始化脚本...');
waitForElement('.tag.flex-row.align-items-center', insertFilterUI);
startGameElementMonitor();
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) {
handlePathChange('pushState', lastPath, newPath);
lastPath = newPath;
}
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
const newPath = location.pathname + location.search;
if (newPath !== lastPath) {
handlePathChange('replaceState', lastPath, newPath);
lastPath = newPath;
}
};
window.addEventListener('popstate', handlePathChange);
window.addEventListener('hashchange', handlePathChange);
if(isTargetPath()){
init();
}
})();