// ==UserScript== // @name Steam 徽章卡片视图 Pro // @namespace https://github.com/steam-badges-card-view // @version 1.5.5 // @description 将Steam徽章页面改为卡片网格展示,支持社区市场价格、徽章价值分析、未解锁徽章预览、智能分析浮窗、可合卡勋章标记、全部卡牌游戏列表(含最大徽章等级)。 // @author SmallRob // @match https://steamcommunity.com/id/*/badges* // @match https://steamcommunity.com/profiles/*/badges* // @icon https://store.steampowered.com/favicon.ico // @require https://update.greasyfork.org/scripts/589926/1893674/SGLV%20%E5%B0%81%E9%9D%A2%E4%B8%89%E7%BA%A7%E9%99%8D%E7%BA%A7%20%28Library%29.js // @connect steamcommunity.com // @connect steamcardexchange.net // @connect store.steampowered.com // @connect api.steampowered.com // @connect steampowered.com // @connect cdn.cloudflare.steamstatic.com // @connect shared.akamai.steamstatic.com // @connect community.cloudflare.steamstatic.com // @connect community.fastly.steamstatic.com // @connect community.akamai.steamstatic.com // @connect shared.fastly.steamstatic.com // @connect cdn.fastly.steamstatic.com // @connect cdn.akamai.steamstatic.com // @connect shared.steamstatic.com // @connect avatars.akamai.steamstatic.com // @connect avatars.cloudflare.steamstatic.com // @connect avatars.fastly.steamstatic.com // @connect api.openai.com // @connect api.deepseek.com // @connect cdn.jsdelivr.net // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ============================================================ // 配置 // ============================================================ const CONFIG = { CARDS_PER_PAGE: 48, DEBOUNCE_DELAY: 300, MARKET_CACHE_TTL: 48 * 60 * 60 * 1000, // v1.5.3: 10分钟→48小时(价格48H内变化不大) MARKET_CACHE_TTL_JITTER: 6 * 60 * 60 * 1000, // v1.5.3: ±6H抖动,错峰过期避免同时大量失效 MARKET_CACHE_STALE_THRESHOLD: 36 * 60 * 60 * 1000, // v1.5.3: 36H后标记stale,后台增量刷新 MARKET_REQUEST_DELAY: 350, // v1.5.3: 450ms→350ms(48H TTL下请求频率大幅降低,可提速) MARKET_REQUEST_CONCURRENCY: 6, // v1.5.3: 4→6(请求频率降低后可安全提升并发) MARKET_REFRESH_CONCURRENCY: 3, // v1.5.3: 后台stale刷新并发数(低优先级,不影响UX) // v1.2.10: 性能优化 MARKET_RERENDER_THROTTLE_MS: 120, // 局部重渲染节流(rAF 兜底) MARKET_PAGE_START_DELAY: 250, // 页面渲染完成后等 250ms 再后台启动价格加载 IMG_BLOB_CACHE_MAX: 300, // v1.5.5: 图片Blob内存缓存(objectURL)上限,LRU淘汰防内存膨胀 MARKET_RESUME_KEY: 'sbc_market_resume_v1', // 断点续传存储键 MARKET_MAX_CONSECUTIVE_FAILS: 6, // 连续失败次数超过则暂停 30s 避免被限流 MARKET_FAIL_COOLDOWN_MS: 30 * 1000, // v1.2.14: 卡牌抓取增强(参考 steam-store-card-prices) MARKET_QUERY_FOIL_CARDS: true, // 是否同时抓取闪卡(tag_cardborder_1) MARKET_QUERY_BUY_ORDERS: false, // 是否抓取求购价(需额外请求挂单页,默认关闭避免限流;详情浮窗按需触发) MARKET_BUY_ORDER_CONCURRENCY: 3, // 求购价并发数 MARKET_BUY_ORDER_RETRY: 1, // 求购价失败重试次数 MARKET_BUY_ORDER_RETRY_DELAY: 600, // 求购价重试间隔(ms) STEAM_TRANSACTION_FEE_PERCENT: 0.05, // Steam 交易手续费(5%) DEFAULT_PUBLISHER_FEE_PERCENT: 0.10, // 默认游戏发行商手续费(10%) // v1.2.1: SteamCardExchange 卡牌数据库 CARD_DB_API: 'https://www.steamcardexchange.net/api/request.php?GetInventory', CARD_DB_CACHE_KEY: 'sbc_card_db_cache', CARD_DB_CACHE_TTL: 24 * 60 * 60 * 1000, // 24小时 VALUE_SCORE_WEIGHTS: { // v1.2.2: 重构价值评分权重,引入升级潜力与性价比维度 // 升级潜力(核心修复:基于游戏最大等级,Lv.3/Max15 > Lv.3/Max5) upgradePotential: 0.28, // 经验值(绝对归一化,避免与等级重复计权) xp: 0.18, // 卡牌成本(低成本=高性价比合卡) cardPrice: 0.18, // 性价比(每元 XP,高 XP 低成本优先) costEfficiency: 0.12, // 卡牌收集进度(接近完成的沉没成本激励) completionBonus: 0.12, // 剩余掉落(即时可行动奖励) dropBonus: 0.12, }, // v1.2.2: 卡牌均价半饱和点(分),用于 cardPriceScore 平滑曲线 CARD_PRICE_HALF_SAT: 80, // v1.2.2: 性能比归一化基准(XP/分),用于 costEfficiencyScore COST_EFFICIENCY_BENCH: 5, // v1.2.2: 升级潜力公式权重(剩余空间 vs 已完成激励) UPGRADE_POTENTIAL_REMAIN_WEIGHT: 0.65, UPGRADE_POTENTIAL_DONE_WEIGHT: 0.35, // v1.2.3: 游戏徽章合成上限 GAME_BADGE_MAX_LEVEL: 5, // 普通合成最高等级 FOIL_BADGE_MAX_LEVEL: 1, // 闪卡合成最高等级 // v1.2.13: 事件/季节徽章识别(Steam 事件 appId 通常 > 300 万) EVENT_APPID_THRESHOLD: 3000000, EVENT_KEYWORDS: [ 'Summer Sale', 'Summer Collection', 'Summer Mini', 'Winter Sale', 'Winter Collection', 'Winter Mini', 'Autumn Sale', 'Autumn Collection', 'Halloween', 'Lunar New Year', 'Spring Cleaning', 'Steam Awards', 'Steam Replay', 'Steam Next Fest', 'Steam Cup', 'Clorthax', 'Paradox Party', 'The Great Steam', ], // v1.2.4: 统一游戏视图配置 UNIFIED_VIEW: { DEFAULT_TAB: 'all', // 默认分类: all/ready/progress/locked/maxed SHOW_FOIL_DETAIL: true, // 是否在展开面板中显示闪卡 COLLAPSE_BY_DEFAULT: false // 展开面板默认是否折叠 }, // v1.2.12: 活动勋章图标 CDN 基础 URL ACTIVITY_ICON_CDN: 'https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/items/', // v1.3.1: 性能与 UI 交互增强配置 LAZY_IMG_ROOT_MARGIN: '200px', // IntersectionObserver 预加载距离 LAZY_IMG_THRESHOLD: 0.01, // 触发懒加载的可见比例阈值 CARD_RENDER_BATCH_SIZE: 12, // rAF 批量渲染每帧卡片数 VIEW_TRANSITION_MS: 280, // 视图切换过渡动画时长(ms) SCROLL_TO_TOP_OFFSET: 80, // 翻页后滚动停留位置(距顶部偏移) TOAST_STACK_GAP: 8, // Toast 堆叠间距(px) TOAST_DEFAULT_DURATION: 2800, // Toast 默认显示时长(ms) // v1.3.1 R2: 智能并发池与网络优化(参考 sglv-core.lib.js 最佳实践) CONCURRENT_POOL_MAX_RETRY: 2, // 并发池单任务最大重试次数 CONCURRENT_POOL_BACKOFF_1: 1000, // 第一次重试退避(ms) CONCURRENT_POOL_BACKOFF_2: 2000, // 第二次重试退避(ms) // v1.3.6: 热门市场物品配置 POPULAR_MARKET_CACHE_TTL: 10 * 60 * 1000, // 热门市场缓存 10 分钟 POPULAR_MARKET_CACHE_KEY: 'sbc_popular_market_cache', POPULAR_MARKET_POLL_COUNT: 5, // 轮询次数(每次获取1个热门物品) POPULAR_MARKET_POLL_INTERVAL: 1500, // 轮询间隔(ms) POPULAR_MARKET_POLL_INTERVAL_BUSY: 5000, // v1.3.7: 市场价格加载中轮询间隔(ms) POPULAR_MARKET_MAX_CONSECUTIVE_FAILS: 3, // v1.3.7: 连续失败上限, 超过则提前终止 POPULAR_MARKET_DISPLAY_COUNT: 20, // 展示数量上限 // v1.3.6: AI 洞察分析配置 AI_API_TIMEOUT: 120000, // AI API 超时 120s AI_API_MAX_TOKENS: 8192, // AI API 最大 token AI_INSIGHT_CACHE_KEY: 'sbc_ai_insight_cache', AI_INSIGHT_CACHE_TTL: 30 * 60 * 1000, // AI 洞察结果缓存 30 分钟 CONCURRENT_POOL_MIN_CONCURRENCY: 3, // 429 降并发下限 CONCURRENT_POOL_DECREASE_RATIO: 0.7, // 429 降并发比例 NEG_CACHE_TTL: 30 * 60 * 1000, // 负缓存 TTL(30 分钟) GM_FETCH_DEFAULT_TIMEOUT: 15000, // GM_xmlhttpRequest 默认超时(ms) GM_FETCH_DEFAULT_RETRIES: 2, // GM_xmlhttpRequest 默认重试次数 // v1.3.1 R2: 多 CDN 回退链 CDN_CHAIN: [ 'https://cdn.cloudflare.steamstatic.com/steam/apps/', 'https://cdn.akamai.steamstatic.com/steam/apps/', 'https://shared.cloudflare.steamstatic.com/steam/apps/', ], // v1.3.1 R2: 缓存版本控制 MARKET_CACHE_VERSION: 3, // v1.5.3: 缓存结构版本(2→3,触发TTL刷新) CARD_DB_CACHE_VERSION: 2, // 卡牌数据库缓存结构版本 }; // ============================================================ // v1.2.3: Steam 活动勋章静态数据库 // 数据来源: SteamDB (https://steamdb.info/badges/) // 用于全量对比用户是否获取到所有活动勋章(含绝版/限定) // appId: 卡牌类活动勋章的应用ID(在 /gamecards/ URL 中出现) // badgeId: 非卡牌类特殊勋章的 badge_type ID // maxLevel: -1 表示无上限, 0 表示未知 // isRetired: 是否已绝版(无法再获取/合成) // ============================================================ const STEAM_ACTIVITY_BADGES = [ // ---- 2026 ---- { name: 'Summer Sale 2026', appId: '4761370', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: false, year: 2026, season: 'summer' }, { name: 'Summer Collection 2026', appId: '4844660', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: false, year: 2026, season: 'summer' }, // ---- 2025 ---- { name: 'Winter Sale 2025', appId: '3558940', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2025, season: 'winter' }, { name: 'Winter Collection 2025', appId: '4228010', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2025, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2025', badgeId: 70, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2025, season: 'awards' }, { name: 'Steam Replay 2025', badgeId: 69, type: 'replay', maxLevel: 1, hasFoil: false, isRetired: true, year: 2025, season: 'special' }, { name: 'Summer Sale 2025', appId: '3558920', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2025, season: 'summer' }, { name: 'Summer Collection 2025', appId: '3812610', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2025, season: 'summer' }, // ---- 2024 ---- { name: 'Winter Collection 2024', appId: '3412320', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2024, season: 'winter' }, { name: 'Winter Sale 2024', appId: '2861720', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2024, season: 'winter' }, { name: 'Steam Replay 2024', badgeId: 68, type: 'replay', maxLevel: 1, hasFoil: false, isRetired: true, year: 2024, season: 'special' }, { name: 'Steam Awards Nomination Committee 2024', badgeId: 67, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2024, season: 'awards' }, { name: 'Summer Sale 2024', appId: '2861690', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2024, season: 'summer' }, { name: 'Summer Collection 2024', appId: '3027110', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2024, season: 'summer' }, // ---- 2023 ---- { name: 'Winter Sale 2023', appId: '2640280', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2023, season: 'winter' }, { name: 'Winter Collection 2023', appId: '2750340', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2023, season: 'winter' }, { name: 'Steam Replay 2023', badgeId: 66, type: 'replay', maxLevel: 1, hasFoil: false, isRetired: true, year: 2023, season: 'special' }, { name: 'Steam Awards Nomination Committee 2023', badgeId: 65, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2023, season: 'awards' }, { name: 'Summer Sale 2023', appId: '2459330', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2023, season: 'summer' }, { name: 'Summer Collection 2023', appId: '2460510', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2023, season: 'summer' }, // ---- 2022 ---- { name: 'Steam Replay 2022', badgeId: 64, type: 'replay', maxLevel: 1, hasFoil: false, isRetired: true, year: 2022, season: 'special' }, { name: 'Steam Awards 2022', appId: '2243720', type: 'awards', maxLevel: -1, hasFoil: true, isRetired: true, year: 2022, season: 'awards' }, { name: 'Winter Collection 2022', appId: '2243810', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2022, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2022', badgeId: 63, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2022, season: 'awards' }, { name: 'Steam Next Fest October 2022', badgeId: 62, type: 'event', maxLevel: 6, hasFoil: false, isRetired: true, year: 2022, season: 'special' }, { name: 'Summer Sale 2022', appId: '2021850', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2022, season: 'summer' }, { name: "Clorthax's Paradox Party 2022", badgeId: 61, type: 'event', maxLevel: 10, hasFoil: false, isRetired: true, year: 2022, season: 'special' }, { name: 'Summer Collection 2022', appId: '2055870', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2022, season: 'summer' }, { name: 'Steam Next Fest June 2022', badgeId: 60, type: 'event', maxLevel: -1, hasFoil: false, isRetired: true, year: 2022, season: 'special' }, { name: '2022 Steam Cup', badgeId: 59, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2022, season: 'special' }, // ---- 2021 ---- { name: 'Steam Awards 2021', appId: '1797760', type: 'awards', maxLevel: -1, hasFoil: true, isRetired: true, year: 2021, season: 'awards' }, { name: 'Winter Sale 2021', appId: '1846860', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2021, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2021', badgeId: 56, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2021, season: 'awards' }, { name: 'Steam Awards Nomination Committee 2021 Classic', badgeId: 57, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2021, season: 'awards' }, { name: 'Summer Collection 2021', appId: '1659580', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2021, season: 'summer' }, { name: 'The Masked Avenger', badgeId: 51, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2021, season: 'special' }, { name: 'The Trailblazing Explorer', badgeId: 52, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2021, season: 'special' }, { name: 'The Gorilla Scientist', badgeId: 53, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2021, season: 'special' }, { name: 'The Paranormal Professor', badgeId: 54, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2021, season: 'special' }, { name: 'The Ghost Detective', badgeId: 55, type: 'event', maxLevel: 1, hasFoil: false, isRetired: true, year: 2021, season: 'special' }, { name: 'Summer Sale 2021', appId: '1658760', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2021, season: 'summer' }, { name: 'Spring Collection 2021', appId: '1615900', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2021, season: 'spring' }, // ---- 2020 ---- { name: 'Winter Sale 2020', appId: '1465680', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2020, season: 'winter' }, { name: 'Winter Collection 2020', appId: '1492660', type: 'collection', maxLevel: 40, hasFoil: false, isRetired: true, year: 2020, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2020', badgeId: 50, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2020, season: 'awards' }, { name: 'Community Contributor', badgeId: 48, type: 'community', maxLevel: -1, hasFoil: false, isRetired: true, year: 2020, season: 'special' }, { name: 'Community Patron', badgeId: 49, type: 'community', maxLevel: -1, hasFoil: false, isRetired: true, year: 2020, season: 'special' }, { name: 'Summer Sale 2020', appId: '1343890', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2020, season: 'summer' }, // ---- 2019 ---- { name: 'Winter Sale 2019', appId: '1264260', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2019, season: 'winter' }, { name: 'Steam Grand Prix 2019', badgeId: 45, type: 'event', maxLevel: -1, hasFoil: false, isRetired: true, year: 2019, season: 'summer' }, { name: 'Steam Awards Nomination Committee 2019', badgeId: 47, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2019, season: 'awards' }, { name: 'Lunar New Year Sale 2019', appId: '1264280', type: 'sale', maxLevel: -1, hasFoil: false, isRetired: true, year: 2019, season: 'lunar' }, // ---- 2018 ---- { name: 'Winter Sale 2018', appId: '1264290', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2018, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2018', badgeId: 44, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2018, season: 'awards' }, { name: 'Summer Sale 2018', appId: '1264300', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2018, season: 'summer' }, { name: 'Lunar New Year Sale 2018', appId: '1264310', type: 'sale', maxLevel: -1, hasFoil: false, isRetired: true, year: 2018, season: 'lunar' }, // ---- 2017 ---- { name: 'Winter Sale 2017', appId: '1264320', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2017, season: 'winter' }, { name: 'Steam Awards 2017', appId: '1264340', type: 'awards', maxLevel: -1, hasFoil: true, isRetired: true, year: 2017, season: 'awards' }, { name: 'Steam Awards Nomination Committee 2017', badgeId: 43, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2017, season: 'awards' }, { name: 'Summer Sale 2017', appId: '1264330', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2017, season: 'summer' }, // ---- 2016 ---- { name: 'Winter Sale 2016', appId: '1264350', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2016, season: 'winter' }, { name: 'Steam Awards Nomination Committee 2016', badgeId: 42, type: 'nomination', maxLevel: 4, hasFoil: false, isRetired: true, year: 2016, season: 'awards' }, { name: 'Summer Sale 2016', appId: '1264360', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2016, season: 'summer' }, // ---- 2015 ---- { name: 'Winter Sale 2015', appId: '1264370', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2015, season: 'winter' }, { name: 'Summer Sale 2015', appId: '1264380', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2015, season: 'summer' }, // ---- 2014 ---- { name: 'Winter Sale 2014', appId: '1264390', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2014, season: 'winter' }, { name: 'Summer Sale 2014', appId: '1264400', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2014, season: 'summer' }, // ---- 2013 ---- { name: 'Winter Sale 2013', appId: '1264410', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2013, season: 'winter' }, { name: 'Summer Sale 2013', appId: '1264420', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2013, season: 'summer' }, // ---- 2012 ---- { name: 'Winter Sale 2012', appId: '1264430', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2012, season: 'winter' }, { name: 'Summer Sale 2012', appId: '1264440', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2012, season: 'summer' }, // ---- 2011 ---- { name: 'Winter Sale 2011', appId: '1264450', type: 'sale', maxLevel: -1, hasFoil: true, isRetired: true, year: 2011, season: 'winter' }, { name: 'Summer Sale 2011', appId: '1264460', type: 'sale', maxLevel: -1, hasFoil: false, isRetired: true, year: 2011, season: 'summer' }, // ---- 社区/特殊勋章 ---- { name: 'Years of Service', badgeId: 1, type: 'community', maxLevel: 1, hasFoil: false, isRetired: false, year: 0, season: 'special' }, { name: 'Community Badge', badgeId: 2, type: 'community', maxLevel: 1, hasFoil: false, isRetired: false, year: 0, season: 'special' }, { name: 'Steam Gem Store', badgeId: 4, type: 'community', maxLevel: 1, hasFoil: false, isRetired: true, year: 2014, season: 'special' }, { name: 'Steam Community Profile', badgeId: 5, type: 'community', maxLevel: 1, hasFoil: false, isRetired: false, year: 0, season: 'special' }, ]; // ============================================================ // CSS 变量 & 样式(v1.3.0: 紫蓝渐变主题 + 玻璃拟态) // ============================================================ const CSS = ` /* ====== 设计令牌 ====== */ :root { --sbc-bg-primary: #0f1628; --sbc-bg-secondary: #0b1120; --sbc-bg-card: rgba(20, 27, 50, 0.9); --sbc-bg-hover: rgba(40, 55, 90, 0.6); --sbc-bg-kpi: rgba(15, 23, 42, 0.5); --sbc-bg-kpi-section: rgba(15, 23, 42, 0.4); --sbc-text-primary: #e2e8f0; --sbc-text-secondary: #94a3b8; --sbc-text-bright: #f8fafc; --sbc-accent: #8b5cf6; --sbc-accent-blue: #66c0f4; --sbc-border: rgba(139, 92, 246, 0.15); --sbc-border-hover: rgba(139, 92, 246, 0.35); --sbc-success: #4ade80; --sbc-warning: #fbbf24; --sbc-danger: #f87171; --sbc-gold: #c7a02e; --sbc-purple: #a855f7; --sbc-emerald: #10b981; --sbc-cyan: #06b6d4; /* KPI 卡片颜色变体 */ --sbc-kpi-blue: #66c0f4; --sbc-kpi-green: #22c55e; --sbc-kpi-amber: #fbbf24; --sbc-kpi-purple: #a78bfa; --sbc-kpi-red: #f44336; --sbc-kpi-cyan: #22d3ee; --sbc-kpi-border-blue: rgba(102, 192, 244, 0.35); --sbc-kpi-border-green: rgba(34, 197, 94, 0.35); --sbc-kpi-border-amber: rgba(245, 158, 11, 0.35); --sbc-kpi-border-purple: rgba(139, 92, 246, 0.35); --sbc-kpi-border-red: rgba(244, 67, 54, 0.35); --sbc-kpi-border-cyan: rgba(34, 211, 238, 0.35); --sbc-radius: 8px; --sbc-radius-lg: 12px; --sbc-radius-xl: 16px; --sbc-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); --sbc-shadow-hover: 0 12px 40px rgba(0, 0, 0, 0.6); --sbc-shadow-panel: 0 16px 56px rgba(0, 0, 0, 0.7); --sbc-transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); color-scheme: dark; } /* ====== 主容器 ====== */ .sbc-root { font-family: "Motiva Sans", Arial, Helvetica, sans-serif; color: var(--sbc-text-primary); padding: 0 16px; max-width: 1400px; margin: 0 auto; } /* ====== v1.2.7: 用户信息头部面板 (参考 Steam 原版 + 第三方设计图) ====== * v1.2.10: UI 美化——参考 html-demo/steam-dashboard.html 的玻璃拟态 + 渐变光带 * - 顶层加高斯模糊的渐变光带(冷色 + 暖色 双色叠加) * - 整体加 backdrop-filter 模糊,响应 hover 微浮起 * - 头像区域加渐变光晕 * ====== */ .sbc-user-header { display: flex; align-items: stretch; gap: 16px; background: radial-gradient(ellipse at 12% -20%, rgba(139, 92, 246, 0.14), transparent 55%), radial-gradient(ellipse at 95% 120%, rgba(6, 182, 212, 0.08), transparent 55%), linear-gradient(135deg, rgba(14, 20, 40, 0.96), rgba(20, 27, 50, 0.94)); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-xl); padding: 12px 16px; margin-bottom: 12px; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, var(--sbc-shadow); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); position: relative; overflow: hidden; transition: box-shadow 0.3s ease, transform 0.3s ease; } .sbc-user-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, rgba(139, 92, 246, 0.6) 30%, rgba(6, 182, 212, 0.5) 70%, transparent 100%); } /* v1.2.10: 头部右下角动态光晕 */ .sbc-user-header::after { content: ''; position: absolute; right: -40px; top: -40px; width: 200px; height: 200px; background: radial-gradient(circle, rgba(139, 92, 246, 0.12), transparent 65%); border-radius: 50%; pointer-events: none; animation: sbc-header-glow 8s ease-in-out infinite; } @keyframes sbc-header-glow { 0%, 100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.15); opacity: 1; } } /* 左侧:头像 + 用户名 + 进度 */ .sbc-user-left { display: flex; gap: 14px; align-items: center; flex: 1; min-width: 0; } .sbc-user-avatar-wrap { position: relative; flex-shrink: 0; } .sbc-user-avatar-frame { position: absolute; top: -8px; left: -8px; width: calc(100% + 16px); height: calc(100% + 16px); pointer-events: none; z-index: 1; } /* v1.2.10 修复: 默认装饰边框(SVG ring),即使没有 Steam 头像框也给个像样的描边 */ .sbc-user-avatar-ring { position: absolute; top: -5px; left: -5px; width: calc(100% + 10px); height: calc(100% + 10px); pointer-events: none; z-index: 0; border-radius: 14px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.45), rgba(6, 182, 212, 0.3), rgba(139, 92, 246, 0.25)); padding: 2px; -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; transition: background 0.3s ease; } .sbc-user-header:hover .sbc-user-avatar-ring { background: linear-gradient(135deg, rgba(139, 92, 246, 0.7), rgba(6, 182, 212, 0.5), rgba(59, 130, 246, 0.5)); } .sbc-user-avatar { position: relative; z-index: 2; width: 72px; height: 72px; border-radius: 10px; overflow: hidden; background: linear-gradient(135deg, #1e2d3d, #0e1a27); box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.25), 0 0 18px rgba(139, 92, 246, 0.12), 0 2px 12px rgba(0, 0, 0, 0.4); transition: box-shadow 0.3s ease, transform 0.3s ease; } .sbc-user-header:hover .sbc-user-avatar { box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.5), 0 0 24px rgba(139, 92, 246, 0.25), 0 2px 12px rgba(0, 0, 0, 0.4); } .sbc-user-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; /* v1.2.10 修复: 即使图片 URL 错误也保持容器不变形 */ background: linear-gradient(135deg, #1e2d3d, #0e1a27); } .sbc-user-avatar-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: 700; color: var(--sbc-accent); background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(6, 182, 212, 0.08)); } .sbc-user-level-badge { position: absolute; bottom: -6px; right: -6px; min-width: 42px; height: 24px; padding: 0 7px; background: linear-gradient(135deg, #8b5cf6, #6366f1); color: #fff; font-size: 12px; font-weight: 800; border-radius: 12px; display: flex; align-items: center; gap: 3px; border: 2px solid #16202d; box-shadow: 0 2px 8px rgba(139, 92, 246, 0.5); z-index: 4; letter-spacing: 0.3px; } .sbc-user-level-badge svg { color: #ffd700; flex-shrink: 0; } .sbc-user-online-dot { position: absolute; top: 4px; right: 4px; width: 12px; height: 12px; border-radius: 50%; background: #7f8c8d; border: 2px solid #16202d; z-index: 4; } .sbc-user-online--online .sbc-user-online-dot { background: #22c55e; box-shadow: 0 0 6px rgba(34, 197, 94, 0.6); animation: sbc-online-pulse 2s ease-in-out infinite; } .sbc-user-online--in-game .sbc-user-online-dot { background: #66c0f4; box-shadow: 0 0 6px rgba(102, 192, 244, 0.6); animation: sbc-online-pulse 1.5s ease-in-out infinite; } @keyframes sbc-online-pulse { 0%, 100% { box-shadow: 0 0 4px rgba(34, 197, 94, 0.4); } 50% { box-shadow: 0 0 10px rgba(34, 197, 94, 0.9), 0 0 16px rgba(34, 197, 94, 0.4); } } /* 用户名 + ID */ .sbc-user-info { flex: 1; min-width: 0; } .sbc-user-name-row { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; } .sbc-user-name { font-size: 18px; font-weight: 700; color: var(--sbc-text-bright); text-decoration: none; transition: color 0.2s; } .sbc-user-name:hover { color: var(--sbc-accent); } .sbc-user-verified { flex-shrink: 0; } /* v1.2.14-fix3: 昵称后的等级展示 */ .sbc-user-name-level { display: inline-flex; align-items: center; gap: 3px; padding: 2px 7px; border-radius: 4px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(6, 182, 212, 0.1)); border: 1px solid rgba(167, 139, 250, 0.35); color: #c4b5fd; font-size: 12px; font-weight: 700; flex-shrink: 0; line-height: 1; } .sbc-user-name-level svg { color: #fbbf24; } .sbc-user-refresh { width: 24px; height: 24px; border: 1px solid var(--sbc-border); border-radius: 6px; background: rgba(255, 255, 255, 0.04); color: var(--sbc-text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .sbc-user-name-row .sbc-user-refresh { margin-left: auto; } .sbc-user-refresh:hover { background: rgba(139, 92, 246, 0.15); color: var(--sbc-accent); border-color: rgba(139, 92, 246, 0.4); } .sbc-user-refresh.spinning svg { animation: sbc-refresh-spin 0.8s linear infinite; } @keyframes sbc-refresh-spin { to { transform: rotate(360deg); } } /* v1.2.10: 设置按钮(齿轮),与刷新按钮一致的紧凑样式 */ .sbc-user-settings { width: 24px; height: 24px; border: 1px solid var(--sbc-border); border-radius: 6px; background: rgba(255, 255, 255, 0.04); color: var(--sbc-text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; margin-left: 6px; transition: all 0.2s; } .sbc-user-settings:hover { background: rgba(251, 191, 36, 0.18); color: var(--sbc-gold); border-color: rgba(251, 191, 36, 0.4); } .sbc-user-settings:hover svg { transform: rotate(45deg); transition: transform 0.3s; } .sbc-user-settings svg { transition: transform 0.3s; } /* v1.2.10: 设置模态框 */ .sbc-modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 100000; display: flex; align-items: center; justify-content: center; padding: 20px; animation: sbc-fade-in 0.2s ease-out; } @keyframes sbc-fade-in { from { opacity: 0; } to { opacity: 1; } } .sbc-modal { background: linear-gradient(135deg, #1e2530 0%, #131820 100%); border: 1px solid var(--sbc-border); border-radius: 14px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(139, 92, 246, 0.1); max-width: 520px; width: 100%; max-height: 90vh; overflow: auto; animation: sbc-modal-in 0.25s ease-out; } @keyframes sbc-modal-in { from { transform: translateY(20px) scale(0.97); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } .sbc-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid var(--sbc-border); } .sbc-modal-title { font-size: 16px; font-weight: 700; color: var(--sbc-text-bright); display: flex; align-items: center; gap: 8px; } .sbc-modal-close { width: 28px; height: 28px; border: none; background: transparent; color: var(--sbc-text-secondary); cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .sbc-modal-close:hover { background: rgba(255, 255, 255, 0.08); color: var(--sbc-text-bright); } .sbc-modal-body { padding: 20px 22px; } .sbc-modal-footer { padding: 14px 22px; border-top: 1px solid var(--sbc-border); display: flex; justify-content: flex-end; gap: 8px; } .sbc-form-group { margin-bottom: 16px; } .sbc-form-label { display: block; font-size: 12px; font-weight: 600; color: var(--sbc-text-secondary); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.4px; } .sbc-form-label-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } .sbc-form-hint { font-size: 11px; color: var(--sbc-text-secondary); font-weight: 400; text-transform: none; letter-spacing: 0; } .sbc-form-input { width: 100%; padding: 10px 12px; background: rgba(0, 0, 0, 0.3); border: 1px solid var(--sbc-border); border-radius: 8px; color: var(--sbc-text-bright); font-size: 13px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; transition: all 0.2s; box-sizing: border-box; } .sbc-form-input:focus { outline: none; border-color: var(--sbc-accent); background: rgba(0, 0, 0, 0.45); box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); } .sbc-form-input::placeholder { color: rgba(255, 255, 255, 0.25); } .sbc-form-help { margin-top: 6px; font-size: 11px; line-height: 1.5; color: var(--sbc-text-secondary); } .sbc-form-help a { color: var(--sbc-accent); text-decoration: none; } .sbc-form-help a:hover { text-decoration: underline; } .sbc-form-status { margin-top: 8px; padding: 8px 10px; border-radius: 6px; font-size: 12px; display: none; } .sbc-form-status.success { display: block; background: rgba(34, 197, 94, 0.12); border: 1px solid rgba(34, 197, 94, 0.3); color: #4ade80; } .sbc-form-status.error { display: block; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); color: #fca5a5; } .sbc-form-status.loading { display: block; background: rgba(139, 92, 246, 0.12); border: 1px solid rgba(139, 92, 246, 0.3); color: var(--sbc-accent); } .sbc-btn-primary { padding: 8px 16px; background: linear-gradient(135deg, var(--sbc-accent) 0%, #6366f1 100%); border: 1px solid var(--sbc-accent); border-radius: 6px; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .sbc-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 192, 244, 0.4); } .sbc-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; } .sbc-btn-ghost { padding: 8px 14px; background: transparent; border: 1px solid var(--sbc-border); border-radius: 6px; color: var(--sbc-text-secondary); font-size: 13px; cursor: pointer; transition: all 0.2s; } .sbc-btn-ghost:hover { color: var(--sbc-text-bright); border-color: var(--sbc-text-secondary); } .sbc-user-steam-id { font-size: 11px; color: var(--sbc-text-secondary); font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; letter-spacing: 0.3px; } /* 等级进度条 (v1.2.14-fix3: 移除 label,数值直接展示在进度条下方) */ .sbc-user-progress { margin-top: 8px; } .sbc-user-progress-bar { width: 100%; height: 6px; background: rgba(255, 255, 255, 0.06); border-radius: 3px; overflow: hidden; position: relative; } .sbc-user-progress-fill { height: 100%; background: linear-gradient(90deg, #fbbf24, #f59e0b, #facc15); border-radius: 3px; box-shadow: 0 0 8px rgba(251, 191, 36, 0.5); transition: width 0.6s ease; } /* v1.2.14-fix3: 合并为一行 - 当前等级 | XP数值 | 下一等级 | 百分比 */ .sbc-user-progress-text { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 4px; font-size: 10px; color: var(--sbc-text-secondary); white-space: nowrap; } .sbc-user-progress-text strong { color: var(--sbc-gold); font-weight: 700; } .sbc-user-progress-lvl-from { color: var(--sbc-gold); font-weight: 700; font-size: 11px; } .sbc-user-progress-lvl-to { color: var(--sbc-text-primary); font-weight: 600; font-size: 11px; } .sbc-user-progress-pct { color: var(--sbc-text-primary); font-weight: 600; } /* 右侧:4 个 KPI 卡片 (v1.2.10: 参考 dashboard .kpi 风格,加 hover 浮起 + 顶部 accent 条) */ .sbc-user-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; align-self: stretch; min-width: 340px; } .sbc-user-kpi { position: relative; background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01)); border: 1px solid var(--sbc-border); border-top: 2px solid transparent; /* 占位,各色 variant 覆盖 */ border-radius: var(--sbc-radius); padding: 6px 10px 6px 10px; display: flex; align-items: center; gap: 7px; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease, background 0.25s ease; cursor: default; overflow: hidden; } /* v1.2.10: KPI 卡片顶部 accent 条 */ .sbc-user-kpi::before { content: ''; position: absolute; top: -1px; left: -1px; right: -1px; height: 2px; background: var(--sbc-accent); opacity: 0.85; transition: opacity 0.25s ease, height 0.25s ease; } .sbc-user-kpi:hover { background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); } .sbc-user-kpi:hover::before { opacity: 1; height: 3px; } /* v1.2.14-fix5: 可点击的 KPI 卡片(好友/游戏/徽章) */ .sbc-user-kpi--clickable { text-decoration: none; color: inherit; cursor: pointer; } .sbc-user-kpi--clickable:hover { border-color: var(--sbc-accent); background: linear-gradient(180deg, rgba(102, 192, 244, 0.08), rgba(102, 192, 244, 0.02)); } .sbc-user-kpi-icon { width: 26px; height: 26px; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: rgba(102, 192, 244, 0.12); color: var(--sbc-accent); } .sbc-user-kpi--cyan::before { background: #22d3ee; } .sbc-user-kpi--cyan .sbc-user-kpi-icon { background: rgba(34, 211, 238, 0.15); color: #22d3ee; } .sbc-user-kpi--cyan { border-color: rgba(34, 211, 238, 0.22); } .sbc-user-kpi--purple::before { background: #a78bfa; } .sbc-user-kpi--purple .sbc-user-kpi-icon { background: rgba(167, 139, 250, 0.15); color: #a78bfa; } .sbc-user-kpi--purple { border-color: rgba(167, 139, 250, 0.22); } .sbc-user-kpi--teal::before { background: #2dd4bf; } .sbc-user-kpi--teal .sbc-user-kpi-icon { background: rgba(45, 212, 191, 0.15); color: #2dd4bf; } .sbc-user-kpi--teal { border-color: rgba(45, 212, 191, 0.22); } .sbc-user-kpi--gold::before { background: #fbbf24; } .sbc-user-kpi--gold .sbc-user-kpi-icon { background: rgba(251, 191, 36, 0.15); color: #fbbf24; } .sbc-user-kpi--gold { border-color: rgba(251, 191, 36, 0.22); } .sbc-user-kpi-body { min-width: 0; flex: 1; } .sbc-user-kpi-value { font-size: 15px; font-weight: 800; color: var(--sbc-text-bright); line-height: 1.15; letter-spacing: -0.3px; font-variant-numeric: tabular-nums; /* 等宽数字,数字变化不抖 */ } /* v1.2.10 修复: 空值时数字用浅灰显示,避免"0"看起来像真实数据 */ .sbc-user-kpi-empty .sbc-user-kpi-value, .sbc-user-kpi-empty { color: var(--sbc-text-secondary); } .sbc-user-kpi-empty { opacity: 0.7; } .sbc-user-kpi-empty .sbc-user-kpi-icon { opacity: 0.55; } .sbc-user-kpi-label { font-size: 9px; color: var(--sbc-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; margin-top: 1px; } .sbc-user-kpi-sub { font-size: 8.5px; color: var(--sbc-text-secondary); opacity: 0.7; margin-top: 0; } /* v1.2.10: KPI 卡片点击交互(可点击查看详情) */ .sbc-user-kpi--clickable { cursor: pointer; } .sbc-user-kpi--clickable:hover .sbc-user-kpi-value { color: var(--sbc-accent); } /* v1.2.7: 响应式 */ @media (max-width: 1100px) { .sbc-user-header { flex-direction: column; } .sbc-user-kpis { min-width: 0; width: 100%; } } @media (max-width: 700px) { .sbc-user-kpis { grid-template-columns: repeat(2, 1fr); } } /* ====== 工具栏 (v1.2.3: 紧凑布局,参考 sglv header) ====== * v1.2.10: 玻璃拟态 + 左/右分割 + 毛玻璃模糊 * ====== */ .sbc-toolbar { background: linear-gradient(180deg, rgba(20, 27, 50, 0.85), rgba(15, 20, 40, 0.82)); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-lg); padding: 10px 14px; margin-bottom: 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: var(--sbc-shadow); position: relative; } /* v1.2.10: 工具栏左右分割线(在 ≥900px 宽度下) */ @media (min-width: 900px) { .sbc-toolbar::after { content: ''; position: absolute; left: 50%; top: 12px; bottom: 12px; width: 1px; background: linear-gradient(180deg, transparent, rgba(139, 92, 246, 0.2), transparent); pointer-events: none; } } .sbc-toolbar-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 300px; flex-wrap: wrap; } .sbc-toolbar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex-wrap: wrap; padding-left: 4px; } /* ====== 搜索框 ====== */ .sbc-search { position: relative; flex: 1; max-width: 320px; min-width: 200px; } .sbc-search input { width: 100%; padding: 7px 10px 7px 34px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--sbc-radius); background: rgba(255, 255, 255, 0.05); color: var(--sbc-text-primary); font-size: 12px; outline: none; transition: border-color var(--sbc-transition); box-sizing: border-box; } .sbc-search input:focus { border-color: var(--sbc-accent); } .sbc-search input::placeholder { color: var(--sbc-text-secondary); } .sbc-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--sbc-text-secondary); pointer-events: none; } /* ====== 下拉框 ====== */ .sbc-select { padding: 7px 28px 7px 10px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: var(--sbc-radius); background: rgba(255, 255, 255, 0.05); color: var(--sbc-text-primary); font-size: 12px; font-weight: 600; cursor: pointer; outline: none; appearance: none; -webkit-appearance: none; background-repeat: no-repeat; background-position: right 8px center; background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238f98a0' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); transition: all 0.2s; } .sbc-select option { background: #1a2236; color: #e2e8f0; padding: 6px 10px; } .sbc-select option:hover, .sbc-select option:checked { background: #2d3a52; color: #fff; } .sbc-select:hover { border-color: rgba(139, 92, 246, 0.3); } .sbc-select:focus { border-color: var(--sbc-accent); } /* ====== 按钮 ====== */ .sbc-btn { display: inline-flex; align-items: center; gap: 5px; padding: 7px 12px; border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); background: rgba(255, 255, 255, 0.05); color: var(--sbc-text-primary); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; white-space: nowrap; user-select: none; line-height: 1.4; } .sbc-btn:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(102, 192, 244, 0.3); color: var(--sbc-accent); } .sbc-btn:active { transform: scale(0.97); } .sbc-btn--accent { background: rgba(102, 192, 244, 0.2); color: var(--sbc-accent); border: 1px solid rgba(102, 192, 244, 0.4); font-weight: 600; } .sbc-btn--accent:hover { background: rgba(102, 192, 244, 0.3); border-color: rgba(102, 192, 244, 0.6); color: #fff; } .sbc-btn--success { background: linear-gradient(135deg, #2f7a3d 0%, #1f5a2d 100%); color: #a8e6a8; border: 1px solid #3f8a4d; } .sbc-btn--purple { background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); color: #d8b4fe; border: 1px solid #8b5cf6; } .sbc-btn--purple:hover { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: #ede9fe; } .sbc-btn--analysis { background: linear-gradient(135deg, #c7a02e 0%, #a67c00 100%); color: #fff; border: none; font-weight: 600; position: relative; } .sbc-btn--analysis:hover { background: linear-gradient(135deg, #d4b63e 0%, #c7a02e 100%); color: #fff; } .sbc-btn--analysis.active::after { content: ''; position: absolute; bottom: -2px; left: 12px; right: 12px; height: 2px; background: #fff; border-radius: 2px; } /* ====== 统计信息 (v1.2.3: 紧凑胶囊式,参考 sglv stats dashboard) * v1.2.10: 加玻璃背景 + 分割,数字使用 tabular-nums 防止抖动 * ====== */ .sbc-stats { display: flex; align-items: center; gap: 6px; padding: 10px 14px; margin-bottom: 10px; font-size: 11.5px; color: var(--sbc-text-secondary); flex-wrap: wrap; background: linear-gradient(180deg, rgba(20, 28, 40, 0.5), rgba(14, 22, 32, 0.4)); border: 1px solid rgba(102, 192, 244, 0.08); border-radius: 10px; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); } .sbc-stats > span { display: inline-flex; align-items: center; gap: 4px; padding: 2px 2px; } .sbc-stats-num { color: var(--sbc-accent); font-weight: 700; background: rgba(102, 192, 244, 0.14); padding: 1px 8px; border-radius: 10px; font-size: 11.5px; font-variant-numeric: tabular-nums; letter-spacing: 0.2px; } .sbc-stats-filter { cursor: pointer; transition: all 0.18s ease; padding: 2px 6px; border-radius: 6px; } .sbc-stats-filter:hover { color: var(--sbc-accent); background: rgba(102, 192, 244, 0.10); } .sbc-stats-filter:hover .sbc-stats-num { background: rgba(102, 192, 244, 0.22); } /* ====== 卡片网格 (v1.2.4: 6列紧凑布局) * v1.2.10: 响应式增强 * - ≥1600px: 7 列(给宽屏留更多空间) * - ≥1100px: 6 列(默认) * - ≥700px : 4 列(平板) * - <700px : 2 列(手机) * ====== */ .sbc-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; padding-bottom: 20px; } @media (min-width: 1600px) { .sbc-root { max-width: 1600px; } .sbc-grid { grid-template-columns: repeat(7, 1fr); } } @media (max-width: 1100px) { .sbc-grid { grid-template-columns: repeat(4, 1fr); } } @media (max-width: 700px) { .sbc-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; } } /* ====== 卡片(含Pro增强) * v1.2.10: hover 过渡曲线统一为 dashboard 风格的 cubic-bezier(.16, 1, .3, 1) * 增加渐入动画 sbc-card-enter (原版只是简单的 opacity 0→1,新版加轻微上移+模糊) * ====== */ .sbc-card { background: var(--sbc-bg-card); border-radius: var(--sbc-radius-lg); overflow: hidden; border: 1px solid var(--sbc-border); transition: box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.3s ease; cursor: pointer; position: relative; display: flex; flex-direction: column; } .sbc-card:hover { box-shadow: 0 0 0 2px var(--sbc-gold), 0 4px 16px rgba(0, 0, 0, 0.4); border-color: var(--sbc-gold); z-index: 5; } /* Pro: 锁定徽章样式(参考游戏库 viewer:灰度滤镜,hover 还原) */ .sbc-card--locked .sbc-card-img-wrap img { filter: grayscale(0.85) brightness(0.55); opacity: 0.7; transition: filter 0.4s ease, opacity 0.4s ease; } .sbc-card--locked:hover .sbc-card-img-wrap img { filter: grayscale(0.2) brightness(0.95); opacity: 1; } .sbc-card--locked { border-color: rgba(255, 255, 255, 0.04); } .sbc-card--locked:hover { border-color: var(--sbc-accent); } /* Pro: 高价值徽章样式 */ .sbc-card--valuable { border-color: rgba(192, 160, 46, 0.35); } .sbc-card--valuable:hover { border-color: var(--sbc-gold); box-shadow: 0 0 0 2px var(--sbc-gold), 0 4px 16px rgba(192, 160, 46, 0.2); } /* 自动合卡: 可合卡/合卡中/已完成 状态 */ .sbc-card--craftable { border-color: rgba(243, 156, 18, 0.5); } .sbc-card--craftable::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #f39c12, #e67e22); z-index: 3; } .sbc-card-craft-badge { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #f39c12, #e67e22); color: #fff; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; pointer-events: none; z-index: 3; white-space: nowrap; } .sbc-card-img-wrap, .sbc-db-card-img-wrap { position: relative; width: 100%; aspect-ratio: 4/3; overflow: hidden; background: radial-gradient(circle at 50% 35%, rgba(102,192,244,0.10), transparent 65%), linear-gradient(135deg, #0e1a27 0%, #16202d 60%, #1a2638 100%); display: flex; align-items: center; justify-content: center; } .sbc-card-img-wrap::after, .sbc-db-card-img-wrap::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 45%; background: linear-gradient(to top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.25) 55%, transparent 100%); pointer-events: none; z-index: 1; } .sbc-card-img-wrap img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 88%; max-height: 88%; object-fit: contain; transition: transform 0.4s ease, filter 0.4s ease; z-index: 2; } /* v1.5.2: 移除卡牌图片 hover 缩放,改为边框高亮 */ /* .sbc-card:hover .sbc-card-img-wrap img { transform: translate(-50%, -50%) scale(1.06); } */ /* 多卡牌网格展示(参考游戏库 viewer 的卡片设计) */ .sbc-card-cardgrid { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: grid; gap: 4px; width: 84%; max-height: 84%; padding: 6px; z-index: 2; } .sbc-card-cardgrid img { width: 100%; aspect-ratio: 2/3; object-fit: cover; border-radius: 4px; border: 1px solid rgba(255,255,255,0.10); background: rgba(0,0,0,0.25); transition: transform 0.4s ease; } /* v1.5.2: 移除多卡牌网格 hover 缩放 */ /* .sbc-card:hover .sbc-card-cardgrid img { transform: scale(1.04); } */ .sbc-card-cardgrid-more { grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: 4px; background: rgba(255,255,255,0.06); color: var(--sbc-text-secondary); font-size: 11px; font-weight: 600; } .sbc-card-placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; gap: 6px; color: var(--sbc-text-secondary); opacity: 0.45; z-index: 2; } .sbc-card-placeholder div { font-size: 11px; } .sbc-card-level-badge, .sbc-db-card-level-badge { position: absolute; top: 6px; right: 6px; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(4px); color: var(--sbc-gold); padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; pointer-events: none; z-index: 3; } .sbc-card-xp-badge { position: absolute; top: 6px; left: 6px; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(4px); color: var(--sbc-accent); padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; pointer-events: none; z-index: 3; } /* Pro: 价值评分 & 市场价格角标 (v1.2.10: 加柔光阴影 + 滑入动画) */ .sbc-card-value-badge { position: absolute; bottom: 8px; right: 8px; background: linear-gradient(135deg, #c7a02e, #a67c00); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; pointer-events: none; z-index: 3; box-shadow: 0 2px 8px rgba(192, 160, 46, 0.35); animation: sbc-badge-pop-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; } .sbc-card-market-badge { position: absolute; bottom: 8px; left: 8px; background: linear-gradient(135deg, rgba(16, 185, 129, 0.92), rgba(5, 150, 105, 0.92)); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; pointer-events: none; z-index: 3; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.35); font-variant-numeric: tabular-nums; animation: sbc-badge-pop-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; } @keyframes sbc-badge-pop-in { from { opacity: 0; transform: scale(0.7); } to { opacity: 1; transform: scale(1); } } /* v1.2.10: 价格角标加载中(骨架) */ .sbc-card-market-badge--loading { background: rgba(102, 192, 244, 0.18); color: var(--sbc-accent); box-shadow: 0 0 0 1px rgba(102, 192, 244, 0.3) inset; background-size: 200% 100%; background-image: linear-gradient(90deg, rgba(102, 192, 244, 0.10) 0%, rgba(102, 192, 244, 0.30) 50%, rgba(102, 192, 244, 0.10) 100%); animation: sbc-shimmer 1.4s linear infinite; } @keyframes sbc-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* Pro: 锁定徽章锁图标 */ .sbc-card-locked-icon-wrap { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; } .sbc-card-locked-icon-inner { width: 48px; height: 48px; border-radius: 50%; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; border: 2px solid rgba(255, 255, 255, 0.12); } .sbc-card-info { padding: 6px 8px 8px; display: flex; flex-direction: column; gap: 3px; flex: 1; } .sbc-card-name-row { display: flex; align-items: center; gap: 4px; } .sbc-card-name { font-size: 12px; font-weight: 600; color: var(--sbc-text-primary); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color 0.2s; flex: 1; min-width: 0; } .sbc-card-detail-btn { flex-shrink: 0; font-size: 10px; color: var(--sbc-accent); cursor: pointer; opacity: 0.6; transition: opacity 0.2s; user-select: none; padding: 1px 4px; } .sbc-card-detail-btn:hover { opacity: 1; } .sbc-card:hover .sbc-card-detail-btn { opacity: 0.8; } .sbc-card:hover .sbc-card-name { color: var(--sbc-accent); } .sbc-card-badge-name { font-size: 10px; color: var(--sbc-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-card-meta { display: flex; justify-content: space-between; align-items: center; font-size: 10px; color: var(--sbc-text-secondary); } .sbc-card-progress { display: flex; align-items: center; gap: 5px; font-size: 10px; } .sbc-card-progress-bar { flex: 1; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.08); overflow: hidden; } .sbc-card-progress-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, var(--sbc-accent), #a4d007); transition: width 0.6s ease; } .sbc-card-drops { font-size: 11px; color: var(--sbc-warning); display: flex; align-items: center; gap: 4px; } .sbc-card-drops--none { color: var(--sbc-text-secondary); } /* ====== 分页 ====== */ .sbc-pagination { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 20px 0 40px; flex-wrap: wrap; } .sbc-page-btn { min-width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); background: var(--sbc-bg-card); color: var(--sbc-text-primary); font-size: 14px; cursor: pointer; transition: all var(--sbc-transition); padding: 0 12px; } .sbc-page-btn:hover { border-color: var(--sbc-accent); color: var(--sbc-accent); background: var(--sbc-bg-hover); } .sbc-page-btn.active { background: var(--sbc-accent); color: #fff; border-color: var(--sbc-accent); font-weight: 600; } .sbc-page-btn:disabled { opacity: 0.4; cursor: not-allowed; } .sbc-page-info { color: var(--sbc-text-secondary); font-size: 13px; padding: 0 12px; } /* ====== Toast ====== */ .sbc-toast { position: fixed; bottom: 24px; right: 24px; background: var(--sbc-bg-secondary); color: var(--sbc-text-primary); padding: 12px 20px; border-radius: var(--sbc-radius); border: 1px solid var(--sbc-accent); box-shadow: var(--sbc-shadow); font-size: 14px; z-index: 99999; animation: sbc-toast-in 0.3s ease; transition: opacity 0.3s ease, transform 0.3s ease; max-width: 360px; word-break: break-word; } .sbc-toast--success { border-color: var(--sbc-success); } .sbc-toast--error { border-color: var(--sbc-danger); } .sbc-toast--warning { border-color: var(--sbc-gold, #c7a02e); } @keyframes sbc-toast-in { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* ====== 卡片动画 ====== * v1.2.10: 渐入曲线改用 cubic-bezier(.16, 1, .3, 1) (dashboard 风格) * 时长 0.35s → 0.45s,起始位移 16px → 10px(更轻) * ====== */ .sbc-card-enter { animation: sbc-card-fade-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; } @keyframes sbc-card-fade-in { from { opacity: 0; transform: translateY(10px) scale(0.98); filter: blur(2px); } to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } } /* ====== 空状态 ====== */ .sbc-empty { text-align: center; padding: 60px 20px; color: var(--sbc-text-secondary); } .sbc-empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.4; } .sbc-empty-text { font-size: 16px; margin-bottom: 8px; } /* ====== 加载 ====== */ .sbc-loading { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); z-index: 100000; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; } .sbc-loading-spinner { width: 48px; height: 48px; border: 4px solid rgba(102, 192, 244, 0.2); border-top-color: var(--sbc-accent); border-radius: 50%; animation: sbc-spin 0.8s linear infinite; } @keyframes sbc-spin { to { transform: rotate(360deg); } } .sbc-loading-text { color: var(--sbc-text-bright); font-size: 16px; font-weight: 500; } /* ====== v1.2.1: 全部卡牌游戏视图 ====== */ .sbc-db-card { background: var(--sbc-bg-card); border-radius: var(--sbc-radius-lg); overflow: hidden; border: 1px solid var(--sbc-border); transition: all var(--sbc-transition); cursor: pointer; position: relative; display: flex; flex-direction: column; text-decoration: none; } .sbc-db-card:hover { box-shadow: 0 0 0 2px var(--sbc-gold), 0 4px 12px rgba(0, 0, 0, 0.4); border-color: var(--sbc-gold); } .sbc-db-card--owned { border-color: rgba(92, 126, 16, 0.3); } .sbc-db-card--owned:hover { border-color: var(--sbc-success); box-shadow: 0 4px 20px rgba(92, 126, 16, 0.15); } .sbc-db-card--unowned { border-color: rgba(255, 255, 255, 0.04); } .sbc-db-card--unowned:hover { border-color: var(--sbc-accent); } /* v1.2.4: 未拥有游戏灰度展示 */ .sbc-db-card--unowned .sbc-db-card-img-wrap { filter: grayscale(0.8) brightness(0.6); } .sbc-db-card--unowned:hover .sbc-db-card-img-wrap { filter: grayscale(0.4) brightness(0.8); } .sbc-db-card-icon { width: 40px; height: 40px; opacity: 0.35; color: var(--sbc-text-secondary); z-index: 2; } .sbc-db-card--owned .sbc-db-card-icon { color: var(--sbc-success); opacity: 0.5; } .sbc-db-card-status-badge { position: absolute; top: 6px; left: 6px; padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: 700; pointer-events: none; z-index: 3; } .sbc-db-card-status--owned { background: rgba(92, 126, 16, 0.85); color: #a8e6a8; } .sbc-db-card-status--unowned { background: rgba(0, 0, 0, 0.7); color: var(--sbc-text-secondary); } .sbc-db-card-info { padding: 6px 8px 8px; display: flex; flex-direction: column; gap: 3px; flex: 1; } .sbc-db-card-name-row { display: flex; align-items: center; gap: 4px; } .sbc-db-card-name { font-size: 12px; font-weight: 600; color: var(--sbc-text-primary); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; } .sbc-db-card-detail-btn { flex-shrink: 0; font-size: 10px; color: var(--sbc-accent); cursor: pointer; opacity: 0.6; transition: opacity 0.2s; user-select: none; padding: 1px 4px; } .sbc-db-card-detail-btn:hover { opacity: 1; } .sbc-db-card:hover .sbc-db-card-detail-btn { opacity: 0.8; } .sbc-db-card-meta { display: flex; justify-content: space-between; align-items: center; font-size: 10px; color: var(--sbc-text-secondary); margin-top: 1px; } .sbc-db-card-appid { font-family: monospace; opacity: 0.7; } .sbc-db-card-maxlvl { color: var(--sbc-gold); font-weight: 600; } /* v1.2.1: 卡片最高等级补充显示 */ .sbc-card-maxlevel-badge { position: absolute; top: 8px; right: 8px; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(4px); color: var(--sbc-text-secondary); padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 500; pointer-events: none; z-index: 4; } /* v1.2.1: 卡牌数据库加载条 */ .sbc-db-loading-bar { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: rgba(102, 192, 244, 0.1); border: 1px solid rgba(102, 192, 244, 0.3); border-radius: var(--sbc-radius); font-size: 12px; color: var(--sbc-accent); } .sbc-db-loading-bar .sbc-db-spinner, .sbc-market-spinner { width: 14px; height: 14px; border: 2px solid rgba(102, 192, 244, 0.2); border-top-color: var(--sbc-accent); border-radius: 50%; animation: sbc-spin 0.8s linear infinite; } /* ====== 游戏分组模式 ====== */ .sbc-game-group { margin-bottom: 24px; background: var(--sbc-bg-secondary); border-radius: var(--sbc-radius-lg); border: 1px solid var(--sbc-border); overflow: hidden; } .sbc-game-group-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; background: linear-gradient(135deg, var(--sbc-bg-primary), #1a3a4a); border-bottom: 1px solid var(--sbc-border); cursor: pointer; transition: background var(--sbc-transition); } .sbc-game-group-header:hover { background: linear-gradient(135deg, #1a3a4a, var(--sbc-bg-primary)); } .sbc-game-group-title { display: flex; align-items: center; gap: 12px; flex: 1; } .sbc-game-group-name { font-size: 16px; font-weight: 600; color: var(--sbc-text-bright); } .sbc-game-group-count { font-size: 12px; color: var(--sbc-text-secondary); background: rgba(255, 255, 255, 0.08); padding: 3px 10px; border-radius: 12px; } .sbc-game-group-stats { display: flex; align-items: center; gap: 16px; font-size: 12px; color: var(--sbc-text-secondary); } .sbc-game-group-stats span { display: inline-flex; align-items: center; gap: 4px; } .sbc-game-group-stats-owned-all { color: var(--sbc-success); } .sbc-game-group-toggle { color: var(--sbc-text-secondary); transition: transform var(--sbc-transition); } .sbc-game-group-toggle.collapsed { transform: rotate(-90deg); } /* v1.2.14-fix2: 分组视图详情按钮(打开大屏浮窗) */ /* v1.4.0: 添加 SVG 图标支持 */ .sbc-game-group-detail-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: var(--sbc-radius); background: rgba(102, 192, 244, 0.12); border: 1px solid rgba(102, 192, 244, 0.3); color: var(--sbc-accent); font-size: 12px; font-weight: 600; cursor: pointer; transition: all var(--sbc-transition); flex-shrink: 0; } .sbc-game-group-detail-btn:hover { background: rgba(102, 192, 244, 0.25); border-color: var(--sbc-accent); } .sbc-game-group-content { padding: 16px; transition: max-height 0.3s ease, opacity 0.3s ease; overflow: hidden; } .sbc-game-group-content.collapsed { max-height: 0; padding: 0 16px; opacity: 0; } .sbc-game-group .sbc-grid { display: none; /* v1.2.6: 隐藏旧 grid,改用徽章行表格 */ } /* ====== v1.2.6: 徽章表格(原版风格) ====== */ .sbc-badge-table { display: flex; flex-direction: column; gap: 4px; padding-bottom: 4px; } .sbc-badge-row { display: grid; grid-template-columns: 36px minmax(0, 1fr) 50px 60px minmax(140px, 200px) minmax(80px, 110px) minmax(110px, auto); align-items: center; gap: 10px; padding: 6px 10px; background: rgba(255, 255, 255, 0.02); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); transition: all 0.15s; text-decoration: none; color: var(--sbc-text-primary); } .sbc-badge-row:hover { background: rgba(255, 255, 255, 0.05); border-color: var(--sbc-border-hover); color: var(--sbc-text-primary); } .sbc-badge-row--completed { border-color: rgba(92, 126, 16, 0.3); background: linear-gradient(90deg, rgba(92, 126, 16, 0.08), rgba(255, 255, 255, 0.02)); } .sbc-badge-row--completed:hover { border-color: var(--sbc-success); } .sbc-badge-row--foil { border-color: rgba(199, 160, 46, 0.4); background: linear-gradient(90deg, rgba(199, 160, 46, 0.08), rgba(255, 255, 255, 0.02)); } .sbc-badge-row--locked { opacity: 0.7; } /* v1.3.8: 徽章行操作区(状态 + 详情按钮) */ .sbc-badge-row-actions { display: flex; align-items: center; gap: 8px; justify-content: flex-end; } /* v1.2.14-fix2: 徽章行详情按钮 */ .sbc-badge-row-detail-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: var(--sbc-radius); background: rgba(102, 192, 244, 0.1); border: 1px solid rgba(102, 192, 244, 0.25); color: var(--sbc-accent); font-size: 11px; font-weight: 600; cursor: pointer; transition: all var(--sbc-transition); text-align: center; white-space: nowrap; } .sbc-badge-row-detail-btn:hover { background: rgba(102, 192, 244, 0.22); border-color: var(--sbc-accent); } .sbc-badge-row-icon { width: 36px; height: 36px; background: var(--sbc-bg-secondary); border-radius: 5px; display: flex; align-items: center; justify-content: center; overflow: hidden; } .sbc-badge-row-icon img { width: 100%; height: 100%; object-fit: cover; } .sbc-badge-row-img-locked { filter: grayscale(1) brightness(0.5); } .sbc-badge-row-img-placeholder { font-size: 18px; color: var(--sbc-text-secondary); } .sbc-badge-row-info { min-width: 0; overflow: hidden; } .sbc-badge-row-name { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--sbc-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-badge-row-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; } .sbc-badge-row-foil-tag { background: var(--sbc-gold); color: #1b2838; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; flex-shrink: 0; } .sbc-badge-row-meta { font-size: 10px; color: var(--sbc-text-secondary); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-badge-row-lvl { font-size: 12px; font-weight: 800; color: var(--sbc-gold); text-align: center; } .sbc-badge-row--completed .sbc-badge-row-lvl { color: var(--sbc-success); } .sbc-badge-row-xp { font-size: 11px; font-weight: 600; color: var(--sbc-accent); text-align: right; } .sbc-badge-row-progress-wrap { min-width: 0; } .sbc-badge-row-progress { display: flex; align-items: center; gap: 6px; font-size: 10px; color: var(--sbc-text-secondary); } .sbc-badge-row-progress--empty { color: var(--sbc-text-secondary); opacity: 0.4; justify-content: center; } .sbc-badge-row-progress-num { font-weight: 700; min-width: 36px; } .sbc-badge-row-progress-bar { flex: 1; height: 5px; border-radius: 2.5px; background: rgba(255, 255, 255, 0.06); overflow: hidden; } .sbc-badge-row-progress-fill { height: 100%; border-radius: 2.5px; } .sbc-badge-row-progress-pct { font-weight: 600; min-width: 30px; text-align: right; color: var(--sbc-text-primary); } .sbc-badge-row-cards { display: flex; align-items: center; gap: 4px; min-height: 28px; } .sbc-badge-row-cards img { width: 36px; height: 24px; object-fit: cover; border-radius: 3px; background: var(--sbc-bg-secondary); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.2s; } .sbc-badge-row-cards img:hover { transform: scale(1.2); z-index: 5; position: relative; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); } .sbc-badge-row-cards-more { font-size: 10px; color: var(--sbc-text-primary); font-weight: 600; background: rgba(102, 192, 244, 0.2); color: var(--sbc-accent); padding: 3px 6px; border-radius: 3px; border: 1px solid rgba(102, 192, 244, 0.3); } .sbc-badge-row-status { text-align: center; font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; } .sbc-badge-row-status--done { background: rgba(92, 126, 16, 0.25); color: var(--sbc-success); } .sbc-badge-row-status--progress { background: rgba(102, 192, 244, 0.18); color: var(--sbc-accent); } .sbc-badge-row-status--locked { background: rgba(255, 255, 255, 0.04); color: var(--sbc-text-secondary); } /* ====== v1.2.6: 分组小节标题 ====== */ .sbc-game-group-section-title { font-size: 11px; font-weight: 700; color: var(--sbc-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--sbc-border); display: flex; align-items: center; gap: 6px; } .sbc-game-group-section-title--cards { color: var(--sbc-gold); } .sbc-game-group-section-hint { font-size: 10px; font-weight: 500; color: var(--sbc-text-secondary); opacity: 0.7; text-transform: none; letter-spacing: 0; } /* v1.2.8: 分组标题中的卡牌完成度提示 */ .sbc-game-group-cards-count { color: var(--sbc-gold); } .sbc-game-group-cards-pct { color: var(--sbc-accent); font-weight: 700; } .sbc-game-group-cards-100 { display: inline-block; background: var(--sbc-success); color: #fff; padding: 1px 6px; border-radius: 3px; font-size: 9px; font-weight: 700; margin-left: 4px; } /* ====== v1.2.8: 5列卡牌紧凑网格 (参考 Steam 原版徽章页卡牌数据视图) ====== */ .sbc-card-grid-7 { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; padding: 8px 0 14px; } @media (min-width: 1600px) { .sbc-card-grid-7 { grid-template-columns: repeat(6, 1fr); } } @media (max-width: 1400px) { .sbc-card-grid-7 { grid-template-columns: repeat(5, 1fr); } } @media (max-width: 1100px) { .sbc-card-grid-7 { grid-template-columns: repeat(4, 1fr); } } @media (max-width: 800px) { .sbc-card-grid-7 { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 500px) { .sbc-card-grid-7 { grid-template-columns: repeat(2, 1fr); } } /* v1.2.8: 卡牌单元(参考原版) */ .sbc-card-tile { position: relative; background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); overflow: hidden; transition: box-shadow 0.2s, border-color 0.2s; animation: sbc-card-enter 0.4s ease backwards; cursor: pointer; display: flex; flex-direction: column; } .sbc-card-tile:hover { box-shadow: 0 0 0 2px var(--sbc-gold), 0 4px 12px rgba(0, 0, 0, 0.4); border-color: var(--sbc-gold); z-index: 2; } .sbc-card-tile-img-wrap { position: relative; width: 100%; aspect-ratio: 2/3; /* v1.2.8: 更接近原版卡牌竖版比例 */ background: var(--sbc-bg-secondary); display: flex; align-items: center; justify-content: center; } .sbc-card-tile img { width: 100%; height: 100%; object-fit: cover; display: block; } .sbc-card-tile-placeholder { width: 100%; height: 100%; background: linear-gradient(135deg, #1e2d3d, #16202d); display: flex; align-items: center; justify-content: center; font-size: 32px; color: var(--sbc-text-secondary); opacity: 0.3; } .sbc-card-tile--unowned img { filter: grayscale(0.85) brightness(0.4) contrast(0.85); } .sbc-card-tile--unowned:hover img { filter: grayscale(0.3) brightness(0.7); } /* 状态徽章 (右上角) */ .sbc-card-tile-badge { position: absolute; top: 5px; right: 5px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; line-height: 1; z-index: 2; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); } .sbc-card-tile-badge--owned { background: var(--sbc-success); color: #fff; } .sbc-card-tile-badge--locked { background: rgba(0, 0, 0, 0.7); color: var(--sbc-text-secondary); } /* v1.2.8: 卡牌信息(名称 + 价签) */ .sbc-card-tile-info { padding: 6px 8px 7px; display: flex; flex-direction: column; gap: 2px; border-top: 1px solid var(--sbc-border); } .sbc-card-tile-name { font-size: 11px; font-weight: 600; color: var(--sbc-text-primary); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .sbc-card-tile--unowned .sbc-card-tile-name { color: var(--sbc-text-secondary); } .sbc-card-tile-price { display: flex; align-items: center; justify-content: space-between; font-size: 10px; } .sbc-card-tile-price-value { color: var(--sbc-emerald); font-weight: 700; } .sbc-card-tile-price--none .sbc-card-tile-price-value { color: var(--sbc-text-secondary); opacity: 0.5; font-weight: 400; } .sbc-card-tile-type { font-size: 9px; color: var(--sbc-text-secondary); opacity: 0.7; padding: 1px 4px; background: rgba(255, 255, 255, 0.05); border-radius: 3px; } /* v1.2.8: 徽章行响应式 (v1.3.8: 适配合并后的7列布局) */ @media (max-width: 1200px) { .sbc-badge-row { grid-template-columns: 32px minmax(0, 1fr) 50px 60px minmax(110px, 1fr) 90px minmax(100px, auto); } } @media (max-width: 800px) { .sbc-badge-row { grid-template-columns: 32px minmax(0, 1fr) 60px minmax(100px, auto); } .sbc-badge-row-cards, .sbc-badge-row-xp, .sbc-badge-row-progress-wrap { display: none; } } /* ============================================================ Pro: 分析浮窗 (Analysis Panel) ============================================================ */ @keyframes sbc-overlay-in { from { opacity: 0; } to { opacity: 1; } } @keyframes sbc-panel-in { from { transform: scale(0.92) translateY(20px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } } .sbc-analysis-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(11, 17, 32, 0.85); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 100000; display: flex; align-items: center; justify-content: center; animation: sbc-overlay-in 0.3s ease; } .sbc-analysis-panel { width: min(960px, 95vw); max-height: 85vh; background: rgba(21, 30, 46, 0.97); border: 1px solid rgba(102, 192, 244, 0.2); border-radius: var(--sbc-radius-xl); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; overflow: hidden; animation: sbc-panel-in 0.35s ease; } .sbc-ap-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; background: linear-gradient(135deg, #1a2738, #16202d); border-bottom: 1px solid var(--sbc-border); flex-shrink: 0; } .sbc-ap-title { display: flex; align-items: center; gap: 8px; } .sbc-ap-title h2 { margin: 0; font-size: 15px; font-weight: 700; color: var(--sbc-text-bright); } .sbc-ap-subtitle { font-size: 12px; color: var(--sbc-text-secondary); background: rgba(255, 255, 255, 0.06); padding: 3px 10px; border-radius: 10px; } .sbc-ap-close { width: 30px; height: 30px; border: 1px solid var(--sbc-border); border-radius: 6px; background: rgba(255, 255, 255, 0.06); color: var(--sbc-text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: all 0.2s; } .sbc-ap-close:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } .sbc-ap-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--sbc-border); flex-shrink: 0; padding: 0 16px; background: rgba(0, 0, 0, 0.15); } .sbc-ap-tab { padding: 8px 16px; font-size: 12px; font-weight: 600; color: var(--sbc-text-secondary); cursor: pointer; border: none; background: none; border-bottom: 2px solid transparent; transition: all var(--sbc-transition); white-space: nowrap; } .sbc-ap-tab:hover { color: var(--sbc-text-bright); } .sbc-ap-tab.active { color: var(--sbc-gold); border-bottom-color: var(--sbc-gold); } .sbc-ap-content { flex: 1; overflow-y: auto; padding: 16px; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.15) transparent; } .sbc-ap-content::-webkit-scrollbar { width: 6px; } .sbc-ap-content::-webkit-scrollbar-track { background: transparent; } .sbc-ap-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 3px; } .sbc-ap-content::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); } /* v1.2.3: 紧凑 KPI 卡片 (参考 sglv stat-dash-card / trend-kpi-card 设计) */ .sbc-ap-stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; } .sbc-ap-stat-card { background: var(--sbc-bg-kpi); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-lg); padding: 10px 12px; text-align: center; display: flex; flex-direction: column; gap: 3px; transition: border-color 0.2s; } .sbc-ap-stat-card:hover { border-color: var(--sbc-border-hover); } .sbc-ap-stat-label { font-size: 10px; color: var(--sbc-text-secondary); line-height: 1.3; } .sbc-ap-stat-value { font-size: 20px; font-weight: 800; line-height: 1.2; color: #fff; } .sbc-ap-stat-desc { font-size: 10px; color: var(--sbc-text-secondary); } /* 颜色变体 */ .sbc-ap-stat-card.blue { border-color: var(--sbc-kpi-border-blue); } .sbc-ap-stat-card.blue .sbc-ap-stat-value { color: var(--sbc-kpi-blue); } .sbc-ap-stat-card.green { border-color: var(--sbc-kpi-border-green); } .sbc-ap-stat-card.green .sbc-ap-stat-value { color: var(--sbc-kpi-green); } .sbc-ap-stat-card.amber { border-color: var(--sbc-kpi-border-amber); } .sbc-ap-stat-card.amber .sbc-ap-stat-value { color: var(--sbc-kpi-amber); } .sbc-ap-stat-card.purple { border-color: var(--sbc-kpi-border-purple); } .sbc-ap-stat-card.purple .sbc-ap-stat-value { color: var(--sbc-kpi-purple); } .sbc-ap-stat-card.red { border-color: var(--sbc-kpi-border-red); } .sbc-ap-stat-card.red .sbc-ap-stat-value { color: var(--sbc-kpi-red); } .sbc-ap-stat-card.cyan { border-color: var(--sbc-kpi-border-cyan); } .sbc-ap-stat-card.cyan .sbc-ap-stat-value { color: var(--sbc-kpi-cyan); } .sbc-ap-section { margin-bottom: 20px; } .sbc-ap-section-title { font-size: 14px; font-weight: 700; color: var(--sbc-text-bright); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .sbc-ap-section-title::before { content: ''; width: 3px; height: 14px; border-radius: 2px; background: var(--sbc-gold); } .sbc-ap-rank-list { display: flex; flex-direction: column; gap: 5px; } .sbc-ap-rank-item { display: grid; grid-template-columns: 28px 46px minmax(0, 1fr) 100px 90px 70px; align-items: center; gap: 10px; padding: 7px 12px; background: rgba(255, 255, 255, 0.02); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); transition: all 0.2s; cursor: pointer; } .sbc-ap-rank-item:hover { background: rgba(255, 255, 255, 0.05); border-color: var(--sbc-border-hover); } .sbc-ap-rank-cover { width: 46px; height: 22px; border-radius: 3px; object-fit: cover; background: rgba(255, 255, 255, 0.04); flex-shrink: 0; } .sbc-ap-rank-num { width: 26px; height: 26px; border-radius: 6px; background: rgba(255, 255, 255, 0.06); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 800; color: var(--sbc-text-secondary); } .sbc-ap-rank-num--gold { background: linear-gradient(135deg, #c7a02e, #a67c00); color: #fff; } .sbc-ap-rank-num--silver { background: linear-gradient(135deg, #9ca3af, #6b7280); color: #fff; } .sbc-ap-rank-num--bronze { background: linear-gradient(135deg, #d97706, #b45309); color: #fff; } .sbc-ap-rank-name { font-size: 12px; font-weight: 600; color: var(--sbc-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-ap-rank-value { font-size: 11px; font-weight: 700; color: var(--sbc-gold); text-align: right; } .sbc-ap-rank-score { font-size: 11px; text-align: right; } .sbc-ap-rank-score--high { color: var(--sbc-emerald); } .sbc-ap-rank-score--mid { color: var(--sbc-warning); } .sbc-ap-rank-score--low { color: var(--sbc-text-secondary); } .sbc-ap-rank-tag { font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; text-align: center; } .sbc-ap-rank-tag--buy { background: rgba(16, 185, 129, 0.15); color: var(--sbc-emerald); } .sbc-ap-rank-tag--trade { background: rgba(168, 85, 247, 0.15); color: var(--sbc-purple); } .sbc-ap-rank-tag--craft { background: rgba(6, 182, 212, 0.15); color: var(--sbc-cyan); } .sbc-ap-rank-tag--drop { background: rgba(245, 158, 11, 0.15); color: var(--sbc-warning); } /* 分布条 */ .sbc-ap-dist-bar { display: flex; height: 22px; border-radius: 6px; overflow: hidden; gap: 2px; margin-bottom: 8px; } .sbc-ap-dist-segment { display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: #fff; transition: flex 0.2s; cursor: default; } .sbc-ap-dist-legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 10px; color: var(--sbc-text-secondary); } .sbc-ap-dist-legend-item { display: flex; align-items: center; gap: 6px; } .sbc-ap-dist-legend-dot { width: 10px; height: 10px; border-radius: 2px; } /* 价格表 */ .sbc-ap-price-table { width: 100%; border-collapse: separate; border-spacing: 0 4px; } .sbc-ap-price-table th { text-align: left; padding: 8px 12px; font-size: 11px; font-weight: 600; color: var(--sbc-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; } .sbc-ap-price-table td { padding: 10px 12px; font-size: 13px; color: var(--sbc-text-primary); background: rgba(255, 255, 255, 0.02); border-top: 1px solid rgba(255, 255, 255, 0.04); border-bottom: 1px solid rgba(255, 255, 255, 0.04); } .sbc-ap-price-table tbody tr { cursor: pointer; transition: background 0.2s; } .sbc-ap-price-table tbody tr:hover td { background: rgba(139, 92, 246, 0.06); } .sbc-ap-price-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.04); border-radius: var(--sbc-radius) 0 0 var(--sbc-radius); } .sbc-ap-price-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.04); border-radius: 0 var(--sbc-radius) var(--sbc-radius) 0; } .sbc-ap-price-value { font-weight: 600; color: var(--sbc-emerald); } .sbc-ap-price-total { font-weight: 700; color: var(--sbc-gold); } .sbc-ap-price-cover { width: 46px; height: 22px; border-radius: 3px; object-fit: cover; vertical-align: middle; background: rgba(255, 255, 255, 0.04); } .sbc-ap-no-data { text-align: center; padding: 40px 20px; color: var(--sbc-text-secondary); font-size: 14px; } .sbc-market-loading { display: flex; align-items: center; gap: 10px; padding: 14px 16px; font-size: 12.5px; color: var(--sbc-text-primary); background: linear-gradient(180deg, rgba(20, 30, 44, 0.6), rgba(15, 22, 32, 0.5)); border: 1px solid rgba(102, 192, 244, 0.12); border-radius: 10px; margin: 12px 0; } /* v1.2.10: 加载进度条(粘性底部) */ .sbc-market-progress { position: sticky; top: 0; z-index: 5; margin-bottom: 10px; padding: 8px 12px; background: linear-gradient(135deg, rgba(20, 30, 44, 0.95), rgba(15, 22, 32, 0.95)); border: 1px solid rgba(102, 192, 244, 0.18); border-radius: 10px; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--sbc-text-primary); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); } .sbc-market-progress-bar { flex: 1; height: 6px; background: rgba(255, 255, 255, 0.06); border-radius: 3px; overflow: hidden; position: relative; } .sbc-market-progress-fill { position: absolute; inset: 0 100% 0 0; background: linear-gradient(90deg, var(--sbc-accent), #4ade80); border-radius: 3px; transition: right 0.3s cubic-bezier(0.16, 1, 0.3, 1); box-shadow: 0 0 8px rgba(102, 192, 244, 0.5); } .sbc-market-progress-text { font-variant-numeric: tabular-nums; color: var(--sbc-accent); font-weight: 700; min-width: 80px; text-align: right; } /* ====== 响应式 ====== */ @media (min-width: 1600px) { .sbc-grid { grid-template-columns: repeat(7, 1fr); } } @media (max-width: 1400px) { .sbc-grid { grid-template-columns: repeat(5, 1fr); } } @media (max-width: 1200px) { .sbc-grid { grid-template-columns: repeat(4, 1fr); } .sbc-ap-rank-item { grid-template-columns: 28px 46px minmax(0, 1fr) 80px 70px; } .sbc-ap-rank-tag { display: none; } } @media (max-width: 768px) { .sbc-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; } .sbc-search { max-width: 100%; } .sbc-ap-rank-item { grid-template-columns: 28px 46px minmax(0, 1fr) 70px; } .sbc-ap-rank-score, .sbc-ap-rank-tag { display: none; } .sbc-ap-tabs { overflow-x: auto; } } @media (max-width: 480px) { .sbc-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; } } /* ====== v1.2.3: 勋章类型标识 ====== */ .sbc-card-type-badge { position: absolute; top: 6px; right: 6px; padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: 700; line-height: 1.4; z-index: 3; pointer-events: none; } .sbc-card-type-badge--activity { background: rgba(168, 85, 247, 0.85); color: #fff; } .sbc-card-type-badge--foil { background: rgba(199, 160, 46, 0.9); color: #1b2838; } .sbc-card-type-badge--game { background: rgba(102, 192, 244, 0.7); color: #1b2838; } .sbc-card-foil-border { border: 2px solid var(--sbc-gold) !important; box-shadow: 0 0 12px rgba(199, 160, 46, 0.4); } /* ====== v1.2.4: 统一游戏汇总视图 (卡牌+徽章聚合) ====== */ .sbc-ugv-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 10px 14px; background: var(--sbc-bg-kpi-section); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-lg); margin-bottom: 12px; } .sbc-ugv-tab { padding: 5px 14px; border: 1px solid var(--sbc-border); border-radius: 14px; background: transparent; color: var(--sbc-text-secondary); font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 5px; } .sbc-ugv-tab:hover { color: var(--sbc-text-bright); border-color: var(--sbc-border-hover); } .sbc-ugv-tab.active { background: rgba(102, 192, 244, 0.2); color: var(--sbc-accent); border-color: rgba(102, 192, 244, 0.4); } .sbc-ugv-tab-num { background: rgba(255, 255, 255, 0.1); padding: 1px 6px; border-radius: 8px; font-size: 10px; color: inherit; } .sbc-ugv-tab.active .sbc-ugv-tab-num { background: rgba(102, 192, 244, 0.3); } /* ====== v1.2.5: 汇总视图网格 (3列紧凑布局) ====== */ .sbc-ugv-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .sbc-ugv-card { background: var(--sbc-bg-kpi); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-lg); overflow: hidden; transition: all var(--sbc-transition); display: flex; flex-direction: column; } .sbc-ugv-card:hover { border-color: var(--sbc-border-hover); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); } .sbc-ugv-card-head { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: linear-gradient(135deg, rgba(27, 40, 56, 0.6), rgba(26, 58, 74, 0.3)); border-bottom: 1px solid var(--sbc-border); } .sbc-ugv-card-img { width: 40px; height: 40px; border-radius: 6px; background: var(--sbc-bg-secondary); flex-shrink: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; } .sbc-ugv-card-img img { width: 100%; height: 100%; object-fit: cover; } .sbc-ugv-card-title { flex: 1; min-width: 0; } .sbc-ugv-card-name { font-size: 12px; font-weight: 700; color: var(--sbc-text-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-ugv-card-sub { font-size: 10px; color: var(--sbc-text-secondary); margin-top: 1px; } .sbc-ugv-card-body { padding: 8px 10px; } .sbc-ugv-track { margin-bottom: 8px; } .sbc-ugv-track:last-child { margin-bottom: 0; } .sbc-ugv-track-header { display: flex; align-items: center; justify-content: space-between; font-size: 10px; margin-bottom: 4px; gap: 6px; } .sbc-ugv-track-label { display: inline-flex; align-items: center; gap: 4px; color: var(--sbc-text-secondary); font-weight: 600; } .sbc-ugv-track-label-foil { color: var(--sbc-gold); } .sbc-ugv-track-value { color: var(--sbc-text-primary); font-weight: 700; font-size: 11px; } .sbc-ugv-bar { height: 6px; border-radius: 3px; background: rgba(255, 255, 255, 0.06); overflow: hidden; display: flex; gap: 1px; } .sbc-ugv-bar-fill { height: 100%; background: linear-gradient(90deg, var(--sbc-accent), #88d5ff); transition: width 0.3s; } .sbc-ugv-bar-fill--complete { background: linear-gradient(90deg, var(--sbc-success), #a4d007); } .sbc-ugv-bar-fill--foil { background: linear-gradient(90deg, var(--sbc-gold), #ffd700); } .sbc-ugv-bar-fill--locked { background: rgba(231, 76, 60, 0.4); } .sbc-ugv-craftable-tag { display: inline-flex; align-items: center; gap: 3px; background: rgba(34, 197, 94, 0.18); color: var(--sbc-kpi-green); padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; } .sbc-ugv-locked-tag { display: inline-flex; align-items: center; gap: 3px; background: rgba(231, 76, 60, 0.18); color: var(--sbc-kpi-red); padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; } .sbc-ugv-maxed-tag { display: inline-flex; align-items: center; gap: 3px; background: rgba(199, 160, 46, 0.18); color: var(--sbc-gold); padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; } .sbc-ugv-foot { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-top: 1px solid var(--sbc-border); background: rgba(0, 0, 0, 0.15); font-size: 10px; } .sbc-ugv-foot-stats { display: flex; gap: 8px; color: var(--sbc-text-secondary); } .sbc-ugv-foot-stats strong { color: var(--sbc-text-bright); } .sbc-ugv-toggle { display: inline-flex; align-items: center; gap: 3px; color: var(--sbc-accent); cursor: pointer; user-select: none; font-size: 10px; font-weight: 600; } .sbc-ugv-toggle svg { transition: transform 0.2s; } .sbc-ugv-toggle.collapsed svg { transform: rotate(-90deg); } .sbc-ugv-detail { border-top: 1px solid var(--sbc-border); background: rgba(0, 0, 0, 0.2); padding: 10px 12px; max-height: 400px; overflow-y: auto; } .sbc-ugv-detail.collapsed { display: none; } .sbc-ugv-detail-title { font-size: 10px; color: var(--sbc-text-secondary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } .sbc-ugv-badge-list { display: flex; flex-direction: column; gap: 5px; } .sbc-ugv-badge-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 5px; background: rgba(255, 255, 255, 0.02); border: 1px solid var(--sbc-border); font-size: 11px; } .sbc-ugv-badge-row--foil { border-color: rgba(199, 160, 46, 0.3); background: rgba(199, 160, 46, 0.05); } .sbc-ugv-badge-row-img { width: 28px; height: 28px; border-radius: 4px; background: var(--sbc-bg-secondary); flex-shrink: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; } .sbc-ugv-badge-row-img img { width: 100%; height: 100%; object-fit: cover; } .sbc-ugv-badge-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--sbc-text-primary); } .sbc-ugv-badge-row-level { color: var(--sbc-gold); font-weight: 700; font-size: 10px; } .sbc-ugv-badge-row-xp { color: var(--sbc-accent); font-size: 10px; } .sbc-ugv-badge-row-action { color: var(--sbc-accent); cursor: pointer; font-size: 10px; opacity: 0.7; transition: opacity 0.2s; } .sbc-ugv-badge-row-action:hover { opacity: 1; } .sbc-ugv-empty { text-align: center; padding: 40px 20px; color: var(--sbc-text-secondary); } .sbc-ugv-empty-icon { font-size: 32px; margin-bottom: 10px; opacity: 0.5; } /* ====== v1.2.3: 活动勋章对比视图 ====== */ .sbc-activity-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; } .sbc-activity-card { background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); padding: 8px; transition: all var(--sbc-transition); position: relative; overflow: hidden; } .sbc-activity-card:hover { border-color: var(--sbc-accent); box-shadow: var(--sbc-shadow-hover); transform: translateY(-1px); } .sbc-activity-card--obtained { border-color: var(--sbc-success); background: linear-gradient(135deg, var(--sbc-bg-card), rgba(92, 126, 16, 0.08)); } .sbc-activity-card--retired { opacity: 0.75; } .sbc-activity-card--retired::after { content: '绝版'; position: absolute; top: 0; right: 0; background: var(--sbc-danger); color: #fff; font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 0 0 0 4px; } .sbc-activity-card-header { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } .sbc-activity-card-icon { width: 32px; height: 32px; border-radius: 4px; background: var(--sbc-bg-secondary); display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; } .sbc-activity-card-icon img { max-width: 100%; max-height: 100%; border-radius: 3px; } .sbc-activity-card-icon svg { width: 100%; height: 100%; border-radius: 3px; } .sbc-activity-card-name { font-size: 11px; font-weight: 600; color: var(--sbc-text-bright); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .sbc-activity-card-year { font-size: 9px; color: var(--sbc-text-secondary); } .sbc-activity-card-info { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; } .sbc-activity-tag { padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; } .sbc-activity-tag--sale { background: rgba(168, 85, 247, 0.2); color: var(--sbc-purple); } .sbc-activity-tag--collection { background: rgba(6, 182, 212, 0.2); color: var(--sbc-cyan); } .sbc-activity-tag--awards { background: rgba(199, 160, 46, 0.2); color: var(--sbc-gold); } .sbc-activity-tag--community { background: rgba(16, 185, 129, 0.2); color: var(--sbc-emerald); } .sbc-activity-tag--event { background: rgba(245, 158, 11, 0.2); color: var(--sbc-warning); } .sbc-activity-tag--obtained { background: rgba(92, 126, 16, 0.25); color: var(--sbc-success); } .sbc-activity-tag--missing { background: rgba(231, 76, 60, 0.2); color: var(--sbc-danger); } .sbc-activity-level { font-size: 10px; color: var(--sbc-text-secondary); margin-top: 3px; } /* v1.2.3: 活动勋章 KPI 汇总 (参考 sglv stats-dashboard 网格设计) */ .sbc-activity-summary { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; background: var(--sbc-bg-kpi-section); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius-lg); padding: 12px; } .sbc-activity-summary-item { background: var(--sbc-bg-kpi); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); padding: 10px 8px; text-align: center; display: flex; flex-direction: column; gap: 3px; } .sbc-activity-summary-num { font-size: 20px; font-weight: 800; line-height: 1.2; } .sbc-activity-summary-label { font-size: 10px; color: var(--sbc-text-secondary); } /* ====== v1.2.3: 卡牌详情弹窗 ====== */ .sbc-detail-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(11, 17, 32, 0.85); z-index: 100000; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; /* v1.2.14-fix6: 浮窗自身管理滚动,overlay 只需居中 */ padding: 20px; } .sbc-detail-overlay.active { display: flex; } .sbc-detail-panel { background: rgba(21, 30, 46, 0.97); border: 1px solid rgba(102, 192, 244, 0.2); border-radius: var(--sbc-radius-xl); max-width: 1200px; width: 100%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); overflow: hidden; /* v1.4.0: 统一浮窗宽度,限制最大高度,超出部分滚动 */ max-height: calc(100vh - 48px); display: flex; flex-direction: column; } .sbc-detail-header { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: linear-gradient(135deg, rgba(27, 40, 56, 0.97), rgba(26, 58, 74, 0.8)); border-bottom: 1px solid var(--sbc-border); } .sbc-detail-header img { width: 56px; height: 56px; border-radius: 8px; object-fit: cover; } .sbc-detail-title { font-size: 16px; font-weight: 700; color: var(--sbc-text-bright); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .sbc-detail-subtitle { font-size: 13px; color: var(--sbc-text-secondary); margin-top: 2px; } /* v1.2.14-fix7: 地区旗帜+货币符号徽章 */ .sbc-detail-region-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 4px; background: rgba(102, 192, 244, 0.1); border: 1px solid rgba(102, 192, 244, 0.25); font-size: 11px; font-weight: 600; color: var(--sbc-accent); flex-shrink: 0; margin-left: 4px; } .sbc-detail-region-flag { font-size: 14px; line-height: 1; display: inline-flex; align-items: center; } .sbc-detail-region-flag svg { width: 18px; height: 14px; border-radius: 2px; display: block; } .sbc-detail-region-currency { font-variant-numeric: tabular-nums; } .sbc-detail-close { margin-left: auto; background: none; border: none; cursor: pointer; color: var(--sbc-text-secondary); font-size: 22px; padding: 4px 8px; border-radius: 4px; transition: all var(--sbc-transition); flex-shrink: 0; } .sbc-detail-close:hover { background: var(--sbc-bg-hover); color: var(--sbc-text-bright); } /* v1.2.14-fix14: 标题栏右上角快捷链接组(fix15: 紧挨关闭按钮,无 margin-left:auto 避免与 close 的 auto 冲突产生间隙) */ .sbc-detail-quicklinks { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; margin-right: 2px; } .sbc-detail-quicklink { display: inline-flex; align-items: center; gap: 3px; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; color: var(--sbc-text-secondary); background: rgba(102,192,244,0.06); border: 1px solid rgba(102,192,244,0.15); text-decoration: none; transition: all var(--sbc-transition); white-space: nowrap; } .sbc-detail-quicklink:hover { background: rgba(102,192,244,0.15); color: var(--sbc-accent); border-color: rgba(102,192,244,0.4); } .sbc-detail-quicklink--accent { color: var(--sbc-accent); background: rgba(102,192,244,0.1); border-color: rgba(102,192,244,0.3); } .sbc-detail-quicklink--accent:hover { background: rgba(102,192,244,0.2); } .sbc-detail-body { padding: 14px 16px; overflow-y: auto; flex: 1 1 auto; } /* v1.2.14-fix6: 浮窗滚动条样式 */ .sbc-detail-body::-webkit-scrollbar { width: 8px; } .sbc-detail-body::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; } .sbc-detail-body::-webkit-scrollbar-thumb { background: rgba(102,192,244,0.3); border-radius: 4px; } .sbc-detail-body::-webkit-scrollbar-thumb:hover { background: rgba(102,192,244,0.5); } /* v1.4.0: 统一浮窗,移除 --large 变体(已合并到基础样式) */ .sbc-detail-overlay { padding: 24px 16px; } .sbc-detail-body { overflow-y: auto; flex: 1; padding: 16px 18px; } /* v1.2.14-fix2: 游戏详情浮窗 - 分区 */ .sbc-gdp-section { margin-bottom: 18px; } .sbc-gdp-section:last-child { margin-bottom: 0; } .sbc-gdp-section--badges { padding-bottom: 14px; border-bottom: 1px solid var(--sbc-border); } .sbc-gdp-section--cards { padding-top: 4px; } /* v1.2.14-fix2: 游戏详情浮窗 - 徽章列表 */ .sbc-gdp-badge-list { display: flex; flex-direction: column; gap: 6px; } .sbc-gdp-badge-row { display: flex; align-items: center; gap: 12px; padding: 8px 10px; background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); transition: all var(--sbc-transition); } .sbc-gdp-badge-row:hover { border-color: var(--sbc-accent); background: var(--sbc-bg-secondary); } .sbc-gdp-badge-row--foil { border-left: 3px solid #ffb84d; } .sbc-gdp-badge-row--completed { border-left: 3px solid var(--sbc-success); } .sbc-gdp-badge-row-synth { display: inline-block; padding: 1px 6px; border-radius: 3px; background: rgba(75, 175, 80, 0.18); color: var(--sbc-success); font-size: 10px; font-weight: 600; margin-left: 4px; vertical-align: middle; } .sbc-gdp-badge-row-img { width: 40px; height: 40px; flex: 0 0 auto; border-radius: 6px; overflow: hidden; background: var(--sbc-bg-secondary); } .sbc-gdp-badge-row-img img { width: 100%; height: 100%; object-fit: cover; } .sbc-gdp-badge-row-info { flex: 1; min-width: 0; } .sbc-gdp-badge-row-name { font-size: 13px; font-weight: 600; color: var(--sbc-text-primary); display: flex; align-items: center; gap: 4px; } .sbc-gdp-badge-row-name-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .sbc-gdp-badge-row-meta { font-size: 11px; color: var(--sbc-text-secondary); margin-top: 2px; } .sbc-gdp-badge-row-action { flex: 0 0 auto; padding: 4px 10px; border-radius: var(--sbc-radius); background: rgba(102, 192, 244, 0.12); color: var(--sbc-accent); font-size: 12px; font-weight: 600; cursor: pointer; transition: all var(--sbc-transition); } .sbc-gdp-badge-row-action:hover { background: rgba(102, 192, 244, 0.25); } .sbc-detail-info-grid { /* v1.2.14-fix6: 紧凑布局,5 列自适应 */ display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; margin-bottom: 8px; } .sbc-detail-info-item { background: var(--sbc-bg-kpi); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); padding: 6px 8px; text-align: center; } .sbc-detail-info-item--muted .sbc-detail-info-value { color: var(--sbc-text-secondary); } .sbc-detail-info-label { font-size: 9.5px; color: var(--sbc-text-secondary); margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.3px; } .sbc-detail-info-value { font-size: 13px; font-weight: 700; color: #fff; line-height: 1.2; } .sbc-detail-cards-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--sbc-text-primary); } .sbc-detail-cards-grid { /* v1.5.0: 6列布局,更大封面展示(参考原版Steam) */ display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; } @media (max-width: 1400px) { .sbc-detail-cards-grid { grid-template-columns: repeat(6, 1fr); } } @media (max-width: 1200px) { .sbc-detail-cards-grid { grid-template-columns: repeat(5, 1fr); } } @media (max-width: 1000px) { .sbc-detail-cards-grid { grid-template-columns: repeat(4, 1fr); } } @media (max-width: 800px) { .sbc-detail-cards-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 600px) { .sbc-detail-cards-grid { grid-template-columns: repeat(2, 1fr); } } .sbc-detail-card-item { background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); padding: 8px; text-align: center; transition: all var(--sbc-transition); } .sbc-detail-card-item:hover { border-color: var(--sbc-gold); box-shadow: 0 0 0 1px var(--sbc-gold); } .sbc-detail-card-item img { width: 100%; max-width: 140px; height: auto; border-radius: 4px; margin-bottom: 6px; } /* v1.5.0: 统一名称区域高度,防止单行/双行名称导致卡片高度不齐 */ .sbc-detail-card-name { font-size: 11px; color: var(--sbc-text-secondary); line-height: 1.3; word-break: break-word; min-height: 1.3em; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .sbc-detail-card-price { font-size: 12px; font-weight: 600; color: var(--sbc-emerald); margin-top: 2px; } .sbc-detail-card-price--none { color: var(--sbc-text-secondary); } .sbc-detail-actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; } /* v1.2.14: 详情浮窗按需抓取卡牌图片/价格 */ .sbc-detail-card-link { display: block; text-decoration: none; color: inherit; } .sbc-detail-card-meta { font-size: 10px; color: var(--sbc-text-secondary); margin-top: 2px; } .sbc-detail-cards-status { display: flex; align-items: center; gap: 10px; padding: 14px 2px; font-size: 13px; color: var(--sbc-text-secondary); } .sbc-detail-cards-status .sbc-market-spinner { flex: 0 0 auto; } .sbc-detail-cards-status--error { color: #ff6b5a; } /* v1.2.14-fix4: 市场空数据提示 */ .sbc-detail-cards-status--empty { color: var(--sbc-text-secondary); background: rgba(255, 184, 77, 0.08); border: 1px dashed rgba(255, 184, 77, 0.3); border-radius: var(--sbc-radius); padding: 10px 14px; margin-bottom: 12px; display: flex; align-items: center; gap: 10px; } .sbc-detail-retry-btn { margin-left: 10px; padding: 4px 12px; border-radius: var(--sbc-radius); border: 1px solid rgba(102, 192, 244, 0.35); background: rgba(42, 71, 94, 0.9); color: var(--sbc-accent); font-size: 12px; cursor: pointer; } .sbc-detail-retry-btn:hover { border-color: var(--sbc-accent); } /* v1.2.14: 卡牌区工具栏(封面/列表切换 + 求购价按钮) */ .sbc-detail-cards-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; } .sbc-detail-cards-toolbar .sbc-detail-cards-title { margin-bottom: 0; flex: 1 1 auto; } .sbc-detail-cards-view-toggle { display: inline-flex; border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); overflow: hidden; background: var(--sbc-bg-secondary); } .sbc-detail-cards-view-btn { padding: 4px 12px; border: none; background: transparent; color: var(--sbc-text-secondary); font-size: 12px; cursor: pointer; transition: all var(--sbc-transition); } .sbc-detail-cards-view-btn:hover { color: var(--sbc-text-primary); } .sbc-detail-cards-view-btn.active { background: var(--sbc-accent); color: #0a1416; font-weight: 600; } .sbc-detail-cards-buyorder-btn { padding: 4px 12px; border-radius: var(--sbc-radius); border: 1px solid rgba(255, 184, 77, 0.35); background: rgba(94, 71, 42, 0.5); color: #ffb84d; font-size: 12px; cursor: pointer; transition: all var(--sbc-transition); } .sbc-detail-cards-buyorder-btn:hover:not(:disabled) { border-color: #ffb84d; background: rgba(94, 71, 42, 0.8); } .sbc-detail-cards-buyorder-btn:disabled { opacity: 0.6; cursor: wait; } .sbc-detail-cards-container { display: flex; flex-direction: column; gap: 16px; } /* v1.2.14-fix: 移除 .hidden 规则(改为只渲染当前激活视图,不再需要 CSS 切换) */ /* v1.2.14: 普通/闪卡分区 */ .sbc-detail-cards-section { display: flex; flex-direction: column; gap: 8px; } .sbc-detail-cards-section--foil .sbc-detail-cards-section-title { color: #ffb84d; } .sbc-detail-cards-section-title { font-size: 13px; font-weight: 600; color: var(--sbc-text-primary); display: flex; align-items: center; gap: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--sbc-border); } .sbc-detail-cards-section-meta { font-size: 11px; font-weight: 400; color: var(--sbc-text-secondary); } /* v1.2.14: 封面视图卡牌(增强) */ .sbc-detail-card-item--foil { border-color: rgba(255, 184, 77, 0.4); box-shadow: 0 0 0 1px rgba(255, 184, 77, 0.15); } .sbc-detail-card-item--foil:hover { border-color: #ffb84d; } /* v1.2.14-fix: 未拥有卡牌灰色遮罩 */ .sbc-detail-card-item--unowned { position: relative; } .sbc-detail-card-item--unowned img { filter: grayscale(1) brightness(0.5) opacity(0.6); } .sbc-detail-card-item--unowned::after { content: ''; position: absolute; inset: 0; border-radius: var(--sbc-radius); background: rgba(13, 17, 23, 0.45); pointer-events: none; z-index: 1; } .sbc-detail-card-item--unowned .sbc-detail-card-name { color: var(--sbc-text-secondary); opacity: 0.65; } .sbc-detail-card-item--unowned .sbc-detail-card-prices { opacity: 0.5; } .sbc-detail-card-item--unowned:hover { border-color: var(--sbc-border); } .sbc-detail-card-item--unowned:hover img { filter: grayscale(0.8) brightness(0.7) opacity(0.8); } .sbc-detail-card-img-placeholder { width: 100%; height: 70px; display: flex; align-items: center; justify-content: center; background: var(--sbc-bg-secondary); border-radius: 4px; font-size: 24px; margin-bottom: 6px; } .sbc-detail-card-owned-mark { position: absolute; top: 4px; right: 4px; width: 18px; height: 18px; background: var(--sbc-emerald); color: #0a1416; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; } .sbc-detail-card-item { position: relative; } .sbc-detail-card-prices { display: flex; flex-direction: column; gap: 1px; margin-top: 4px; } .sbc-detail-card-price { font-size: 11px; font-weight: 600; line-height: 1.3; } .sbc-detail-card-price--sell { color: var(--sbc-emerald); } .sbc-detail-card-price--net { color: var(--sbc-accent); font-size: 10px; } .sbc-detail-card-price--buy { color: #ffb84d; } .sbc-detail-card-price--none { color: var(--sbc-text-secondary); font-weight: 400; } .sbc-detail-card-price--pending { color: var(--sbc-text-secondary); font-weight: 400; opacity: 0.6; } .sbc-detail-card-price--error { color: #ff6b5a; font-size: 10px; } .sbc-detail-card-foil-tag { font-size: 9px; font-style: normal; padding: 1px 4px; border-radius: 3px; background: rgba(255, 184, 77, 0.2); color: #ffb84d; margin-left: 4px; vertical-align: middle; } /* v1.5.0: 闪卡封面右上角图标标记(替代文字标签,避免换行) */ .sbc-detail-card-foil-badge { position: absolute; top: 4px; left: 4px; width: 20px; height: 20px; background: linear-gradient(135deg, #ffb84d, #ff9500); color: #1a1a2e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; line-height: 1; box-shadow: 0 1px 4px rgba(255, 184, 77, 0.4); z-index: 2; } /* v1.2.14: 列表视图卡牌行(参考 steam-store-card-prices.sscp-row) */ .sbc-detail-cards-list { display: flex; flex-direction: column; gap: 4px; } .sbc-detail-card-list-row { display: flex; align-items: center; gap: 10px; padding: 6px 8px; background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); text-decoration: none; color: inherit; transition: all var(--sbc-transition); } .sbc-detail-card-list-row:hover { border-color: var(--sbc-accent); background: var(--sbc-bg-secondary); } .sbc-detail-card-list-row--foil { border-left: 3px solid #ffb84d; } /* v1.2.14-fix: 未拥有卡牌列表行灰色遮罩 */ .sbc-detail-card-list-row--unowned { opacity: 0.6; } .sbc-detail-card-list-row--unowned .sbc-detail-card-list-icon { filter: grayscale(1) brightness(0.6); } .sbc-detail-card-list-row--unowned .sbc-detail-card-list-name { color: var(--sbc-text-secondary); } .sbc-detail-card-list-row--unowned:hover { opacity: 0.85; } .sbc-detail-card-list-owned { color: var(--sbc-emerald); font-weight: 700; width: 14px; text-align: center; flex: 0 0 auto; } .sbc-detail-card-list-unowned { color: var(--sbc-text-secondary); width: 14px; text-align: center; flex: 0 0 auto; } .sbc-detail-card-list-icon { width: 32px; height: 22px; object-fit: cover; border-radius: 3px; flex: 0 0 auto; background: var(--sbc-bg-secondary); } .sbc-detail-card-list-icon--empty { display: inline-flex; align-items: center; justify-content: center; font-size: 14px; } .sbc-detail-card-list-main { flex: 1 1 auto; min-width: 0; } .sbc-detail-card-list-name { font-size: 12px; color: var(--sbc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; max-width: 100%; } .sbc-detail-card-list-prices { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; } .sbc-detail-card-list-prices .sbc-detail-card-price { font-size: 11px; } .sbc-detail-card-list-prices .sbc-detail-card-meta { margin-top: 0; } /* v1.2.14: 统一游戏汇总 - 卡牌分区 */ .sbc-ugv-detail-section { margin-bottom: 12px; } .sbc-ugv-detail-section:last-child { margin-bottom: 0; } .sbc-ugv-detail-section--cards { margin-top: 16px; padding-top: 12px; border-top: 1px dashed var(--sbc-border); } .sbc-ugv-cards-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; } .sbc-ugv-cards-title { font-size: 13px; font-weight: 600; color: var(--sbc-text-primary); flex: 1 1 auto; } .sbc-ugv-cards-placeholder { padding: 16px 12px; text-align: center; font-size: 12px; color: var(--sbc-text-secondary); background: var(--sbc-bg-secondary); border-radius: var(--sbc-radius); border: 1px dashed var(--sbc-border); } /* 统一游戏卡片内的卡牌网格(v1.5.0: 6列布局,更大封面) */ .sbc-ugv-detail-section--cards .sbc-detail-cards-grid { gap: 8px; } .sbc-ugv-detail-section--cards .sbc-detail-card-item { padding: 6px; } .sbc-ugv-detail-section--cards .sbc-detail-card-item img { max-width: 120px; } /* v1.4.0: 分组视图徽章行内联卡牌封面视图(替代浮窗) */ /* v1.5.0: 改为6列,更大封面展示(参考原版Steam) */ .sbc-badge-row-inline-cards { padding: 10px 12px; background: rgba(0, 0, 0, 0.2); border: 1px solid var(--sbc-border); border-radius: var(--sbc-radius); margin-top: 4px; animation: sbc-card-enter 0.3s ease; } .sbc-badge-row-inline-cards-title { font-size: 12px; font-weight: 600; color: var(--sbc-text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 8px; } .sbc-badge-row-inline-cards-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; } @media (max-width: 1400px) { .sbc-badge-row-inline-cards-grid { grid-template-columns: repeat(6, 1fr); } } @media (max-width: 1200px) { .sbc-badge-row-inline-cards-grid { grid-template-columns: repeat(5, 1fr); } } @media (max-width: 1000px) { .sbc-badge-row-inline-cards-grid { grid-template-columns: repeat(4, 1fr); } } @media (max-width: 800px) { .sbc-badge-row-inline-cards-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 600px) { .sbc-badge-row-inline-cards-grid { grid-template-columns: repeat(2, 1fr); } } .sbc-badge-row-inline-cards-grid .sbc-detail-card-item { padding: 6px; } .sbc-badge-row-inline-cards-grid .sbc-detail-card-item img { max-width: 120px; } .sbc-badge-row-inline-cards-grid .sbc-detail-card-name { font-size: 11px; } .sbc-badge-row-inline-cards-loading { display: flex; align-items: center; gap: 8px; padding: 12px; font-size: 12px; color: var(--sbc-text-secondary); } /* ============================================================ v1.3.1: 性能与 UI 交互增强 CSS ============================================================ */ /* --- 视图切换过渡动画 --- */ .sbc-view-enter { animation: sbcViewFadeIn var(--sbc-transition-ms, 280ms) cubic-bezier(0.22, 0.61, 0.36, 1) both; } @keyframes sbcViewFadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } .sbc-view-exit { animation: sbcViewFadeOut var(--sbc-transition-ms, 280ms) cubic-bezier(0.22, 0.61, 0.36, 1) both; } @keyframes sbcViewFadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-8px); } } /* --- 加载骨架屏 --- */ .sbc-skeleton-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; padding: 4px 0; } .sbc-skeleton-card { background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: 10px; overflow: hidden; animation: sbcShimmer 1.4s ease-in-out infinite; } .sbc-skeleton-img { width: 100%; aspect-ratio: 4 / 3; background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 37%, rgba(255,255,255,0.04) 63%); background-size: 400% 100%; } .sbc-skeleton-body { padding: 10px 12px; } .sbc-skeleton-line { height: 10px; border-radius: 4px; margin-bottom: 6px; background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 37%, rgba(255,255,255,0.04) 63%); background-size: 400% 100%; } .sbc-skeleton-line--w60 { width: 60%; } .sbc-skeleton-line--w40 { width: 40%; } @keyframes sbcShimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } /* --- v1.5.2: 增强卡片 hover 改为金色边框高亮(无位移/缩放) --- */ .sbc-card { transition: box-shadow 0.2s ease, border-color 0.2s ease; } .sbc-card:hover { box-shadow: 0 0 0 2px var(--sbc-gold), 0 4px 16px rgba(0, 0, 0, 0.4); border-color: var(--sbc-gold); z-index: 5; } .sbc-card:active { box-shadow: 0 0 0 1px var(--sbc-gold); } .sbc-card-img-wrap img { transition: filter 0.3s ease; } .sbc-card-detail-btn { transition: background 0.15s ease, color 0.15s ease, opacity 0.2s ease; opacity: 0.7; } .sbc-card:hover .sbc-card-detail-btn { opacity: 1; } /* --- 增强工具栏按钮交互 --- */ .sbc-btn { transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.15s ease, border-color 0.15s ease; } .sbc-btn:hover { transform: translateY(-1px); } .sbc-btn:active { transform: translateY(0); } .sbc-btn--accent { box-shadow: 0 0 0 1px rgba(102, 192, 244, 0.3); } /* --- 响应式工具栏:小屏溢出处理 --- */ @media (max-width: 1100px) { .sbc-toolbar { flex-wrap: wrap; gap: 6px; } .sbc-toolbar-left, .sbc-toolbar-right { flex-wrap: wrap; } } @media (max-width: 768px) { .sbc-toolbar-left .sbc-select { max-width: 120px; } .sbc-btn { padding: 4px 8px; font-size: 11px; } .sbc-btn svg { width: 12px; height: 12px; } } /* --- 增强分页按钮交互 --- */ .sbc-page-btn { transition: background 0.12s ease, transform 0.12s ease; } .sbc-page-btn:not(:disabled):hover { transform: translateY(-1px); } .sbc-page-btn:not(:disabled):active { transform: translateY(0); } /* --- 增强进度条动画 --- */ .sbc-card-progress-fill { transition: width 0.5s cubic-bezier(0.22, 0.61, 0.36, 1); } /* --- 键盘快捷键提示 --- */ .sbc-kbd { display: inline-block; padding: 1px 5px; font-size: 10px; font-family: monospace; line-height: 16px; color: var(--sbc-text-secondary); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 3px; vertical-align: middle; } .sbc-kbd-hint { position: fixed; bottom: 12px; right: 12px; z-index: 9000; display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 11px; color: var(--sbc-text-secondary); background: rgba(20, 27, 50, 0.92); border: 1px solid var(--sbc-border); border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.3); opacity: 0; transform: translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; } .sbc-kbd-hint--visible { opacity: 1; transform: translateY(0); } /* --- 懒加载图片占位 --- */ .sbc-lazy-img { background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.06) 37%, rgba(255,255,255,0.03) 63%); background-size: 200% 100%; animation: sbcShimmer 1.6s ease-in-out infinite; } .sbc-lazy-img.sbc-lazy-loaded { background: none; animation: none; } /* v1.5.5: 图片加载失败占位 — 隐藏破图图标,用柔和渐变兜底 */ .sbc-lazy-img.sbc-img-fallback { background: linear-gradient(135deg, rgba(102,192,244,0.08) 0%, rgba(42,71,94,0.25) 100%); animation: none; opacity: 0.6; } /* ====== v1.3.6: 热门市场物品样式 ====== */ .sbc-popular-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; } .sbc-popular-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 14px; } .sbc-popular-list { display: flex; flex-direction: column; gap: 8px; } .sbc-popular-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: var(--sbc-bg-secondary); border: 1px solid var(--sbc-border); border-radius: 8px; transition: border-color .15s, transform .12s; cursor: pointer; } .sbc-popular-item:hover { border-color: var(--sbc-accent); transform: translateX(2px); } .sbc-popular-rank { width: 28px; height: 28px; border-radius: 50%; background: var(--sbc-bg-kpi); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; color: var(--sbc-accent); flex-shrink: 0; } .sbc-popular-item:nth-child(1) .sbc-popular-rank { background: var(--sbc-gold); color: #1b2838; } .sbc-popular-item:nth-child(2) .sbc-popular-rank { background: #b8c3cc; color: #1b2838; } .sbc-popular-item:nth-child(3) .sbc-popular-rank { background: #cd7f32; color: #fff; } .sbc-popular-img { width: 48px; height: 48px; border-radius: 6px; object-fit: cover; flex-shrink: 0; background: #000; } .sbc-popular-body { flex: 1; min-width: 0; } .sbc-popular-name { font-size: 13px; color: var(--sbc-text-bright); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-popular-name a { color: var(--sbc-text-bright); text-decoration: none; } .sbc-popular-name a:hover { color: var(--sbc-accent); } .sbc-popular-meta { font-size: 11px; color: var(--sbc-text-secondary); margin-top: 2px; display: flex; gap: 8px; flex-wrap: wrap; } .sbc-popular-price { font-size: 14px; font-weight: 700; color: var(--sbc-emerald); flex-shrink: 0; } .sbc-popular-qty { font-size: 11px; color: var(--sbc-accent-blue); font-weight: 600; } .sbc-popular-empty { padding: 32px; text-align: center; color: var(--sbc-text-secondary); font-size: 13px; } .sbc-popular-card-tag { display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px; background: rgba(139, 92, 246, .2); color: var(--sbc-accent); border: 1px solid rgba(139, 92, 246, .3); } .sbc-popular-refresh-btn { background: var(--sbc-bg-secondary); color: var(--sbc-text-primary); border: 1px solid var(--sbc-border); border-radius: 6px; padding: 6px 14px; font-size: 12px; cursor: pointer; transition: all .15s; } .sbc-popular-refresh-btn:hover { background: var(--sbc-bg-hover); border-color: var(--sbc-accent); } .sbc-popular-refresh-btn:disabled { opacity: 0.5; cursor: wait; } /* ====== v1.3.6: AI 洞察分析样式 ====== */ .sbc-ai-insight-hero { background: linear-gradient(135deg, rgba(139, 92, 246, 0.12), rgba(102, 192, 244, 0.08)); border: 1px solid rgba(139, 92, 246, 0.25); border-radius: 12px; padding: 16px; margin-bottom: 14px; } .sbc-ai-insight-summary { font-size: 14px; line-height: 1.7; color: var(--sbc-text-primary); } .sbc-ai-insight-summary strong { color: var(--sbc-text-bright); } .sbc-ai-insight-section-title { font-size: 15px; font-weight: 700; color: var(--sbc-text-bright); margin: 18px 0 10px; display: flex; align-items: center; gap: 8px; } .sbc-ai-insight-section-icon { width: 28px; height: 28px; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .sbc-ai-insight-section-icon svg { width: 16px; height: 16px; display: block; } .sbc-ai-rec-list { display: flex; flex-direction: column; gap: 10px; } .sbc-ai-rec-item { display: flex; gap: 12px; padding: 12px; background: var(--sbc-bg-secondary); border: 1px solid var(--sbc-border); border-radius: 10px; transition: all .15s; } .sbc-ai-rec-item:hover { border-color: var(--sbc-border-hover); transform: translateX(2px); } .sbc-ai-rec-action { width: 56px; height: 56px; border-radius: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; text-align: center; line-height: 1.3; gap: 3px; } .sbc-ai-rec-action svg { width: 18px; height: 18px; display: block; } .sbc-ai-rec-action--buy { background: rgba(74, 222, 128, .15); color: var(--sbc-success); border: 1px solid rgba(74, 222, 128, .3); } .sbc-ai-rec-action--sell { background: rgba(231, 76, 60, .15); color: var(--sbc-danger); border: 1px solid rgba(231, 76, 60, .3); } .sbc-ai-rec-action--hold { background: rgba(148, 163, 184, .15); color: var(--sbc-text-secondary); border: 1px solid rgba(148, 163, 184, .3); } .sbc-ai-rec-action--craft { background: rgba(255, 184, 77, .15); color: var(--sbc-gold); border: 1px solid rgba(255, 184, 77, .3); } .sbc-ai-rec-body { flex: 1; min-width: 0; } .sbc-ai-rec-name { font-size: 14px; font-weight: 600; color: var(--sbc-text-bright); margin-bottom: 4px; } .sbc-ai-rec-reason { font-size: 12px; color: var(--sbc-text-secondary); line-height: 1.5; } .sbc-ai-insight-trigger { text-align: center; padding: 32px 16px; } .sbc-ai-insight-trigger-icon { width: 64px; height: 64px; margin: 0 auto 12px; border-radius: 16px; background: linear-gradient(135deg, rgba(139,92,246,.2), rgba(102,192,244,.15)); border: 1px solid rgba(139,92,246,.3); display: flex; align-items: center; justify-content: center; } .sbc-ai-insight-trigger-icon svg { width: 32px; height: 32px; display: block; } .sbc-ai-insight-trigger-btn { background: linear-gradient(135deg, var(--sbc-accent), var(--sbc-accent-blue)); color: #fff; border: none; border-radius: 10px; padding: 12px 32px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all .2s; box-shadow: 0 4px 16px rgba(139, 92, 246, .3); } .sbc-ai-insight-trigger-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(139, 92, 246, .4); } .sbc-ai-insight-trigger-btn:disabled { opacity: 0.6; cursor: wait; transform: none; box-shadow: none; } .sbc-ai-insight-trigger-desc { font-size: 12px; color: var(--sbc-text-secondary); margin-top: 12px; line-height: 1.6; } .sbc-ai-insight-loading { text-align: center; padding: 40px 16px; } .sbc-ai-insight-loading .sbc-market-spinner { width: 32px; height: 32px; margin: 0 auto 12px; } .sbc-ai-insight-loading-text { font-size: 13px; color: var(--sbc-text-secondary); } .sbc-ai-insight-error { padding: 16px; background: rgba(231, 76, 60, .08); border: 1px solid rgba(231, 76, 60, .2); border-radius: 8px; color: var(--sbc-danger); font-size: 13px; line-height: 1.6; } .sbc-ai-insight-config-hint { padding: 12px 14px; background: rgba(255, 184, 77, .08); border: 1px solid rgba(255, 184, 77, .2); border-radius: 8px; font-size: 12px; color: var(--sbc-gold); line-height: 1.6; } .sbc-ai-insight-config-hint a { color: var(--sbc-accent-blue); text-decoration: underline; cursor: pointer; } .sbc-ai-insight-radar { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 14px; } .sbc-ai-insight-dim { background: var(--sbc-bg-secondary); border: 1px solid var(--sbc-border); border-radius: 10px; padding: 12px 8px; text-align: center; transition: all .2s; } .sbc-ai-insight-dim:hover { border-color: var(--sbc-border-hover); transform: translateY(-2px); } .sbc-ai-insight-dim-label { font-size: 10px; color: var(--sbc-text-secondary); text-transform: uppercase; letter-spacing: .5px; } .sbc-ai-insight-dim-value { font-size: 24px; font-weight: 700; margin-top: 4px; } .sbc-ai-insight-dim-bar { height: 4px; border-radius: 2px; background: var(--sbc-bg-kpi); margin-top: 8px; overflow: hidden; } .sbc-ai-insight-dim-bar-fill { height: 100%; border-radius: 2px; transition: width .4s; } `; // ============================================================ // 全局状态 // ============================================================ let allBadges = []; let filteredBadges = []; let currentPage = 1; let cardViewVisible = true; let viewMode = 'grid'; // 'grid' | 'grouped' | 'alldb' let analysisVisible = false; let analysisActiveTab = 'overview'; let marketPrices = {}; let marketLoading = false; // v1.2.10 修复: 单一进行中 promise,防止后台预加载和手动点击并发导致计数翻倍 let _marketLoadPromise = null; // v1.2.10: 性能优化新增状态 let marketInflight = new Set(); // 正在请求中的 appId,用于去重 let marketDone = new Set(); // 已成功(或失败)处理的 appId let marketConsecutiveFails = 0; // 连续失败计数 let marketProgress = { total: 0, done: 0, successCount: 0, startedAt: 0 }; let marketPauseUntil = 0; // 被限流后暂停至该时间戳 let valueScores = {}; let craftableBadges = []; // v1.2.1: SteamCardExchange 卡牌数据库 let cardDatabase = {}; // { appId: { name, size } } let cardDbLoaded = false; let cardDbLoading = false; let ownedAppIds = {}; // 用户已拥有的 appId -> true // v1.2.3: 勋章分类状态 let activityBadges = []; // 活动勋章列表 let gameBadges = []; // 游戏勋章列表 // v1.3.6: 热门市场物品状态 let popularMarketItems = []; // 热门市场物品列表 let popularMarketLoading = false; let popularMarketError = null; // v1.3.6: AI 洞察分析状态 let aiInsightResult = null; // AI 洞察分析结果 let aiInsightLoading = false; let aiInsightError = null; // ============================================================ // 工具函数 // ============================================================ // v1.5.5: CSS.escape 兼容兜底(数字 appId 本身安全,此处防御性处理) var _cssEscape = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape : function(s) { return String(s).replace(/[^a-zA-Z0-9_-]/g, '\\$&'); }; function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>"']/g, function(c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } // v1.3.0: 游戏封面图标 URL (参考 wishlist sidebar CDN 逻辑) var SBC_CDN_BASE = 'https://cdn.cloudflare.steamstatic.com/steam/apps/'; // v1.3.3: 占位图 data URI 中所有单引号改用 %27 编码 // 原因: SBC_COVER_PLACEHOLDER 被嵌入 onerror 内联 JS 的单引号字符串中, // 未编码的单引号会截断字符串导致 SyntaxError: Unexpected identifier 'http' var SBC_COVER_PLACEHOLDER = "data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%2046%2022%27%3E%3Crect%20fill=%27%23141b32%27%20width=%2746%27%20height=%2722%27%20rx=%273%27/%3E%3Ctext%20x=%2750%25%27%20y=%2750%25%27%20text-anchor=%27middle%27%20dy=%27.35em%27%20fill=%27%23475569%27%20font-size=%278%27%3E🎮%3C/text%3E%3C/svg%3E"; function getGameCoverHtml(appId) { if (!appId) return ''; // v1.3.1 R2: 多 CDN 回退链(cloudflare → akamai → shared → 占位图) var urls = CONFIG.CDN_CHAIN.map(function(cdn) { return cdn + appId + '/capsule_184x69.jpg'; }); var primaryUrl = urls[0]; var cdnUrlsAttr = JSON.stringify(urls); // onerror 逐个尝试 CDN 链中的下一个,全部失败后回退占位图 var onerrorHandler = "var i=parseInt(this.dataset.cdnIdx||'0')+1;var u=JSON.parse(this.dataset.cdnUrls||'[]');if(i'; } // v1.3.5: 游戏封面 标签生成(引用 @require sglv-cover-fallback.lib.js) // 当 SGLVCoverFallback 库可用时,使用其三级降级链(Akamai→Cloudflare→appdetails API→SVG); // 库不可用时安全降级到内置 CDN 单链。 // img 标签携带 data-sglv-cover="1" + data-appid,由 bindErrorHandler 全局委托自动降级。 function getCoverImgTag(appId, opts) { opts = opts || {}; var extraClass = opts.className || ''; var lazyAttr = opts.lazy !== false ? ' loading="lazy"' : ''; var cls = extraClass ? (' class="' + extraClass + '"') : ''; if (!appId) { var ph = (window.SGLVCoverFallback && window.SGLVCoverFallback.SVG_PLACEHOLDER) || SBC_COVER_PLACEHOLDER; return ''; } var coverUrl; if (window.SGLVCoverFallback && window.SGLVCoverFallback.getCoverUrl) { coverUrl = window.SGLVCoverFallback.getCoverUrl(appId); } else { // 兜底:cloudflare capsule(兼容 lib 未加载场景) coverUrl = SBC_CDN_BASE + appId + '/capsule_184x69.jpg'; } return ''; } // v1.3.1: 增强 Toast — 支持堆叠、自定义时长、进度模式 var _toastStack = []; function showToast(message, type, opts) { type = type || 'info'; opts = opts || {}; var duration = opts.duration || CONFIG.TOAST_DEFAULT_DURATION; var stack = opts.stack !== false; // 默认堆叠 if (!stack) { // 非堆叠模式:移除旧 toast _toastStack.forEach(function(t) { t.remove(); }); _toastStack = []; } // 限制堆叠数量 if (_toastStack.length >= 4) { var oldest = _toastStack.shift(); if (oldest) oldest.remove(); } var toast = document.createElement('div'); toast.className = 'sbc-toast' + (type === 'success' ? ' sbc-toast--success' : '') + (type === 'error' ? ' sbc-toast--error' : '') + (type === 'warning' ? ' sbc-toast--warning' : ''); if (opts.html) toast.innerHTML = message; else toast.textContent = message; // 堆叠定位 if (stack && _toastStack.length > 0) { var offset = _toastStack.reduce(function(sum, t) { return sum + t.offsetHeight + CONFIG.TOAST_STACK_GAP; }, 0); toast.style.bottom = (16 + offset) + 'px'; } document.body.appendChild(toast); _toastStack.push(toast); // 进度条模式:不自动消失,由调用方关闭 if (opts.persistent) return toast; var timer = setTimeout(function() { _dismissToast(toast); }, duration); // 鼠标悬停暂停自动消失 toast.addEventListener('mouseenter', function() { clearTimeout(timer); toast.style.opacity = '1'; }); toast.addEventListener('mouseleave', function() { timer = setTimeout(function() { _dismissToast(toast); }, 1000); }); return toast; } function _dismissToast(toast) { if (!toast || !toast.parentNode) return; toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(function() { toast.remove(); var idx = _toastStack.indexOf(toast); if (idx >= 0) _toastStack.splice(idx, 1); // 重新排列剩余 toast var offset = 0; _toastStack.forEach(function(t) { t.style.bottom = (16 + offset) + 'px'; offset += t.offsetHeight + CONFIG.TOAST_STACK_GAP; }); }, 300); } /** * v1.2.10: 显示/更新市场价格加载进度条(粘性顶部,实时刷新) * 用法: showMarketProgress({ done, total, success }) * showMarketProgress(null) 关闭 */ function showMarketProgress(progress) { var existing = document.getElementById('sbc-market-progress'); if (!progress) { if (existing) existing.remove(); return; } if (!existing) { existing = document.createElement('div'); existing.id = 'sbc-market-progress'; existing.className = 'sbc-market-progress'; existing.innerHTML = '' + '加载市场价格中...' + '' + '0/0'; // 插到 sbc-root 顶部 var root = document.getElementById('sbc-root'); if (root && root.firstChild) root.insertBefore(existing, root.firstChild); else if (root) root.appendChild(existing); else document.body.appendChild(existing); } var pct = progress.total > 0 ? Math.min(100, Math.round((progress.done / progress.total) * 100)) : 0; // v1.2.10 修复: done 兜底不超过 total,避免出现 "260/150" 这种显示异常 var shownDone = Math.min(progress.done || 0, progress.total || 0); var fillEl = existing.querySelector('.sbc-market-progress-fill'); var textEl = existing.querySelector('.sbc-market-progress-text'); var labelEl = existing.querySelector('#sbc-market-progress-label'); if (fillEl) fillEl.style.right = (100 - pct) + '%'; if (textEl) textEl.textContent = shownDone + '/' + progress.total + ' (' + pct + '%)'; if (labelEl && progress.success != null) { labelEl.textContent = '加载市场价格 · 成功 ' + progress.success; } } function debounce(fn, delay) { var timer = null; return function() { var ctx = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(ctx, args); }, delay); }; } function delay(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } // ============================================================ // v1.3.1 R2: 网络与缓存增强工具集(参考 sglv-core.lib.js 最佳实践) // ============================================================ // --- gmFetchText: 带重试的 GM_xmlhttpRequest 文本包装 --- // 参考: sglv-core.lib.js#gmFetchText function gmFetchText(url, options) { options = options || {}; var timeout = options.timeout || CONFIG.GM_FETCH_DEFAULT_TIMEOUT; var retries = options.retries != null ? options.retries : CONFIG.GM_FETCH_DEFAULT_RETRIES; var method = options.method || 'GET'; var headers = options.headers || { 'Accept': 'application/json,text/html,text/plain,*/*' }; var data = options.data || null; return new Promise(function(resolve, reject) { var attempt = 0; function doRequest() { GM_xmlhttpRequest({ method: method, url: url, headers: headers, data: data, timeout: timeout, responseType: 'text', onload: function(r) { if (r.status >= 200 && r.status < 300) { resolve(r.responseText); } else if (r.status === 429 || r.status >= 500) { // 可重试的错误 if (attempt < retries) { attempt++; var backoff = attempt === 1 ? CONFIG.CONCURRENT_POOL_BACKOFF_1 : CONFIG.CONCURRENT_POOL_BACKOFF_2; setTimeout(doRequest, backoff); } else { reject(new Error('HTTP ' + r.status)); } } else { reject(new Error('HTTP ' + r.status)); } }, onerror: function() { if (attempt < retries) { attempt++; var backoff = attempt === 1 ? CONFIG.CONCURRENT_POOL_BACKOFF_1 : CONFIG.CONCURRENT_POOL_BACKOFF_2; setTimeout(doRequest, backoff); } else { reject(new Error('网络请求失败')); } }, ontimeout: function() { if (attempt < retries) { attempt++; var backoff = attempt === 1 ? CONFIG.CONCURRENT_POOL_BACKOFF_1 : CONFIG.CONCURRENT_POOL_BACKOFF_2; setTimeout(doRequest, backoff); } else { reject(new Error('请求超时')); } } }); } doRequest(); }); } // --- gmFetchJson: 带重试的 GM_xmlhttpRequest JSON 包装 --- function gmFetchJson(url, options) { return gmFetchText(url, options).then(function(text) { try { return JSON.parse(text); } catch (e) { throw new Error('JSON 解析失败: ' + e.message); } }); } // v1.5.5: gmFetchBlob — 用 GM_xmlhttpRequest 获取二进制(图片等) // 关键优势: 运行在油猴扩展环境,不受页面 CORS 策略限制 // (community.fastly.steamstatic.com 无 Access-Control-Allow-Origin,页面 fetch 必失败) function gmFetchBlob(url, timeoutMs) { return new Promise(function(resolve, reject) { try { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: timeoutMs || 15000, responseType: 'blob', onload: function(r) { if (r.status >= 200 && r.status < 300 && r.response) { resolve(r.response); } else { reject(new Error('HTTP ' + r.status)); } }, onerror: function() { reject(new Error('网络请求失败')); }, ontimeout: function() { reject(new Error('请求超时')); } }); } catch (e) { reject(e); } }); } // --- concurrentPool: 智能并发池(滑动窗口 + 429 降并发 + 指数退避) --- // 参考: sglv-core.lib.js#concurrentPool async function concurrentPool(tasks, concurrency, onProgress) { var results = new Array(tasks.length); var nextIndex = 0; var completed = 0; var activeConcurrency = concurrency; var MAX_RETRY = CONFIG.CONCURRENT_POOL_MAX_RETRY; async function runTask(index) { var retryCount = 0; while (retryCount <= MAX_RETRY) { try { var result = await tasks[index](); results[index] = { index: index, status: 'fulfilled', value: result }; return; } catch (e) { retryCount++; if (retryCount <= MAX_RETRY) { var delayMs = retryCount === 1 ? CONFIG.CONCURRENT_POOL_BACKOFF_1 : CONFIG.CONCURRENT_POOL_BACKOFF_2; await delay(delayMs); // 429 自动降并发 var msg = String(e && e.message || ''); if (msg.indexOf('429') >= 0 || msg.indexOf('rate') >= 0) { activeConcurrency = Math.max( CONFIG.CONCURRENT_POOL_MIN_CONCURRENCY, Math.floor(activeConcurrency * CONFIG.CONCURRENT_POOL_DECREASE_RATIO) ); } } else { results[index] = { index: index, status: 'rejected', reason: e }; } } } } async function worker() { while (nextIndex < tasks.length) { var idx = nextIndex++; await runTask(idx); completed++; if (onProgress) { try { onProgress(completed, tasks.length); } catch (e) {} } } } var workerCount = Math.min(activeConcurrency, tasks.length); var workers = []; for (var w = 0; w < workerCount; w++) workers.push(worker()); await Promise.all(workers); return results; } // --- negCache: 负缓存(避免重复请求失败项) --- // 参考: sglv-core.lib.js#negCache var negCache = (function() { var _cache = {}; return { get: function(key) { var entry = _cache[key]; if (entry && Date.now() - entry.time < CONFIG.NEG_CACHE_TTL) { return entry.value; } if (entry) delete _cache[key]; return undefined; }, set: function(key, value) { _cache[key] = { value: value, time: Date.now() }; }, has: function(key) { return this.get(key) !== undefined; }, clear: function() { _cache = {}; }, size: function() { return Object.keys(_cache).length; } }; })(); // --- ensureCacheVersion: 缓存版本控制 --- // 参考: sglv-core.lib.js#ensureCacheVersions function ensureCacheVersion(versionKey, currentVersion) { try { var cached = GM_getValue(versionKey, 0); if (cached < currentVersion) { return true; // 需要清除旧缓存 } } catch (e) {} return false; } function setCacheVersion(versionKey, currentVersion) { try { GM_setValue(versionKey, currentVersion); } catch (e) {} } // ============================================================ // v1.5.0: IndexedDB 缓存层 // ------------------------------------------------------------ // 替代 GM_setValue/GM_getValue 的大容量缓存,避免存储限制 // 对象仓库: // gameData - 游戏/勋章/卡牌数据库数据 (永不失效,只增不减) // marketPrices - 市场价格缓存 (TTL: 10分钟) // cardImages - 卡牌图片URL/数据 (永不失效) // popularMarket- 热门市场缓存 (TTL: 10分钟) // aiInsight - AI洞察结果缓存 (TTL: 30分钟) // marketResume - 断点续传状态 (TTL: 24小时) // meta - 元数据/版本控制 (永不过期) // ============================================================ var IDB = (function() { var DB_NAME = 'sbc_pro_cache'; var DB_VERSION = 1; var STORES = ['gameData', 'marketPrices', 'cardImages', 'popularMarket', 'aiInsight', 'marketResume', 'meta']; var _db = null; var _initPromise = null; function initDB() { if (_initPromise) return _initPromise; _initPromise = new Promise(function(resolve, reject) { if (typeof indexedDB === 'undefined') { console.warn('[SBC-Pro] IndexedDB 不可用,将回退到 GM 存储'); resolve(null); return; } try { var req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = function(e) { var db = e.target.result; STORES.forEach(function(storeName) { if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'key' }); } }); }; req.onsuccess = function(e) { _db = e.target.result; console.log('[SBC-Pro] IndexedDB 初始化成功'); resolve(_db); }; req.onerror = function(e) { console.warn('[SBC-Pro] IndexedDB 初始化失败,回退到 GM 存储:', e.target.error); resolve(null); }; } catch (e) { console.warn('[SBC-Pro] IndexedDB 异常,回退到 GM 存储:', e); resolve(null); } }); return _initPromise; } function tx(storeName, mode) { return _db.transaction(storeName, mode).objectStore(storeName); } function reqToPromise(request) { return new Promise(function(resolve, reject) { request.onsuccess = function() { resolve(request.result); }; request.onerror = function() { reject(request.error); }; }); } // 读取单条记录 (带 TTL 检查) async function get(storeName, key) { if (!_db) return null; try { var record = await reqToPromise(tx(storeName, 'readonly').get(key)); if (!record) return null; // TTL 过期检查 if (record.expiresAt && Date.now() > record.expiresAt) return null; return record.data; } catch (e) { return null; } } // 写入单条记录 (可选 TTL, 单位毫秒) async function set(storeName, key, data, ttl) { if (!_db) return; try { var record = { key: key, data: data, updatedAt: Date.now(), }; if (ttl && ttl > 0) record.expiresAt = Date.now() + ttl; await reqToPromise(tx(storeName, 'readwrite').put(record)); } catch (e) { /* 静默 */ } } // 删除单条记录 async function del(storeName, key) { if (!_db) return; try { await reqToPromise(tx(storeName, 'readwrite').delete(key)); } catch (e) {} } // 清空整个仓库 async function clearStore(storeName) { if (!_db) return; try { await reqToPromise(tx(storeName, 'readwrite').clear()); } catch (e) {} } // 获取所有 key (用于批量操作) async function getAllKeys(storeName) { if (!_db) return []; try { return await reqToPromise(tx(storeName, 'readonly').getAllKeys()); } catch (e) { return []; } } // 批量读取 (用于市场缓存等大量 key 场景) async function bulkGet(storeName, keys) { if (!_db) return {}; var results = {}; try { var store = tx(storeName, 'readonly'); await Promise.all(keys.map(function(key) { return reqToPromise(store.get(key)).then(function(record) { if (record) { if (record.expiresAt && Date.now() > record.expiresAt) return; results[key] = record.data; } }); })); } catch (e) {} return results; } // 合并写入 (只增不减): 将 newData 合并到已有数据 // 适用于 gameData 等永不失效的缓存 async function mergeSet(storeName, key, newData) { if (!_db) return; try { var existing = await get(storeName, key); var merged; if (existing && typeof existing === 'object' && typeof newData === 'object') { // 对象合并: 只增加新 key,不删除已有 key merged = Object.assign({}, existing, newData); } else { merged = newData; } await set(storeName, key, merged); } catch (e) { /* 静默 */ } } // 批量合并写入 (用于市场价格等场景) async function bulkMergeSet(storeName, entries) { if (!_db) return; try { var store = tx(storeName, 'readwrite'); await Promise.all(entries.map(function(entry) { return reqToPromise(store.get(entry.key)).then(function(record) { var existing = record ? record.data : null; var merged; // v1.5.5: 数组走并集合并(只增不减),对象走浅合并 if (Array.isArray(existing) && Array.isArray(entry.data)) { merged = Array.from(new Set(existing.concat(entry.data))); } else if (existing && typeof existing === 'object' && entry.data && typeof entry.data === 'object' && !Array.isArray(existing) && !Array.isArray(entry.data)) { merged = Object.assign({}, existing, entry.data); } else { merged = entry.data; } var newRecord = { key: entry.key, data: merged, updatedAt: Date.now(), }; if (entry.ttl && entry.ttl > 0) newRecord.expiresAt = Date.now() + entry.ttl; return reqToPromise(store.put(newRecord)); }); })); } catch (e) { /* 静默 */ } } // 获取所有记录 (用于遍历缓存) async function getAll(storeName) { if (!_db) return []; try { return await reqToPromise(tx(storeName, 'readonly').getAll()); } catch (e) { return []; } } // 元数据读写 (用于版本控制等) async function getMeta(key) { return get('meta', key); } async function setMeta(key, value) { return set('meta', key, value); } return { init: initDB, get: get, set: set, del: del, clearStore: clearStore, getAllKeys: getAllKeys, bulkGet: bulkGet, mergeSet: mergeSet, bulkMergeSet: bulkMergeSet, getAll: getAll, getMeta: getMeta, setMeta: setMeta, isReady: function() { return _db !== null; }, STORES: STORES, }; })(); // ============================================================ // v1.5.0: 统一缓存适配层 // ------------------------------------------------------------ // 优先使用 IndexedDB,回退到 GM_setValue/GM_getValue // 提供同步/异步双模式:同步模式用 GM (小数据),异步模式用 IDB (大数据) // ============================================================ // 异步缓存读取 (IndexedDB 优先, GM 回退) async function cacheGet(storeName, key) { if (IDB.isReady()) { var data = await IDB.get(storeName, key); if (data !== null) return data; } // GM 回退 try { var raw = GM_getValue(key, null); if (!raw) return null; var obj = typeof raw === 'string' ? JSON.parse(raw) : raw; if (obj && obj.expiresAt && Date.now() > obj.expiresAt) return null; return obj && obj.data !== undefined ? obj.data : obj; } catch (e) { return null; } } // 异步缓存写入 (IndexedDB 优先, GM 回退) async function cacheSet(storeName, key, data, ttl) { if (IDB.isReady()) { await IDB.set(storeName, key, data, ttl); } // 同时写 GM 作为回退 (小数据才写,避免 GM 溢出) try { if (ttl && ttl > 0) { GM_setValue(key, JSON.stringify({ data: data, expiresAt: Date.now() + ttl })); } else if (typeof data === 'string' && data.length < 5000000) { GM_setValue(key, data); } } catch (e) { /* 静默 */ } } // ============================================================ // v1.5.0: 卡牌图片缓存 (IndexedDB cardImages 仓库) // ------------------------------------------------------------ // 缓存卡牌图片 URL 映射和图片 Blob 数据,永不失效,只增不减 // ============================================================ // v1.5.5: 会话内已缓存的卡片URL集合(按 appId 去重,避免重复 IDB 读写) var _cardImgUrlsSession = new Set(); // 缓存卡牌图片 URL 列表 (按 appId) — v1.5.5: 会话内去重 + 后台批量合并 var _pendingCardImgUrls = new Map(); // appId -> Set(url) var _cardImgFlushScheduled = false; async function cacheCardImageUrls(appId, imgUrls) { if (!appId || !imgUrls || imgUrls.length === 0) return; // 会话内去重:完全相同的 (appId+url) 组合只处理一次 var fresh = []; for (var i = 0; i < imgUrls.length; i++) { var k = appId + '|' + imgUrls[i]; if (!_cardImgUrlsSession.has(k)) { _cardImgUrlsSession.add(k); fresh.push(imgUrls[i]); } } if (fresh.length === 0) return; if (!_pendingCardImgUrls.has(appId)) _pendingCardImgUrls.set(appId, new Set()); var bucket = _pendingCardImgUrls.get(appId); fresh.forEach(function(u) { bucket.add(u); }); // 延迟 2s 批量落盘,把 extractBadgesFromDOM 期间的数百次调用合并为少量事务 if (!_cardImgFlushScheduled) { _cardImgFlushScheduled = true; setTimeout(_flushCardImgUrls, 2000); } } async function _flushCardImgUrls() { _cardImgFlushScheduled = false; if (!IDB.isReady() || _pendingCardImgUrls.size === 0) { _pendingCardImgUrls.clear(); return; } var entries = []; _pendingCardImgUrls.forEach(function(urls, appId) { entries.push({ key: 'urls_' + appId, data: Array.from(urls) }); }); _pendingCardImgUrls.clear(); try { await IDB.bulkMergeSet('cardImages', entries); } catch (e) { /* 静默 */ } } // 读取缓存的卡牌图片 URL 列表 async function getCachedCardImageUrls(appId) { if (!IDB.isReady() || !appId) return null; try { return await IDB.get('cardImages', 'urls_' + appId); } catch (e) { return null; } } // v1.5.0: 缓存卡牌图片 Blob 数据 (用于离线/快速加载) var _imgBlobCache = new Map(); // 内存缓存: url -> objectURL (LRU) var _imgBlobFetching = new Set(); // 正在获取的 url 集合 var _imgBlobMisses = new Set(); // v1.5.5: IDB 未命中的 url,跳过重复 IDB 查询 var _imgBlobWarmed = false; // v1.5.5: 是否已完成 IDB 索引预热 var _imgBlobWarming = null; // v1.5.5: 预热中的 promise(防重复) // v1.5.5: 写入内存缓存(LRU: 超出上限时淘汰最久未用并回收 objectURL) function _imgBlobCachePut(url, objUrl) { if (_imgBlobCache.has(url)) _imgBlobCache.delete(url); // 重新插入到末尾(最近使用) _imgBlobCache.set(url, objUrl); while (_imgBlobCache.size > CONFIG.IMG_BLOB_CACHE_MAX) { var oldestKey = _imgBlobCache.keys().next().value; var oldestUrl = _imgBlobCache.get(oldestKey); _imgBlobCache.delete(oldestKey); try { URL.revokeObjectURL(oldestUrl); } catch (e) {} } } // v1.5.5: 预热图片缓存索引 — 一次性 getAllKeys 避免每张图片一次 IDB get 往返 // 只预热索引(key 存在与否),Blob 本身仍在首次使用时才读取,内存友好 function warmImageBlobIndex() { if (_imgBlobWarmed) return Promise.resolve(); if (_imgBlobWarming) return _imgBlobWarming; _imgBlobWarming = (async function() { if (!IDB.isReady()) { _imgBlobWarmed = true; return; } try { var keys = await IDB.getAllKeys('cardImages'); keys.forEach(function(k) { if (typeof k === 'string' && k.indexOf('blob_') === 0) _imgBlobMisses.add('__has__'); // 占位保持 Set 语义 }); // 用独立集合记录 IDB 中已存在的 blob key _imgBlobHasIndex = new Set(); keys.forEach(function(k) { if (typeof k === 'string' && k.indexOf('blob_') === 0) _imgBlobHasIndex.add(k.substring(5)); }); } catch (e) { /* 静默 */ } finally { _imgBlobWarmed = true; } })(); return _imgBlobWarming; } var _imgBlobHasIndex = null; // v1.5.5: IDB 中已缓存 blob 的 url 索引(由 warmImageBlobIndex 填充) async function getCachedImageBlob(url) { if (!url) return null; // 内存缓存优先 if (_imgBlobCache.has(url)) return _imgBlobCache.get(url); if (!IDB.isReady()) return null; // v1.5.5: 预热后命中索引快速判定,已知未缓存的 url 跳过 IDB 查询 if (_imgBlobWarmed) { if (_imgBlobHasIndex && !_imgBlobHasIndex.has(url)) return null; } else if (_imgBlobMisses.has(url)) { return null; // v1.5.5: 之前已确认未缓存,避免重复 IDB 往返 } try { var blob = await IDB.get('cardImages', 'blob_' + url); if (blob && blob instanceof Blob) { var objUrl = URL.createObjectURL(blob); _imgBlobCachePut(url, objUrl); return objUrl; } // v1.5.5: 未命中记录到 miss 集合 if (!_imgBlobWarmed) _imgBlobMisses.add(url); } catch (e) {} return null; } // v1.5.5: 图片加载失败处理 — onerror 时标记占位,避免破图图标影响观感 var _imgErrHandled = new WeakSet(); function handleImageError(img) { if (!img || _imgErrHandled.has(img)) return; _imgErrHandled.add(img); img.classList.add('sbc-img-fallback'); img.removeAttribute('src'); // 移除失败 src,触发 CSS 占位背景 } // 全局捕获 错误(capture 阶段,冒泡不到 window) if (typeof document !== 'undefined') { document.addEventListener('error', function(e) { var t = e.target; if (t && t.tagName === 'IMG' && t.classList && t.classList.contains('sbc-lazy-img')) { handleImageError(t); } }, true); } // v1.5.5: CORS 失败域名黑名单 — 页面 fetch 被 CORS 拦截过的域名不再重复尝试 // (控制台 ERR_FAILED 刷屏的根因: 每张图都徒劳地 fetch 一次) var _imgCorsBlockedHosts = new Set(); function _imgHostOf(url) { try { return new URL(url).hostname; } catch (e) { return ''; } } // 异步获取并缓存图片 Blob (fire-and-forget) // v1.5.5: 优先 GM_xmlhttpRequest(扩展环境不受页面 CORS 限制),失败降级页面 fetch; // 两者都被 CORS 拦截的域名加入黑名单,之后直接用网络 src 显示,不再尝试缓存 async function fetchAndCacheImageBlob(url) { if (!url || !IDB.isReady() || _imgBlobFetching.has(url)) return; if (_imgBlobCache.has(url)) return; // 内存已有,无需重抓 var host = _imgHostOf(url); if (host && _imgCorsBlockedHosts.has(host)) return; // 该域名已知 CORS 失败,跳过 _imgBlobFetching.add(url); try { var blob = null; try { blob = await gmFetchBlob(url, 15000); } catch (gmErr) { // GM 失败(域名未在 @connect / 网络问题),尝试页面 fetch 兜底 try { var resp = await fetch(url, { mode: 'cors', credentials: 'omit' }); if (resp.ok) blob = await resp.blob(); } catch (fetchErr) { // 页面 fetch 被 CORS 拦截 → 记录域名,后续该域名的图直接跳过缓存抓取 if (host) { _imgCorsBlockedHosts.add(host); console.log('[SBC-Pro] 图片缓存: ' + host + ' 被 CORS 拦截,后续该域名图片直接使用网络加载(显示不受影响)'); } return; } } if (blob && blob.size > 0 && blob.size < 512 * 1024) { // 仅缓存 < 512KB 的图片 await IDB.set('cardImages', 'blob_' + url, blob); var objUrl = URL.createObjectURL(blob); _imgBlobCachePut(url, objUrl); if (_imgBlobHasIndex) _imgBlobHasIndex.add(url); // 更新索引 _imgBlobMisses.delete(url); } } catch (e) { /* 静默 */ } finally { _imgBlobFetching.delete(url); } } // v1.5.0: 智能图片加载 — 优先从 IDB Blob 缓存加载,回退到网络 // v1.5.5: 先设置网络 src 保证立即可见,IDB 命中后再无缝替换为本地 blob(更快且防内存大图) async function smartLoadImage(imgEl, url) { if (!url) return; // 内存命中:直接用本地 blob(零网络) if (_imgBlobCache.has(url)) { imgEl.src = _imgBlobCache.get(url); return; } // 立即用网络 URL 渲染,避免等待 IDB 查询的白屏延迟 imgEl.src = url; if (!IDB.isReady()) return; try { var cachedUrl = await getCachedImageBlob(url); // 若期间用户已滚动/元素被复用(src 改变)则不覆盖 if (cachedUrl && imgEl.getAttribute('src') === url) { imgEl.src = cachedUrl; return; // 已有本地缓存,无需后台再抓 } } catch (e) {} // 无本地缓存:后台抓取并缓存(下次访问秒开) fetchAndCacheImageBlob(url); } // ============================================================ // v1.3.1: 性能增强工具集 // ============================================================ // --- LazyImageLoader: 基于 IntersectionObserver 的图片懒加载 --- // v1.5.0: 集成 IDB Blob 缓存,优先从缓存加载图片 // 用法: const img = LazyImageLoader.create(src, alt, className); // 返回一个 元素,进入视口后才加载 src var LazyImageLoader = (function() { var observer = null; var pending = new Set(); function getObserver() { if (observer) return observer; if (typeof IntersectionObserver === 'undefined') return null; observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { var img = entry.target; var src = img.getAttribute('data-src'); if (src) { // v1.5.5: 页面隐藏时不做后台抓取缓存,减少不可见时的网络开销 if (document.visibilityState === 'hidden') { img.src = src; } else { // v1.5.0: 优先从 IDB Blob 缓存加载,回退到网络 smartLoadImage(img, src); } img.removeAttribute('data-src'); img.classList.add('sbc-lazy-loaded'); } observer.unobserve(img); pending.delete(img); } }); }, { rootMargin: CONFIG.LAZY_IMG_ROOT_MARGIN, threshold: CONFIG.LAZY_IMG_THRESHOLD }); return observer; } return { create: function(src, alt, className) { var img = document.createElement('img'); if (alt) img.alt = alt; if (className) img.className = className + ' sbc-lazy-img'; else img.className = 'sbc-lazy-img'; img.setAttribute('data-src', src); var obs = getObserver(); if (obs) { obs.observe(img); pending.add(img); } else { // v1.5.0: Fallback — 不支持 IntersectionObserver 时用 smartLoadImage smartLoadImage(img, src); img.classList.add('sbc-lazy-loaded'); } return img; }, // 批量创建图片 HTML 字符串(用于 innerHTML 场景) createLazyHtml: function(src, alt, extraClass) { var cls = 'sbc-lazy-img' + (extraClass ? ' ' + extraClass : ''); return ''; }, // 初始化: 扫描容器内所有 data-src 图片并注册观察 scan: function(container) { if (!container) return; var obs = getObserver(); if (!obs) return; container.querySelectorAll('img[data-src]').forEach(function(img) { obs.observe(img); pending.add(img); }); }, // 清理: 断开所有观察(视图切换时调用) clear: function() { if (observer) { pending.forEach(function(img) { observer.unobserve(img); }); pending.clear(); } } }; })(); // --- DOMRefCache: 缓存频繁访问的 DOM 元素引用 --- var DOMRefCache = (function() { var cache = {}; return { get: function(id) { if (!cache[id]) cache[id] = document.getElementById(id); return cache[id]; }, invalidate: function(id) { if (id) delete cache[id]; else cache = {}; } }; })(); // --- scrollToTop: 平滑滚动到指定位置 --- function smoothScrollToTop(offset) { offset = offset || 0; var target = document.getElementById('sbc-root'); if (!target) { window.scrollTo({ top: offset, behavior: 'smooth' }); return; } var rect = target.getBoundingClientRect(); var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var targetY = rect.top + scrollTop - offset; if (targetY < 0) targetY = 0; window.scrollTo({ top: targetY, behavior: 'smooth' }); } // --- showSkeleton: 显示骨架屏占位 --- function showSkeleton(container, count) { if (!container) return; count = count || 12; var html = ''; for (var i = 0; i < count; i++) { html += '' + '' + '' + '' + '' + ''; } html += ''; container.innerHTML = html; } // --- applyViewTransition: 视图切换过渡动画 --- function applyViewTransition(container, renderFn) { if (!container) { renderFn(); return; } // 退出动画 container.classList.add('sbc-view-exit'); setTimeout(function() { container.classList.remove('sbc-view-exit'); renderFn(); // 进入动画 container.classList.add('sbc-view-enter'); setTimeout(function() { container.classList.remove('sbc-view-enter'); }, CONFIG.VIEW_TRANSITION_MS); }, CONFIG.VIEW_TRANSITION_MS); } // --- KbdShortcuts: 键盘快捷键管理 --- var KbdShortcuts = (function() { var hintEl = null; var hintTimer = null; function showHint(text) { if (!hintEl) { hintEl = document.createElement('div'); hintEl.className = 'sbc-kbd-hint'; document.body.appendChild(hintEl); } hintEl.innerHTML = text; hintEl.classList.add('sbc-kbd-hint--visible'); clearTimeout(hintTimer); hintTimer = setTimeout(function() { hintEl.classList.remove('sbc-kbd-hint--visible'); }, 2000); } function handleKey(e) { // 在输入框中不触发快捷键(除了 Escape) var tag = (e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea' || tag === 'select') { if (e.key === 'Escape') e.target.blur(); return; } // / 聚焦搜索 if (e.key === '/') { e.preventDefault(); var search = DOMRefCache.get('sbc-search-input'); if (search) { search.focus(); search.select(); } showHint('/ 搜索'); return; } // Escape 关闭浮窗 if (e.key === 'Escape') { var overlay = document.querySelector('.sbc-detail-overlay'); if (overlay) { overlay.remove(); return; } var analysis = document.getElementById('sbc-analysis-panel'); if (analysis && analysis.style.display !== 'none') { analysis.style.display = 'none'; analysisVisible = false; var btn = DOMRefCache.get('sbc-analysis-btn'); if (btn) btn.classList.remove('active'); return; } return; } // 方向键翻页(仅 grid 视图) if (viewMode === 'grid') { if (e.key === 'ArrowLeft' && currentPage > 1) { e.preventDefault(); currentPage--; renderGrid(); smoothScrollToTop(CONFIG.SCROLL_TO_TOP_OFFSET); showHint('← 第 ' + currentPage + ' 页'); } else if (e.key === 'ArrowRight') { var totalPages = Math.ceil(filteredBadges.length / CONFIG.CARDS_PER_PAGE); if (currentPage < totalPages) { e.preventDefault(); currentPage++; renderGrid(); smoothScrollToTop(CONFIG.SCROLL_TO_TOP_OFFSET); showHint('→ 第 ' + currentPage + ' 页'); } } } // 数字键 1-5 切换视图 var viewMap = { '1': 'sbc-mode-grid', '2': 'sbc-mode-unified', '3': 'sbc-mode-grouped', '4': 'sbc-mode-activity', '5': 'sbc-mode-alldb' }; if (viewMap[e.key]) { e.preventDefault(); var btn = DOMRefCache.get(viewMap[e.key]); if (btn) { btn.click(); showHint('' + e.key + ' ' + btn.title); } } } return { init: function() { document.addEventListener('keydown', handleKey); }, showHint: showHint }; })(); // ============================================================ // 数据提取(增强版:含appId、锁定徽章图标) // ============================================================ // ============================================================ // v1.2.9: 提取 Steam 页面原版用户信息 // 数据源: .profile_small_header_texture, .profile_xp_block // + g_steamID 全局变量 (权威 Steam ID 来源) // + userHint[0] (来自 MiniProfileHover 弹层) // + XML profile API (异步获取总游玩时长) // ============================================================ function detectCurrentSteamId() { // 参考 steam-friend-manager 1.0.16 的实现 try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow.g_steamID) return unsafeWindow.g_steamID; } catch(e) {} try { if (typeof window.g_steamID !== 'undefined' && window.g_steamID) return window.g_steamID; } catch(e) {} // 兜底:从 HTML 中正则提取 var match = document.documentElement.innerHTML.match(/g_steamID\s*=\s*"(\d{17})"/); return match ? match[1] : ''; } function extractUserInfo() { var info = { personaName: '', profileUrl: '', steamId: '', avatarUrl: '', avatarFrameUrl: '', onlineState: 'offline', playerLevel: 0, currentXp: 0, xpForNextLevel: 0, xpRemainingToNext: 0, xpProgressPct: 0, totalGames: 0, totalBadges: 0, totalFriends: 0, totalPlaytimeMinutes: 0 // v1.2.9: 异步从 XML 拉取 }; // 1. Steam ID (v1.2.9 优先使用全局变量 g_steamID) info.steamId = detectCurrentSteamId(); // 2. 用户名 (多种兜底) // 2a. .profile_small_header_name a var personaEl = document.querySelector('.profile_small_header_name a, .persona_name_text_content'); if (personaEl) info.personaName = personaEl.textContent.trim(); // 2b. .actual_persona_name (某些页面使用) if (!info.personaName) { var actualPersona = document.querySelector('.actual_persona_name'); if (actualPersona) info.personaName = actualPersona.textContent.trim(); } // 2c. g_steamProfilePersonaName 全局变量 if (!info.personaName && typeof window.g_steamProfilePersonaName !== 'undefined') { info.personaName = window.g_steamProfilePersonaName; } // 2d. userHint 数组 (来自 MiniProfileHover) if (!info.personaName && typeof window.userHint !== 'undefined' && Array.isArray(window.userHint) && window.userHint.length > 0) { info.personaName = window.userHint[0] || ''; } // 3. 主页 URL var profileLink = document.querySelector('.profile_small_header_text a[href*="/id/"], .profile_small_header_text a[href*="/profiles/"]'); if (profileLink) info.profileUrl = profileLink.href; if (!info.profileUrl && typeof window.g_strProfileURL !== 'undefined') info.profileUrl = window.g_strProfileURL; if (!info.profileUrl && info.steamId) { info.profileUrl = 'https://steamcommunity.com/profiles/' + info.steamId; } if (!info.profileUrl && info.personaName) { info.profileUrl = 'https://steamcommunity.com/id/' + info.personaName; } // 4. 头像图片 // v1.2.14-fix2: 增强头像选择器,适配 Steam 多种 DOM 结构 var avatarImg = document.querySelector( '.profile_small_header_avatar img, ' + '.profile_small_header_avatar picture img, ' + '.profile_avatar_frame + picture img, ' + '.playerAvatarAutoSizeInner img, ' + '.playerAvatar img, ' + '.profile_header .playerAvatar img, ' + '.profile_summary .playerAvatar img' ); if (avatarImg) { info.avatarUrl = avatarImg.getAttribute('src') || avatarImg.getAttribute('data-delayed-image') || avatarImg.getAttribute('data-src') || ''; // v1.2.14-fix2: Steam 可能用空 src + data-delayed-image,过滤掉占位 if (info.avatarUrl && !/^https?:\/\//.test(info.avatarUrl) && !/^\/\//.test(info.avatarUrl)) { info.avatarUrl = ''; } } // v1.2.14-fix2: 兜底——从页面 CSS background-image 提取头像 if (!info.avatarUrl) { var avatarWrap = document.querySelector('.profile_small_header_avatar, .playerAvatar'); if (avatarWrap) { var bg = window.getComputedStyle(avatarWrap).backgroundImage; var urlMatch = bg && bg.match(/url\(["']?(https?:\/\/[^"')]+)["']?\)/); if (urlMatch) info.avatarUrl = urlMatch[1]; } } // 5. 头像框(头像周围的装饰图) var avatarFrame = document.querySelector('.profile_avatar_frame img, .profile_avatar_frame source, .profile_small_header_avatar .profile_avatar_frame img'); if (avatarFrame) info.avatarFrameUrl = avatarFrame.getAttribute('src') || avatarFrame.getAttribute('srcset') || ''; // 6. 在线状态 var onlineStateEl = document.querySelector('.profile_small_header_texture .persona'); if (onlineStateEl) { var classes = onlineStateEl.className; if (classes.indexOf('in-game') >= 0) info.onlineState = 'in-game'; else if (classes.indexOf('online') >= 0) info.onlineState = 'online'; else info.onlineState = 'offline'; } // 7. 等级 var levelEl = document.querySelector('.profile_xp_block_level .friendPlayerLevelNum, .friendPlayerLevelNum, .profile_xp_block .friendPlayerLevelNum'); if (levelEl) info.playerLevel = parseInt(levelEl.textContent.trim(), 10) || 0; // 8. 经验值 var xpEl = document.querySelector('.profile_xp_block_xp'); if (xpEl) { var xpMatch = xpEl.textContent.replace(/,/g, '').match(/(\d+)/); if (xpMatch) info.currentXp = parseInt(xpMatch[1], 10) || 0; } // 9. 升到下一级还差经验值 var remainingEl = document.querySelector('.profile_xp_block_remaining'); if (remainingEl) { var remMatch = remainingEl.textContent.replace(/,/g, '').match(/(\d+)/); if (remMatch) info.xpRemainingToNext = parseInt(remMatch[1], 10) || 0; } // 10. 进度条宽度 var progressBar = document.querySelector('.profile_xp_block_remaining_bar_progress'); if (progressBar) { var w = progressBar.style.width || ''; var pctMatch = w.match(/(\d+(?:\.\d+)?)/); if (pctMatch) info.xpProgressPct = parseFloat(pctMatch[1]) || 0; } // 11. 计算: 下一级所需总经验值 if (info.xpRemainingToNext > 0) info.xpForNextLevel = info.currentXp + info.xpRemainingToNext; // 12. v1.2.9: 总游戏数(从 .profile_count_link[href*="/games/"] 优先,API 兜底) var gameCountEl = document.querySelector('.profile_count_link[href*="/games/"] .profile_count_link_total, .profile_summary .game_count .count'); if (gameCountEl) { info.totalGames = parseInt(gameCountEl.textContent.replace(/,/g, '').trim(), 10) || 0; } // 兜底:从页面文本中提取 "X 款产品" / "X games" if (info.totalGames === 0) { var gamesLabel = document.querySelector('.profile_count_link[href*="/games/"]'); if (gamesLabel) { var gamesText = gamesLabel.textContent.replace(/,/g, ''); var gMatch = gamesText.match(/(\d+)/); if (gMatch) info.totalGames = parseInt(gMatch[1], 10) || 0; } } // 13. 总徽章数 var badgeCountEl = document.querySelector('.profile_count_link[href*="/badges/"] .profile_count_link_total, .profile_summary .badge_count .count'); if (badgeCountEl) info.totalBadges = parseInt(badgeCountEl.textContent.replace(/,/g, '').trim(), 10) || 0; if (info.totalBadges === 0) { var badgesLabel = document.querySelector('.profile_count_link[href*="/badges/"]'); if (badgesLabel) { var bText = badgesLabel.textContent.replace(/,/g, ''); var bMatch = bText.match(/(\d+)/); if (bMatch) info.totalBadges = parseInt(bMatch[1], 10) || 0; } } // 14. 好友数 // v1.2.14-fix5: 增强选择器,徽章页可能无 .profile_count_link,从多个位置提取 var friendsCountEl = document.querySelector( '.profile_count_link[href*="/friends/"] .profile_count_link_total, ' + '.profile_summary .friend_count .count, ' + '.profile_in_game_header .friend_count, ' + '.profile_friends .profile_count_link_total' ); if (friendsCountEl) info.totalFriends = parseInt(friendsCountEl.textContent.replace(/,/g, '').trim(), 10) || 0; if (info.totalFriends === 0) { var friendsLabel = document.querySelector('.profile_count_link[href*="/friends/"]'); if (friendsLabel) { var fText = friendsLabel.textContent.replace(/,/g, ''); var fMatch = fText.match(/(\d+)/); if (fMatch) info.totalFriends = parseInt(fMatch[1], 10) || 0; } } return info; } // v1.3.0: 通过 Steam Web API 获取好友数 (参考 steam-friend-manager GetFriendList 逻辑) // 优先使用 ISteamUser/GetFriendList/v1 API,无 API Key 时降级到 HTML 抓取 function fetchFriendsCount(steamId, profileUrl) { var apiKey = getStoredApiKey(); var useSteamId = steamId || getStoredFallbackSteamId(); // 方案1: Steam Web API (精确可靠) if (apiKey && useSteamId) { return new Promise(function(resolve) { GM_xmlhttpRequest({ method: 'GET', url: 'https://api.steampowered.com/ISteamUser/GetFriendList/v1/?key=' + encodeURIComponent(apiKey) + '&steamid=' + encodeURIComponent(useSteamId) + '&relationship=friend', timeout: 10000, onload: function(r) { try { var data = JSON.parse(r.responseText); if (data && data.friendslist && Array.isArray(data.friendslist.friends)) { resolve(data.friendslist.friends.length); } else { // API 返回但无 friendslist (私密资料) → 降级 HTML resolve(fetchFriendsCountFromHtml(profileUrl)); } } catch (e) { resolve(fetchFriendsCountFromHtml(profileUrl)); } }, onerror: function() { resolve(fetchFriendsCountFromHtml(profileUrl)); }, ontimeout: function() { resolve(fetchFriendsCountFromHtml(profileUrl)); } }); }); } // 方案2: 无 API Key → HTML 抓取降级 return Promise.resolve(fetchFriendsCountFromHtml(profileUrl)); } // v1.3.0: HTML 降级方案 — 从用户主页解析好友数 function fetchFriendsCountFromHtml(profileUrl) { if (!profileUrl) return Promise.resolve(0); return new Promise(function(resolve) { GM_xmlhttpRequest({ method: 'GET', url: profileUrl, timeout: 8000, onload: function(response) { try { var html = response.responseText || ''; var match = html.match(/href="[^"]*\/friends\/"[^>]*>[\s\S]*?(\d[\d,]*)\s*<\/span>/i); if (!match) match = html.match(/"friend_count"\s*:\s*(\d+)/i); if (!match) match = html.match(/profile_count_link_total[^>]*>(\d[\d,]*)<\/span>/i); if (match) { resolve(parseInt(match[1].replace(/,/g, ''), 10) || 0); } else { resolve(0); } } catch (e) { resolve(0); } }, onerror: function() { resolve(0); }, ontimeout: function() { resolve(0); } }); }); } /** * v1.2.9: 异步获取用户总游玩时长(从 XML profile API) * 拉取 IPlayerService 接口: OwnedGames 或 Badges? 实际用 stats API 或 games?xml=1 * 这里采用最轻量方案: 尝试 /profiles/{steamid}/games/?xml=1 */ function fetchUserPlaytime(steamId) { if (!steamId) return Promise.resolve(0); return new Promise(function(resolve) { GM_xmlhttpRequest({ method: 'GET', url: 'https://steamcommunity.com/profiles/' + steamId + '/games/?tab=all&xml=1', timeout: 8000, onload: function(response) { try { var xml = new DOMParser().parseFromString(response.responseText, 'text/xml'); // ... // 或 var games = xml.querySelectorAll('game'); var totalMinutes = 0; games.forEach(function(g) { var hrs = parseFloat(g.querySelector('hoursOnRecord')?.textContent || '0'); if (!isNaN(hrs)) totalMinutes += hrs * 60; }); resolve(totalMinutes); } catch (e) { resolve(0); } }, onerror: function() { resolve(0); }, ontimeout: function() { resolve(0); } }); }); } // v1.2.7: 用户信息缓存(模块级) var userInfo = null; /** * v1.2.7: 创建用户信息头部面板(参考 Steam 原版 + 第三方设计图) * 布局: [头像+等级徽章] [用户名/Steam ID/经验/进度] [4个KPI卡片(游戏/徽章/好友/时长)] */ function createUserHeader(info) { if (!info) return null; var header = document.createElement('div'); header.className = 'sbc-user-header'; // 头像区域 var onlineStateClass = 'sbc-user-online--' + info.onlineState; var onlineStateText = ({ 'online': '在线', 'in-game': '游戏中', 'offline': '离线' })[info.onlineState] || '离线'; var avatarHtml = '' + // v1.2.10 修复: 有 Steam 头像框就用,没有就给个默认 ring,避免头像"光秃秃"无装饰 (info.avatarFrameUrl ? '' : '') + '' + (info.avatarUrl ? '' : '' + (info.personaName ? info.personaName[0] : '?') + '') + // 占位符放在 img 之后,onerror 失败时显示 '' + (info.personaName ? info.personaName[0] : '?') + '' + // v1.2.14-fix5: 移除头像右下角的等级标记(等级已展示在昵称后) '' + '' + ''; // 用户名 + 等级 + ID // v1.2.14-fix3: 移除对号,等级信息展示在昵称后面 var nameHtml = '' + '' + '' + escapeHtml(info.personaName || 'Steam User') + '' + // v1.2.14-fix3: 等级展示在昵称后(inline) (info.playerLevel > 0 ? '' + '' + '' + '' + 'Lv.' + info.playerLevel + '' + '' : '') + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + (info.steamId ? 'STEAM ID: ' + escapeHtml(info.steamId) + '' : '') + ''; // 等级进度条(Lv.X → Lv.Y) // v1.2.14-fix3: 移除"经验值 · 等级进度"字样,直接展示数值 var progressHtml = ''; if (info.playerLevel > 0) { var currentLevelXp = info.currentXp; var earnedThisLevel = info.xpForNextLevel - info.xpRemainingToNext; // 当前等级已获得经验 var pct = info.xpProgressPct || 0; progressHtml = '' + '' + '' + '' + '' + '' + 'Lv.' + info.playerLevel + '' + '' + info.xpRemainingToNext + ' / ' + info.xpForNextLevel + ' XP' + 'Lv.' + (info.playerLevel + 1) + '' + '' + Math.round(pct) + '%' + '' + '' + ''; } // 左侧:头像 + 名称 + 进度 var leftHtml = '' + avatarHtml + '' + nameHtml + progressHtml + '' + ''; // 右侧:4 个 KPI 卡片 // v1.2.9: 第四个改为"总时长"(由 fetchUserPlaytime 异步填充) // v1.2.10 修复: 空值显示 "—" 而不是 0(避免"游戏: 0"看起来很尴尬) // v1.2.10 修复: 用 allBadges.length 兜底 totalBadges(用户徽章页一定有徽章数据) // v1.2.14-fix5: KPI 卡片支持点击跳转(好友→好友页,游戏→游戏页,徽章→徽章页) var fallbackBadges = (typeof allBadges !== 'undefined' && Array.isArray(allBadges)) ? allBadges.length : 0; var profileBaseUrl = info.profileUrl || (info.steamId ? ('https://steamcommunity.com/profiles/' + info.steamId) : ''); var kpis = [ { label: '游戏', value: info.totalGames, color: 'cyan', icon: '', href: profileBaseUrl ? (profileBaseUrl + '/games') : '' }, { label: '徽章', value: info.totalBadges || fallbackBadges, color: 'purple', icon: '', href: profileBaseUrl ? (profileBaseUrl + '/badges') : '' }, { label: '好友', value: info.totalFriends, color: 'teal', icon: '', href: profileBaseUrl ? (profileBaseUrl + '/friends') : '' }, { label: '总时长', value: formatPlaytime(info.totalPlaytimeMinutes), color: 'gold', icon: '', dataKey: 'playtime' } ]; var kpiHtml = '' + kpis.map(function(k) { // v1.2.10 修复: 数字 = 0 / 字符串 = "—" / null/undefined → 显示 "—" var isEmpty = (k.value === 0 || k.value === '—' || k.value === null || k.value === undefined); var displayValue; if (k.dataKey === 'playtime') { // 总时长: 总是包在 data-sbc-kpi 里方便后续更新,空值也保留 slot displayValue = '' + (isEmpty ? '—' : k.value) + ''; } else { displayValue = isEmpty ? '—' : k.value.toLocaleString(); } // v1.2.14-fix5: 支持点击跳转的卡片用 包裹 var tag = k.href ? 'a' : 'div'; var hrefAttr = k.href ? (' href="' + escapeHtml(k.href) + '" target="_blank" rel="noopener"') : ''; var clickClass = k.href ? ' sbc-user-kpi--clickable' : ''; return '<' + tag + ' class="sbc-user-kpi sbc-user-kpi--' + k.color + (isEmpty ? ' sbc-user-kpi--empty' : '') + clickClass + '"' + hrefAttr + (k.href ? ' title="点击在新标签页打开"' : '') + '>' + '' + k.icon + '' + '' + '' + displayValue + '' + '' + k.label + '' + '' + '' + tag + '>'; }).join('') + ''; header.innerHTML = leftHtml + kpiHtml; return header; } /** * v1.2.9: 格式化游玩时长(分钟 → "Xh Ym" / "X.Yk" / "Xh") */ function formatPlaytime(minutes) { if (!minutes || minutes <= 0) return '—'; var hours = minutes / 60; if (hours < 100) return Math.round(hours) + 'h'; if (hours < 10000) return Math.round(hours / 10) / 100 + 'k'; // 避免过长数字 return (Math.round(hours / 100) / 10) + 'kh'; } // ============================================================ // v1.2.10: Steam Web API 集成(用户配置 + 异步数据抓取) // 持久化键: sbc_steam_api_key, sbc_fallback_steam_id // ============================================================ var STORAGE_KEY_API = 'sbc_steam_api_key'; var STORAGE_KEY_FALLBACK_ID = 'sbc_fallback_steam_id'; function getStoredApiKey() { try { return GM_getValue(STORAGE_KEY_API, ''); } catch (e) { return ''; } } function setStoredApiKey(v) { try { GM_setValue(STORAGE_KEY_API, (v || '').trim()); } catch (e) {} } function getStoredFallbackSteamId() { try { return GM_getValue(STORAGE_KEY_FALLBACK_ID, ''); } catch (e) { return ''; } } function setStoredFallbackSteamId(v) { try { GM_setValue(STORAGE_KEY_FALLBACK_ID, (v || '').trim()); } catch (e) {} } // v1.3.6: AI 模型接口配置存储 (兼容 OpenAI / DeepSeek 等格式) var STORAGE_KEY_AI_API_URL = 'sbc_ai_api_url'; var STORAGE_KEY_AI_API_KEY = 'sbc_ai_api_key'; var STORAGE_KEY_AI_MODEL = 'sbc_ai_model'; function getStoredAiApiUrl() { try { return GM_getValue(STORAGE_KEY_AI_API_URL, 'https://api.deepseek.com/v1/chat/completions'); } catch (e) { return 'https://api.deepseek.com/v1/chat/completions'; } } function setStoredAiApiUrl(v) { try { GM_setValue(STORAGE_KEY_AI_API_URL, (v || '').trim()); } catch (e) {} } function getStoredAiApiKey() { try { return GM_getValue(STORAGE_KEY_AI_API_KEY, ''); } catch (e) { return ''; } } function setStoredAiApiKey(v) { try { GM_setValue(STORAGE_KEY_AI_API_KEY, (v || '').trim()); } catch (e) {} } function getStoredAiModel() { try { return GM_getValue(STORAGE_KEY_AI_MODEL, 'deepseek-v4-pro'); } catch (e) { return 'deepseek-v4-pro'; } } function setStoredAiModel(v) { try { GM_setValue(STORAGE_KEY_AI_MODEL, (v || '').trim()); } catch (e) {} } /** * v1.2.10: 通过 Steam Web API 拉取用户基础数据 * - IPlayerService/GetOwnedGames/v1 → 总游戏数 + 总游玩时长 * - ISteamUser/GetPlayerSummaries/v2 → 用户名 / 头像 / 在线状态 * 若未配置 API Key 或请求失败 → 返回 null,调用方需降级到 XML/页面提取 */ function loadUserStatsWithApi(steamId) { var apiKey = getStoredApiKey(); var useSteamId = (steamId && /^\d{17}$/.test(steamId)) ? steamId : getStoredFallbackSteamId(); if (!apiKey || !useSteamId) return Promise.resolve(null); return new Promise(function(resolve) { var result = { source: 'api', steamId: useSteamId, personaName: '', avatarUrl: '', onlineState: 'offline', totalGames: 0, totalPlaytimeMinutes: 0 }; var pending = 2; var apiSuccessCount = 0; // v1.2.12: 追踪有多少个 API 成功响应 function maybeDone() { pending--; if (pending > 0) return; // v1.2.12: 至少有一个 API 成功响应即认为 Key 有效 // (即便 game_count=0 或用户资料私密,只要 API 返回了有效 JSON 结构即说明 Key 可用) if (apiSuccessCount > 0) resolve(result); else resolve(null); } // 1. IPlayerService/GetOwnedGames GM_xmlhttpRequest({ method: 'GET', url: 'https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=' + encodeURIComponent(apiKey) + '&steamid=' + encodeURIComponent(useSteamId) + '&include_appinfo=0&include_played_free_games=1', timeout: 8000, onload: function(r) { try { var data = JSON.parse(r.responseText); // v1.2.12: 检查是否返回了 error 字段 (Key 无效时 Steam 返回 {response:{error:...}}) if (data && data.response) { if (data.response.error) { // API 明确返回错误 (Key 无效 / 权限不足) console.warn('[SBC-Pro] GetOwnedGames API error:', data.response.error); } else if (typeof data.response.game_count === 'number') { result.totalGames = data.response.game_count; var games = data.response.games || []; var totalMin = 0; for (var i = 0; i < games.length; i++) { totalMin += (parseInt(games[i].playtime_forever, 10) || 0); } result.totalPlaytimeMinutes = totalMin; apiSuccessCount++; } else { // response 存在但无 game_count (可能是私密资料库),仍视为 Key 有效 apiSuccessCount++; } } } catch (e) { /* parse error, keep as failure */ } maybeDone(); }, onerror: function() { maybeDone(); }, ontimeout: function() { maybeDone(); } }); // 2. ISteamUser/GetPlayerSummaries GM_xmlhttpRequest({ method: 'GET', url: 'https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=' + encodeURIComponent(apiKey) + '&steamids=' + encodeURIComponent(useSteamId), timeout: 8000, onload: function(r) { try { var data = JSON.parse(r.responseText); var players = data && data.response && data.response.players; if (players && players.length > 0) { var p = players[0]; result.personaName = p.personaname || ''; result.avatarUrl = p.avatarfull || p.avatarmedium || p.avatar || ''; result.onlineState = p.gameid ? 'in-game' : (p.personastate === 1 ? 'online' : (p.personastate === 2 ? 'busy' : 'offline')); apiSuccessCount++; // v1.2.12: GetPlayerSummaries 成功也计入 } else if (data && data.response && !data.response.error) { // response 存在且无错误,即便 players 为空也视为 Key 有效 apiSuccessCount++; } } catch (e) { /* ignore */ } maybeDone(); }, onerror: function() { maybeDone(); }, ontimeout: function() { maybeDone(); } }); }); } /** * v1.2.10: 用 API 数据回填 KPI 卡片 + 头部信息 */ function updateUserHeaderKpis(userHeader, apiData) { if (!userHeader || !apiData) return; try { // KPI: 总时长 if (typeof apiData.totalPlaytimeMinutes === 'number') { var playtimeSlot = userHeader.querySelector('[data-sbc-kpi="playtime"]'); if (playtimeSlot) { playtimeSlot.textContent = formatPlaytime(apiData.totalPlaytimeMinutes); // 移除 empty 样式 var ptCard = playtimeSlot.closest('.sbc-user-kpi'); if (ptCard) ptCard.classList.remove('sbc-user-kpi--empty'); } if (userInfo) userInfo.totalPlaytimeMinutes = apiData.totalPlaytimeMinutes; } // KPI: 总游戏(若 API 返回 >0) if (apiData.totalGames > 0) { var gamesCard = userHeader.querySelector('.sbc-user-kpi--cyan'); var gamesKpi = gamesCard ? gamesCard.querySelector('.sbc-user-kpi-value') : null; if (gamesKpi && (!userInfo || !userInfo.totalGames)) { gamesKpi.textContent = apiData.totalGames.toLocaleString(); if (gamesCard) { gamesCard.classList.remove('sbc-user-kpi--empty'); var emptySpan = gamesCard.querySelector('.sbc-user-kpi-empty'); if (emptySpan) emptySpan.remove(); } } if (userInfo && !userInfo.totalGames) userInfo.totalGames = apiData.totalGames; } // 用户名 / 头像(如果页面提取为空,API 兜底) if (userInfo) { if (apiData.personaName && !userInfo.personaName) { userInfo.personaName = apiData.personaName; var nameEl = userHeader.querySelector('.sbc-user-name'); if (nameEl) nameEl.textContent = apiData.personaName; } if (apiData.avatarUrl && !userInfo.avatarUrl) { userInfo.avatarUrl = apiData.avatarUrl; var avatarImg = userHeader.querySelector('.sbc-user-avatar img'); if (avatarImg) avatarImg.src = apiData.avatarUrl; } if (apiData.onlineState && userInfo.onlineState === 'offline') { userInfo.onlineState = apiData.onlineState; // 同步更新 class var avatarEl = userHeader.querySelector('.sbc-user-avatar'); if (avatarEl) { avatarEl.classList.remove('sbc-user-online--online', 'sbc-user-online--in-game', 'sbc-user-online--offline'); avatarEl.classList.add('sbc-user-online--' + apiData.onlineState); } } } } catch (e) { console.warn('[SBC-Pro] updateUserHeaderKpis failed:', e); } } /** * v1.2.10: 打开用户设置模态框(Steam ID + API Key) * 数据保存到 GM_setValue,刷新后生效 */ function openUserSettings() { // 防止重复创建 var existing = document.getElementById('sbc-settings-modal'); if (existing) { existing.remove(); } var currentApiKey = getStoredApiKey(); var currentFallbackId = getStoredFallbackSteamId(); var detectedId = (userInfo && userInfo.steamId) || detectCurrentSteamId() || ''; var backdrop = document.createElement('div'); backdrop.className = 'sbc-modal-backdrop'; backdrop.id = 'sbc-settings-modal'; backdrop.innerHTML = '' + '' + '' + '' + '' + '' + '' + '用户信息抓取设置' + '' + '' + '' + '' + '' + '' + '' + '' + // 备用 Steam ID(当页面提取失败时使用) '' + '' + '备用 Steam ID' + '17 位 SteamID64' + '' + '' + '页面 DOM 抓取失败时,作为兜底来源。查询自己的 SteamID →' + '' + // Steam Web API Key '' + '' + 'Steam Web API Key' + '用于抓取总游戏数 / 总时长' + '' + '' + '' + '申请地址: steamcommunity.com/dev/apikey' + '该 Key 仅保存在本地(Tampermonkey 存储),不会上传。' + '' + '' + '' + // v1.3.6: AI 模型接口配置 (兼容 OpenAI / DeepSeek 等格式) '' + '' + 'AI 模型接口配置' + '用于 AI 洞察分析 (兼容 OpenAI 格式)' + '' + '' + 'API 地址' + '' + '默认 DeepSeek,可填 OpenAI 或其他兼容接口地址' + '' + '' + 'API Key' + '' + '仅保存在本地,不会上传' + '' + '' + '模型名称' + '' + '如 deepseek-v4-pro / gpt-4o / gpt-4o-mini 等' + '' + '' + '' + '' + ''; document.body.appendChild(backdrop); var inputId = backdrop.querySelector('#sbc-input-steam-id'); var inputKey = backdrop.querySelector('#sbc-input-api-key'); var inputAiUrl = backdrop.querySelector('#sbc-input-ai-url'); var inputAiKey = backdrop.querySelector('#sbc-input-ai-key'); var inputAiModel = backdrop.querySelector('#sbc-input-ai-model'); var statusEl = backdrop.querySelector('#sbc-form-status'); var closeBtn = backdrop.querySelector('#sbc-modal-close-btn'); var cancelBtn = backdrop.querySelector('#sbc-modal-cancel-btn'); var clearBtn = backdrop.querySelector('#sbc-modal-clear-btn'); var saveBtn = backdrop.querySelector('#sbc-modal-save-btn'); var testBtn = backdrop.querySelector('#sbc-modal-test-btn'); function close() { backdrop.remove(); } function showStatus(msg, kind) { statusEl.className = 'sbc-form-status ' + (kind || ''); statusEl.textContent = msg; } closeBtn.addEventListener('click', close); cancelBtn.addEventListener('click', close); backdrop.addEventListener('click', function(e) { if (e.target === backdrop) close(); }); document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', escHandler); } }); clearBtn.addEventListener('click', function() { inputId.value = ''; inputKey.value = ''; if (inputAiUrl) inputAiUrl.value = ''; if (inputAiKey) inputAiKey.value = ''; if (inputAiModel) inputAiModel.value = ''; setStoredFallbackSteamId(''); setStoredApiKey(''); setStoredAiApiUrl(''); setStoredAiApiKey(''); setStoredAiModel(''); showStatus('已清空所有配置', 'success'); }); saveBtn.addEventListener('click', function() { var idVal = (inputId.value || '').trim(); var keyVal = (inputKey.value || '').trim(); if (idVal && !/^\d{17}$/.test(idVal)) { showStatus('SteamID 格式不正确(应为 17 位数字)', 'error'); inputId.focus(); return; } setStoredFallbackSteamId(idVal); setStoredApiKey(keyVal); // v1.3.6: 保存 AI 配置 if (inputAiUrl) setStoredAiApiUrl(inputAiUrl.value || ''); if (inputAiKey) setStoredAiApiKey(inputAiKey.value || ''); if (inputAiModel) setStoredAiModel(inputAiModel.value || ''); showStatus('已保存,刷新页面生效', 'success'); setTimeout(close, 800); }); testBtn.addEventListener('click', function() { var idVal = (inputId.value || '').trim() || detectedId; var keyVal = (inputKey.value || '').trim(); if (!keyVal) { showStatus('请先填写 API Key', 'error'); return; } if (!idVal) { showStatus('缺少 SteamID', 'error'); return; } testBtn.disabled = true; showStatus('正在测试连接…', 'loading'); // 临时写入以便 loadUserStatsWithApi 使用最新值 setStoredFallbackSteamId(idVal); setStoredApiKey(keyVal); loadUserStatsWithApi(idVal).then(function(data) { testBtn.disabled = false; // v1.2.12: data 非 null 即说明至少一个 API 成功响应(Key 有效) if (data) { var msg = '连接成功!'; if (data.totalGames > 0) msg += ' 游戏数 ' + data.totalGames; if (data.totalPlaytimeMinutes > 0) msg += ' · 总时长 ' + formatPlaytime(data.totalPlaytimeMinutes); if (data.personaName) msg += ' · 用户 ' + data.personaName; showStatus(msg, 'success'); } else { showStatus('连接失败,Key 无效或网络错误', 'error'); } }); }); } // ============================================================ // 数据提取(增强版:含appId、锁定徽章图标) // ============================================================ function extractBadgesFromDOM() { var badges = []; var badgeRows = document.querySelectorAll('.badge_row'); badgeRows.forEach(function(row, index) { try { var linkEl = row.querySelector('.badge_row_overlay'); var url = linkEl ? linkEl.href : ''; var appId = ''; var isFoil = false; var border = '0'; if (url) { // v1.2.3: 检测闪卡合成 (/gamecards/foil/APPID) var foilMatch = url.match(/\/gamecards\/foil\/(\d+)/); if (foilMatch) { appId = foilMatch[1]; isFoil = true; border = '1'; } else { var appMatch = url.match(/\/gamecards\/(\d+)/); if (appMatch) appId = appMatch[1]; } } var titleEl = row.querySelector('.badge_title'); var gameName = ''; if (titleEl) { gameName = titleEl.textContent.replace(/查看详情/g, '').replace(/\s+/g, ' ').trim(); } // 徽章图片(多策略提取) var badgeImg = ''; var badgeInfoImg = row.querySelector('.badge_info_image img'); if (badgeInfoImg) badgeImg = badgeInfoImg.getAttribute('data-delayed-image') || badgeInfoImg.src; // 锁定图标 var lockedIconImg = ''; var lockedIconEl = row.querySelector('.badge_empty_circle img'); if (lockedIconEl) lockedIconImg = lockedIconEl.getAttribute('data-delayed-image') || lockedIconEl.src; if (!lockedIconImg) { var emptyLeftImg = row.querySelector('.badge_empty_left img'); if (emptyLeftImg) lockedIconImg = emptyLeftImg.getAttribute('data-delayed-image') || emptyLeftImg.src; } // 没有已解锁图片时降级 if (!badgeImg) { var ownedCard = row.querySelector('.badge_progress_card.owned img'); if (ownedCard) badgeImg = ownedCard.getAttribute('data-delayed-image') || ownedCard.src; } if (!badgeImg && lockedIconImg) badgeImg = lockedIconImg; if (!badgeImg) { var emptyCircleImg = row.querySelector('.badge_empty_circle img'); if (emptyCircleImg) badgeImg = emptyCircleImg.getAttribute('data-delayed-image') || emptyCircleImg.src; } // 徽章名称 var badgeName = ''; var badgeNameEl = row.querySelector('.badge_info_title'); if (badgeNameEl) badgeName = badgeNameEl.textContent.trim(); if (!badgeName) { var emptyNameEls = row.querySelectorAll('.badge_empty_name'); if (emptyNameEls.length > 0) badgeName = emptyNameEls[0].textContent.trim(); } // 经验值 & 等级 var xp = 100, level = 1; var descEl = row.querySelector('.badge_info_description'); if (descEl) { var descText = descEl.textContent; var xpMatch = descText.match(/(\d+)\s*点经验值/); if (xpMatch) xp = parseInt(xpMatch[1]); var levelMatch = descText.match(/(\d+)\s*级/); if (levelMatch) level = parseInt(levelMatch[1]); } if (xp === 100) { var emptyRight = row.querySelector('.badge_empty_right'); if (emptyRight) { var xpMatch2 = emptyRight.textContent.match(/(\d+)\s*点经验值/); if (xpMatch2) xp = parseInt(xpMatch2[1]); } } var unlockEl = row.querySelector('.badge_info_unlocked'); var unlockTime = unlockEl ? unlockEl.textContent.replace('解锁', '').trim() : ''; // 卡牌进度 var progressEl = row.querySelector('.badge_progress_info'); var progressText = '', collected = 0, total = 0; if (progressEl) { progressText = progressEl.textContent.trim(); var match = progressText.match(/(\d+)\s*\/\s*(\d+)/); if (match) { collected = parseInt(match[1]); total = parseInt(match[2]); } } // v1.2.14-fix10: 检测 Steam 原生"收集齐全"合卡按钮(可合卡的真正信号) // .badge_progress_info 内的 .badge_craft_button 文本为"收集齐全"时,表示用户当前拥有足够卡牌可立即合成 var hasCraftButton = false; if (progressEl) { var craftBtn = progressEl.querySelector('.badge_craft_button'); if (craftBtn && /收集齐全/.test(craftBtn.textContent)) { hasCraftButton = true; // 有合卡按钮时,从进度文本补齐 collected/total(若未匹配到) if (total === 0) { var dbCardCount = appId ? getCardCountForApp(appId) : 0; if (dbCardCount > 0) { total = dbCardCount; collected = dbCardCount; } } } } // 掉落 var drops = 0, dropsText = ''; var dropsEl = row.querySelector('.badge_title_stats_drops .progress_info_bold'); if (dropsEl) { dropsText = dropsEl.textContent.trim(); var dropsMatch = dropsText.match(/(\d+)\s*张剩余卡牌掉落/); if (dropsMatch) drops = parseInt(dropsMatch[1]); } // 游玩时间 var playtime = ''; var playtimeEl = row.querySelector('.badge_title_stats_playtime'); if (playtimeEl) { playtime = playtimeEl.textContent.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); if (playtime === '' || playtime === ' ') playtime = ''; } // 卡牌图片 var cardImgs = []; row.querySelectorAll('.badge_progress_card.owned img').forEach(function(img) { var src = img.getAttribute('data-delayed-image') || img.src; if (src) cardImgs.push(upgradeCardImageUrl(src)); }); // v1.2.14-fix10: isCompleted 仅在 collected>=total 时为 true(恢复原逻辑) // 移除 fix9 错误逻辑(原把所有 level>0 的已合成徽章都标记为 isCompleted,导致 150 个全计入可合卡) // 可合卡判断改用 hasCraftButton(Steam 原生合卡按钮) 或 collected>=total // v1.5.2: level>0 且 total=0 的已合成徽章(卡牌已被消耗)也标记为 isCompleted // 安全性: getCraftableBadges 检查 isCompleted && total>0, total=0 不会被误判为可合卡 var isCompleted = (collected >= total && total > 0) || hasCraftButton || (level > 0 && total === 0); // v1.5.2: level>0 的已合成徽章不应标记为 locked var isLocked = !isCompleted && collected === 0 && level === 0; // v1.2.3: 勋章分类 — 无 appId 或 appId 不在卡牌数据库中则为活动勋章 // v1.2.13: 初步分类,使用 classifyBadgeType(数据库加载后再次修正) var badgeType = classifyBadgeType({ appId: appId, badgeName: badgeName, gameName: gameName, isFoil: isFoil }); if (gameName) { badges.push({ index: index, appId: appId, gameName: gameName, badgeName: badgeName || gameName, badgeImg: badgeImg, lockedIconImg: lockedIconImg, url: url, xp: xp, level: level, unlockTime: unlockTime, progressText: progressText, collected: collected, total: total, progress: total > 0 ? Math.round((collected / total) * 100) : 0, drops: drops, dropsText: dropsText, playtime: playtime, cardImgs: cardImgs, isCompleted: isCompleted, isLocked: isLocked, // v1.2.14-fix10: 新增 hasCraftButton(Steam 原生合卡按钮) hasCraftButton: hasCraftButton, // v1.2.3: 新增字段 badgeType: badgeType, isFoil: isFoil, border: border, }); } } catch (e) { console.warn('[SBC-Pro] 提取徽章数据失败:', e); } }); console.log('[SBC-Pro] 提取到 ' + badges.length + ' 个徽章'); // v1.5.0: 异步缓存卡牌图片 URL (fire-and-forget,只增不减) badges.forEach(function(b) { if (b.appId && b.cardImgs && b.cardImgs.length > 0) { cacheCardImageUrls(b.appId, b.cardImgs); } }); return badges; } // ============================================================ // v1.2.1: SteamCardExchange 卡牌数据库 // 数据源: https://www.steamcardexchange.net/api/request.php?GetInventory // 返回格式: { data: [ [[appid, name], ..., [size]], ... ] } // 解析为: { appId: { name, size } } // ============================================================ /** * 获取全部有交易卡牌的游戏数据库(带本地缓存) * @returns {Promise} { appId: { name, size } } */ // v1.3.1 R2: 使用 gmFetchJson 替代原始 GM_xmlhttpRequest,自动获得重试能力 async function fetchCardDatabase() { // v1.5.0: 使用 IndexedDB 缓存 (gameData 仓库,永不失效,只增不减) var CARD_DB_IDB_KEY = 'cardDatabase'; // v1.5.0: 确保 IDB 已初始化 (如果 init() 中异步初始化尚未完成) await IDB.init(); // v1.5.0: 缓存版本控制 — 版本不匹配时清除旧 IDB 缓存 var CARD_DB_VER_KEY = 'sbc_card_db_cache_ver'; if (ensureCacheVersion(CARD_DB_VER_KEY, CONFIG.CARD_DB_CACHE_VERSION)) { try { await IDB.clearStore('gameData'); GM_setValue(CONFIG.CARD_DB_CACHE_KEY, null); } catch (e) {} setCacheVersion(CARD_DB_VER_KEY, CONFIG.CARD_DB_CACHE_VERSION); console.log('[SBC-Pro] 卡牌数据库缓存版本更新,清除旧缓存'); } // v1.5.0: 优先从 IndexedDB 读取缓存 (永不失效) if (IDB.isReady()) { var idbCached = await IDB.get('gameData', CARD_DB_IDB_KEY); if (idbCached && idbCached.data && Object.keys(idbCached.data).length > 0) { console.log('[SBC-Pro] 使用 IDB 卡牌数据库缓存(' + Object.keys(idbCached.data).length + ' 个游戏)'); // 后台静默更新 (合并新数据,只增不减) _refreshCardDatabaseInBackground(CARD_DB_IDB_KEY, idbCached.data); return idbCached.data; } } // GM 回退 var gmCached = GM_getValue(CONFIG.CARD_DB_CACHE_KEY, null); if (gmCached && gmCached.timestamp && (Date.now() - gmCached.timestamp < CONFIG.CARD_DB_CACHE_TTL)) { console.log('[SBC-Pro] 使用 GM 卡牌数据库缓存(' + Object.keys(gmCached.data).length + ' 个游戏)'); // 迁移到 IDB if (IDB.isReady()) { await IDB.set('gameData', CARD_DB_IDB_KEY, { data: gmCached.data, timestamp: gmCached.timestamp }); } _refreshCardDatabaseInBackground(CARD_DB_IDB_KEY, gmCached.data); return gmCached.data; } console.log('[SBC-Pro] 正在从 SteamCardExchange 获取卡牌数据库...'); try { var json = await gmFetchJson(CONFIG.CARD_DB_API, { timeout: 30000, retries: 2 }); if (!json || !json.data || !Array.isArray(json.data)) { throw new Error('API 返回数据格式异常'); } // 解析数据: data[i] = [[appid, name], ..., [size]] var db = {}; var count = 0; json.data.forEach(function(current) { if (!current || !current[0] || !current[3]) return; var appId = String(current[0][0]); var name = current[0][1] || ''; var size = current[3][0] || 0; if (appId && size) { db[appId] = { name: name, size: size }; count++; } }); console.log('[SBC-Pro] 卡牌数据库加载完成: ' + count + ' 个游戏'); // v1.5.0: 缓存到 IndexedDB (永不失效) + GM 回退 if (IDB.isReady()) { await IDB.set('gameData', CARD_DB_IDB_KEY, { data: db, timestamp: Date.now() }); } try { GM_setValue(CONFIG.CARD_DB_CACHE_KEY, { timestamp: Date.now(), data: db }); } catch (e) {} return db; } catch (e) { console.error('[SBC-Pro] 卡牌数据库获取失败:', e); // v1.5.0: API 失败时尝试返回过期缓存 (有总比没有好) if (IDB.isReady()) { var fallback = await IDB.get('gameData', CARD_DB_IDB_KEY); if (fallback && fallback.data) { console.log('[SBC-Pro] API 失败,使用过期 IDB 缓存(' + Object.keys(fallback.data).length + ' 个游戏)'); return fallback.data; } } throw new Error('卡牌数据库获取失败: ' + e.message); } } // v1.5.0: 后台静默刷新卡牌数据库 (合并新数据,只增不减) async function _refreshCardDatabaseInBackground(idbKey, existingData) { try { var json = await gmFetchJson(CONFIG.CARD_DB_API, { timeout: 30000, retries: 1 }); if (!json || !json.data || !Array.isArray(json.data)) return; var newDb = {}; json.data.forEach(function(current) { if (!current || !current[0] || !current[3]) return; var appId = String(current[0][0]); var name = current[0][1] || ''; var size = current[3][0] || 0; if (appId && size) newDb[appId] = { name: name, size: size }; }); // 合并: 只增加新游戏,不删除已有 (只增不减) var merged = Object.assign({}, existingData, newDb); var oldCount = Object.keys(existingData).length; var newCount = Object.keys(merged).length; if (newCount > oldCount) { if (IDB.isReady()) { await IDB.set('gameData', idbKey, { data: merged, timestamp: Date.now() }); } cardDatabase = merged; console.log('[SBC-Pro] 卡牌数据库后台更新: ' + oldCount + ' → ' + newCount + ' 个游戏'); // 如果当前在全部游戏视图,刷新 if (viewMode === 'alldb' || viewMode === 'activity') { renderCardView(); } } } catch (e) { // 后台更新失败静默处理 } } /** * 异步加载卡牌数据库并更新 UI */ async function loadCardDatabase() { if (cardDbLoaded || cardDbLoading) return; cardDbLoading = true; // 更新工具栏按钮状态 var dbBtn = document.getElementById('sbc-mode-alldb'); if (dbBtn) { dbBtn.innerHTML = '加载中...'; } try { cardDatabase = await fetchCardDatabase(); cardDbLoaded = true; // 构建已拥有 AppID 集合 ownedAppIds = {}; allBadges.forEach(function(b) { if (b.appId) ownedAppIds[b.appId] = true; }); // v1.2.3: 数据库加载后重新分类勋章(修正活动/游戏分类) classifyBadges(); var totalGames = Object.keys(cardDatabase).length; var ownedCount = Object.keys(ownedAppIds).filter(function(id) { return cardDatabase[id]; }).length; showToast('卡牌数据库加载完成: ' + totalGames + ' 款游戏,你已拥有 ' + ownedCount + ' 款', 'success'); // 如果当前在全部游戏视图或活动勋章视图,刷新 if (viewMode === 'alldb' || viewMode === 'activity') { renderCardView(); } else { // 否则只更新卡片上的最高等级显示 updateCardMaxLevelBadges(); } // v1.2.2: 卡牌数据库加载后,升级潜力维度有了准确的最大等级数据,重算价值评分 // 仅在已有市场价格数据时重算(避免覆盖无价格时的默认评分) if (Object.keys(marketPrices).length > 0) { computeValueScores(); if (analysisVisible) renderAnalysisContent(); } } catch (e) { showToast('卡牌数据库加载失败: ' + e.message, 'error'); console.error('[SBC-Pro] 卡牌数据库加载失败:', e); } finally { cardDbLoading = false; // 恢复按钮 if (dbBtn) { dbBtn.innerHTML = createAllDbButtonInner(); } } } /** * 获取某个游戏的最大徽章等级 */ // v1.2.3: 重命名语义 — size 是单次合卡所需卡牌数,非最高等级 function getCardCountForApp(appId) { if (!appId || !cardDatabase[appId]) return 0; return cardDatabase[appId].size || 0; } // 保留旧名称兼容(已弃用,内部转发到 getCardCountForApp) function getMaxLevelForApp(appId) { return getCardCountForApp(appId); } /** * v1.2.3: 获取徽章的合成上限等级 * 游戏普通徽章: 5 级 | 游戏闪卡徽章: 1 级 | 活动勋章: 不定(查静态数据) */ function getMaxBadgeLevel(badge) { if (!badge) return 0; // 活动勋章:查找静态数据库 if (badge.badgeType === 'activity') { var matched = findActivityBadge(badge); if (matched) return matched.maxLevel; // -1 表示无上限 return 0; // 未知 } // 游戏勋章 if (badge.isFoil) return CONFIG.FOIL_BADGE_MAX_LEVEL; // 闪卡 1 级 return CONFIG.GAME_BADGE_MAX_LEVEL; // 普通 5 级 } /** * v1.2.3: 在活动勋章静态数据中查找匹配项 * v1.5.5: 按 appId/badgeName 缓存结果,避免 computeValueScores 中反复线性扫描 */ var _activityBadgeCache = {}; function findActivityBadge(badge) { var cacheKey = badge.appId || badge.badgeName || badge.gameName || ''; if (cacheKey && Object.prototype.hasOwnProperty.call(_activityBadgeCache, cacheKey)) { return _activityBadgeCache[cacheKey]; } var result = _findActivityBadgeImpl(badge); if (cacheKey) _activityBadgeCache[cacheKey] = result; return result; } function _findActivityBadgeImpl(badge) { for (var i = 0; i < STEAM_ACTIVITY_BADGES.length; i++) { var ab = STEAM_ACTIVITY_BADGES[i]; if (ab.appId && badge.appId === ab.appId) return ab; // 名称模糊匹配 if (ab.name && badge.badgeName && badge.badgeName.toLowerCase().indexOf(ab.name.toLowerCase()) >= 0) return ab; if (ab.name && badge.gameName && badge.gameName.toLowerCase().indexOf(ab.name.toLowerCase()) >= 0) return ab; } return null; } // ============================================================ // v1.2.12: 活动勋章图标加载(SteamCDN + 分类占位兜底) // ============================================================ /** * v1.2.12: 根据活动类型生成分类占位 SVG 图标 */ function getActivityPlaceholderIcon(type) { var placeholders = { sale: '🎉Sale', collection: '📚Cards', nomination: '🏆Nominate', replay: '🎮Replay', event: '🎊Event', awards: '⭐Awards', community: '🤝Community', special: '✨Special', }; return placeholders[type] || '?'; } /** * v1.2.12: 解析活动勋章的 SteamCDN 图标 URL */ function resolveActivityIconUrl(activityBadge) { var appId = activityBadge.appId; if (appId) { return CONFIG.ACTIVITY_ICON_CDN + appId + '/icon_64x64.png'; } return null; } /** * v1.2.12: 异步加载活动勋章真实图标,加载失败时保留占位 */ function loadActivityIconAsync(card, activityBadge) { var iconUrl = resolveActivityIconUrl(activityBadge); if (!iconUrl) return; var img = new Image(); img.onload = function() { var placeholderEl = card.querySelector('.sbc-activity-card-icon svg, .sbc-activity-card-icon .sbc-activity-icon-placeholder'); if (placeholderEl) { var newImg = document.createElement('img'); newImg.className = 'sbc-activity-icon-img'; newImg.src = iconUrl; newImg.alt = activityBadge.name || ''; newImg.loading = 'lazy'; placeholderEl.replaceWith(newImg); } }; img.onerror = function() { // 加载失败,保留占位 SVG }; img.src = iconUrl; } /** * v1.2.13: 判断是否为事件/季节徽章(名称关键词 / 超大 appId) */ function isEventBadgeName(name) { if (!name) return false; var n = String(name).toLowerCase(); for (var i = 0; i < CONFIG.EVENT_KEYWORDS.length; i++) { if (n.indexOf(CONFIG.EVENT_KEYWORDS[i].toLowerCase()) !== -1) return true; } return false; } function isEventBadge(badge) { if (!badge) return false; var appId = parseInt(String(badge.appId), 10) || 0; if (appId > CONFIG.EVENT_APPID_THRESHOLD) return true; if (isEventBadgeName(badge.gameName) || isEventBadgeName(badge.badgeName)) return true; return false; } function classifyBadgeType(badge) { if (badge.isFoil) return 'foil'; if (!badge.appId) return 'community'; if (isEventBadge(badge)) return 'event'; if (cardDbLoaded && cardDatabase[badge.appId]) return 'game'; // 有 appId 但不在卡牌数据库,且 cardDb 已加载 → 事件/季节(兜底) if (cardDbLoaded && !cardDatabase[badge.appId]) return 'event'; // cardDb 未加载时,无法判断,保守按游戏处理(后续加载后修正) return 'game'; } /** * v1.2.3: 卡牌数据库加载后重新分类勋章 * 无 appId → community;超大 appId / 名称关键词 → event;在卡牌数据库中 → game;否则 → event */ function classifyBadges() { activityBadges = []; gameBadges = []; allBadges.forEach(function(badge) { badge.badgeType = classifyBadgeType(badge); if (badge.badgeType === 'game' || badge.badgeType === 'foil') { gameBadges.push(badge); } else { activityBadges.push(badge); } }); console.log('[SBC-Pro] 勋章分类: 活动/社区/节日 ' + activityBadges.length + ' 个, 游戏 ' + gameBadges.length + ' 个'); } /** * 获取未拥有的卡牌游戏列表(差集) * @returns {Array} [{ appId, name, size }] */ function getUnownedCardGames() { var result = []; for (var appId in cardDatabase) { if (!ownedAppIds[appId]) { result.push({ appId: appId, name: cardDatabase[appId].name, size: cardDatabase[appId].size }); } } return result; } /** * 获取全部卡牌游戏列表(已拥有 + 未拥有) * @returns {Array} [{ appId, name, size, isOwned }] */ function getAllCardGames() { var result = []; var seen = {}; // 先加已拥有的 allBadges.forEach(function(b) { if (b.appId && !seen[b.appId]) { seen[b.appId] = true; var maxLvl = getCardCountForApp(b.appId); // v1.2.3: size = 单次合卡所需卡牌数 result.push({ appId: b.appId, name: b.gameName, size: maxLvl, isOwned: true, badge: b }); } }); // 再加未拥有的 for (var appId in cardDatabase) { if (!seen[appId]) { seen[appId] = true; result.push({ appId: appId, name: cardDatabase[appId].name, size: cardDatabase[appId].size, isOwned: false, badge: null }); } } return result; } /** * 更新已有卡片上的最高等级角标 */ function updateCardMaxLevelBadges() { if (!cardDbLoaded) return; // v1.5.5: 一次性抓取当前渲染的卡片并建 index→element 索引,避免每个徽章一次全文档 querySelector var cardEls = document.querySelectorAll('.sbc-card[data-index]'); var cardByIndex = {}; for (var ci = 0; ci < cardEls.length; ci++) { cardByIndex[cardEls[ci].getAttribute('data-index')] = cardEls[ci]; } allBadges.forEach(function(badge) { if (!badge.appId) return; var maxLvl = getCardCountForApp(badge.appId); // v1.2.3: 卡牌数,非等级 if (maxLvl <= 0) return; var cardEl = cardByIndex[String(badge.index)]; if (!cardEl) return; // 如果已有角标则不重复添加 if (cardEl.querySelector('.sbc-card-maxlevel-badge')) return; var imgWrap = cardEl.querySelector('.sbc-card-img-wrap'); if (!imgWrap) return; var badge_el = document.createElement('div'); badge_el.className = 'sbc-card-maxlevel-badge'; badge_el.textContent = maxLvl + ' 张卡'; imgWrap.appendChild(badge_el); }); } // ============================================================ // 货币识别模块(参考 steam-shop-history-sort 的主货币识别逻辑) // ============================================================ var CURRENCIES = [ { id: 'HKD', label: '港币', match: /HK\$\s*[\d,. ]+/, symbol: 'HK$' }, { id: 'TWD', label: '新台币', match: /NT\$\s*[\d,. ]+/, symbol: 'NT$' }, { id: 'AUD', label: '澳元', match: /A\$\s*[\d,. ]+/, symbol: 'A$' }, { id: 'CAD', label: '加元', match: /CDN\$\s*[\d,. ]+/, symbol: 'CDN$' }, { id: 'NZD', label: '新西兰元', match: /NZ\$\s*[\d,. ]+/, symbol: 'NZ$' }, { id: 'ARS', label: '阿根廷比索', match: /ARS\$\s*[\d,. ]+/, symbol: 'ARS$' }, { id: 'SGD', label: '新加坡元', match: /(?', TWD:'', AUD:'', CAD:'', NZD:'', ARS:'', SGD:'', COL:'', CLP:'', MexP:'', BRL:'', UYU:'', USD:'', CNY:'', JPY:'', EUR:'', GBP:'', KRW:'', RUB:'', TRY:'', UAH:'', INR:'', VND:'', IDR:'', PHP:'', KZT:'', THB:'', MYR:'', CRC:'', PEN:'', PLN:'', NOK:'', CHF:'', ILS:'', SAR:'', QAR:'', KWD:'', AED:'', }; var primaryCurrency = CURRENCY_MAP.CNY; var primaryCurrencyResolved = false; // v1.2.14-fix7: 获取用户钱包国家代码(用于显示地区旗帜) var primaryCountryCode = ''; function detectPrimaryCountryCode() { if (primaryCountryCode) return primaryCountryCode; try { var win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; var walletInfo = win.g_rgWalletInfo || {}; primaryCountryCode = (walletInfo.wallet_country || win.g_strCountryCode || '').toUpperCase(); } catch (e) { primaryCountryCode = ''; } // v1.2.14-fix8: 如果全局变量无国家代码,从主货币反查国家代码 if (!primaryCountryCode) { var cur = detectPrimaryCurrency(); if (cur && cur.id) { var reverseMap = { 'CNY':'CN','USD':'US','INR':'IN','JPY':'JP','EUR':'DE','GBP':'GB','KRW':'KR','RUB':'RU','TRY':'TR','UAH':'UA','VND':'VN','IDR':'ID','PHP':'PH','KZT':'KZ','THB':'TH','MYR':'MY','BRL':'BR','AUD':'AU','CAD':'CA','HKD':'HK','TWD':'TW','SGD':'SG','NZD':'NZ' }; primaryCountryCode = reverseMap[cur.id] || ''; } } return primaryCountryCode; } // ¥ 究竟是 CNY 还是 JPY:带小数点的为 CNY,纯整数的为 JPY function resolveYen(text) { return /\.\d+/.test(text) ? 'CNY' : 'JPY'; } // 主货币识别:优先用 Steam 全局变量,其次 DOM 文本扫描,最后默认 CNY function detectPrimaryCurrency() { if (primaryCurrencyResolved) return primaryCurrency; try { var win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // 1) Steam 全局变量 g_strCurrencyCode(最准确,所有页面都注入) if (win.g_strCurrencyCode && CURRENCY_MAP[win.g_strCurrencyCode]) { primaryCurrency = CURRENCY_MAP[win.g_strCurrencyCode]; primaryCurrencyResolved = true; return primaryCurrency; } // 2) g_rgWalletInfo.wallet_currency + GetCurrencyCode var walletInfo = win.g_rgWalletInfo || {}; if (typeof win.GetCurrencyCode === 'function' && walletInfo.wallet_currency != null) { var code = win.GetCurrencyCode(walletInfo.wallet_currency); if (code && CURRENCY_MAP[code]) { primaryCurrency = CURRENCY_MAP[code]; primaryCurrencyResolved = true; return primaryCurrency; } } // v1.2.14-fix7: 2.5) 通过 g_strCountryCode 推断货币(钱包国家代码) var countryCode = (win.g_strCountryCode || walletInfo.wallet_country || '').toUpperCase(); if (countryCode && COUNTRY_CURRENCY_MAP[countryCode]) { var mapped = CURRENCY_MAP[COUNTRY_CURRENCY_MAP[countryCode]]; if (mapped) { primaryCurrency = mapped; primaryCurrencyResolved = true; return primaryCurrency; } } } catch (e) { /* 忽略,走 DOM 检测 */ } // 3) DOM 文本扫描(备用方案,参考消费分类脚本的 detectPrimaryCurrency) // v1.2.14-fix7: 收紧扫描范围,只在明确的价格元素中查找,避免误匹配任意 $ 文本 try { var scanSelectors = '.badge_title_stats_drops, .progress_info_bold, .badge_info_description, ' + '.wallet_history_table td, .price, .discount_prices, .game_purchase_price, ' + '.badge_progress_info, .profile_xp_block, .game_area_purchase_game .price, ' + '.search_price, .market_listing_price'; var scanNodes = document.querySelectorAll(scanSelectors); for (var i = 0; i < scanNodes.length; i++) { var text = (scanNodes[i].textContent || '').trim(); if (!text) continue; // 优先识别 ¥(区分 CNY/JPY) if (/¥/.test(text) && !/JP¥|JPY/i.test(text)) { primaryCurrency = CURRENCY_MAP[resolveYen(text)]; primaryCurrencyResolved = true; return primaryCurrency; } // 按顺序匹配其他货币(排除裸 $ 避免误判——要求 $ 后紧跟数字) for (var j = 0; j < CURRENCIES.length; j++) { if (CURRENCIES[j].id === 'USD') { // v1.2.14-fix7: 裸 $ 只在确实出现 "$数字" 且无其他更强匹配时才用 if (/\$\s*\d/.test(text)) { primaryCurrency = CURRENCIES[j]; primaryCurrencyResolved = true; return primaryCurrency; } continue; } if (CURRENCIES[j].match.test(text)) { primaryCurrency = CURRENCIES[j]; primaryCurrencyResolved = true; return primaryCurrency; } } } } catch (e) { /* 忽略 */ } // 4) 默认 CNY primaryCurrencyResolved = true; return primaryCurrency; } // ============================================================ // 获取账号所有游戏列表(通过游戏库页面抓取) // ============================================================ var ownedGamesCache = null; // [{ appid, name }] // v1.3.3: Steam 游戏库页面已迁移到 React 架构,rgGames 变量不再存在于静态 HTML 中 // 改为优先使用 dynamicstore/userdata API(已登录用户可用),rgGames 正则作为兼容回退 async function fetchOwnedGamesFromDynamicStore() { try { var data = await gmFetchJson('https://store.steampowered.com/dynamicstore/userdata/', { timeout: CONFIG.GM_FETCH_DEFAULT_TIMEOUT, retries: 1 }); if (data && Array.isArray(data.rgOwnedApps) && data.rgOwnedApps.length > 0) { var games = data.rgOwnedApps.map(function(appId) { return { appid: String(appId), name: '' }; }); console.log('[SBC-Pro] 从 dynamicstore 获取 ' + games.length + ' 款游戏'); return games; } } catch (e) { console.warn('[SBC-Pro] dynamicstore/userdata 请求失败:', e.message); } return null; } async function fetchOwnedGamesFromPage() { if (ownedGamesCache) return ownedGamesCache; // v1.3.3: 优先使用 dynamicstore API var dsGames = await fetchOwnedGamesFromDynamicStore(); if (dsGames && dsGames.length > 0) { ownedGamesCache = dsGames; return ownedGamesCache; } // 兼容回退: 从游戏库页面 HTML 提取 rgGames(旧版 Steam 页面格式) var steamId = (typeof unsafeWindow !== 'undefined' && unsafeWindow.g_steamID) || ''; if (!steamId) { var m = location.pathname.match(/\/profiles\/(\d+)/); if (m) steamId = m[1]; } if (!steamId) { console.warn('[SBC-Pro] 无法获取 SteamID'); return []; } var url = 'https://steamcommunity.com/profiles/' + steamId + '/games?tab=all'; var text; try { text = await fmRequest(url); } catch (e) { console.warn('[SBC-Pro] 游戏库页面请求失败:', e.message); return []; } // 页面内嵌 rgGames 数组,格式: var rgGames = [{appid:123,name:"...",...}, ...]; var match = text.match(/var\s+rgGames\s*=\s*(\[.+?\]);/s); if (!match) { console.warn('[SBC-Pro] 游戏库页面未找到 rgGames 数据(Steam 已迁移到 React 架构,使用 dynamicstore 失败时回退到徽章数据)'); return []; } try { var games = JSON.parse(match[1]); ownedGamesCache = games.map(function(g) { return { appid: String(g.appid), name: g.name || '' }; }); console.log('[SBC-Pro] 从游戏库获取 ' + ownedGamesCache.length + ' 款游戏'); return ownedGamesCache; } catch (e) { console.warn('[SBC-Pro] rgGames 解析失败:', e.message); return []; } } // ============================================================ // 社区市场价格服务 // ============================================================ var MARKET_SEARCH_API = 'https://steamcommunity.com/market/search/render/'; var MARKET_LISTING_PAGE = 'https://steamcommunity.com/market/listings/753/'; function getCacheKey(appId) { return 'sbc-market-' + appId; } // v1.5.0: 异步读取市场价格缓存 (IDB 优先, GM 回退) // v1.5.3: 支持 SWR 模式 — stale 缓存(36-48H)也返回,由调用方决定是否后台刷新 async function readMarketCache(appId) { var key = getCacheKey(appId); if (IDB.isReady()) { var idbData = await IDB.get('marketPrices', key); if (idbData) { if (!idbData.v || idbData.v < CONFIG.MARKET_CACHE_VERSION) return null; if (!idbData.updatedAt) return null; var age = Date.now() - idbData.updatedAt; if (age > CONFIG.MARKET_CACHE_TTL) return null; // 过期 return idbData; } } // GM 回退 try { var raw = GM_getValue(key, null); if (!raw) return null; var data = typeof raw === 'string' ? JSON.parse(raw) : raw; if (!data.v || data.v < CONFIG.MARKET_CACHE_VERSION) return null; if (!data.updatedAt || Date.now() - data.updatedAt > CONFIG.MARKET_CACHE_TTL) return null; // 迁移到 IDB if (IDB.isReady()) { await IDB.set('marketPrices', key, data, CONFIG.MARKET_CACHE_TTL); } return data; } catch (e) { return null; } } // v1.5.3: 判断缓存是否 stale (36-48H区间,可用但需后台刷新) function isCacheStale(data) { if (!data || !data.updatedAt) return false; var age = Date.now() - data.updatedAt; return age > CONFIG.MARKET_CACHE_STALE_THRESHOLD && age <= CONFIG.MARKET_CACHE_TTL; } // v1.5.0: 批量读取市场价格缓存 (用于初始化时一次性加载) // v1.5.3: stale缓存(36-48H)也返回,由调用方分类处理 async function bulkReadMarketCache(appIds) { if (!IDB.isReady()) return {}; var keys = appIds.map(getCacheKey); var results = await IDB.bulkGet('marketPrices', keys); // 版本和 TTL 检查 (stale 也保留,由调用方通过 isCacheStale 判断) var valid = {}; for (var k in results) { var data = results[k]; if (data && data.v && data.v >= CONFIG.MARKET_CACHE_VERSION && data.updatedAt && Date.now() - data.updatedAt <= CONFIG.MARKET_CACHE_TTL) { valid[k] = data; } } return valid; } // v1.5.0: 异步写入市场价格缓存 (IDB 优先, GM 回退, fire-and-forget) // v1.5.3: 写入时加入随机抖动TTL,实现错峰过期 async function writeMarketCache(appId, data) { try { data.v = CONFIG.MARKET_CACHE_VERSION; var key = getCacheKey(appId); // v1.5.3: 随机抖动TTL (base ± jitter),每个游戏的过期时间自然分散 var jitter = (Math.random() - 0.5) * 2 * CONFIG.MARKET_CACHE_TTL_JITTER; var ttl = Math.max(CONFIG.MARKET_CACHE_STALE_THRESHOLD, CONFIG.MARKET_CACHE_TTL + jitter); if (IDB.isReady()) { await IDB.set('marketPrices', key, data, ttl); } // GM 回退 (仅小数据,GM无TTL概念,updatedAt仍由读取侧检查) try { GM_setValue(key, JSON.stringify(data)); } catch (e) {} } catch (e) {} } function buildMarketApiUrl(appId, start, count) { return MARKET_SEARCH_API + '?query=&start=' + start + '&count=' + count + '&search_descriptions=0&sort_column=name&sort_dir=asc&appid=753&norender=1' + '&category_753_Game[]=tag_app_' + appId + '&category_753_item_class[]=tag_item_class_2' + '&category_753_cardborder[]=tag_cardborder_0'; } // v1.2.14: 支持指定卡牌边框(0=普通, 1=闪卡),参考 steam-store-card-prices.buildMarketApiUrl function buildMarketApiUrlEx(appId, foil, start, count) { return MARKET_SEARCH_API + '?query=&start=' + start + '&count=' + count + '&search_descriptions=0&sort_column=name&sort_dir=asc&appid=753&norender=1' + '&category_753_Game[]=tag_app_' + appId + '&category_753_item_class[]=tag_item_class_2' + '&category_753_cardborder[]=tag_cardborder_' + (foil ? '1' : '0'); } // v1.3.1 R2: fmRequest 升级为 gmFetchText 的薄包装,自动获得重试能力 function fmRequest(url) { return gmFetchText(url, { timeout: CONFIG.GM_FETCH_DEFAULT_TIMEOUT, retries: CONFIG.GM_FETCH_DEFAULT_RETRIES }); } async function fetchCardsForApp(appId) { // v1.2.14: 重构为同时抓取普通卡 + 闪卡,参考 steam-store-card-prices.fetchCardGroup var regularCards = await _fetchCardGroup(appId, false); var foilCards = CONFIG.MARKET_QUERY_FOIL_CARDS ? await _fetchCardGroup(appId, true) : []; // 合并(向后兼容: cards = regular + foil) var allCards = regularCards.concat(foilCards); // 去重(以 hashName 为键) var seen = {}, unique = []; allCards.forEach(function(c) { if (!seen[c.hashName]) { seen[c.hashName] = true; unique.push(c); } }); // 普通卡在前,闪卡在后;同组内按名称排序 unique.sort(function(a, b) { if (a.foil !== b.foil) return a.foil ? 1 : -1; return a.name.localeCompare(b.name, undefined, { numeric: true }); }); // 统计 var totalCost = 0, priceCount = 0; var foilTotalCost = 0, foilPriceCount = 0; var regularTotalCost = 0, regularPriceCount = 0; // v1.3.2: 收集所有价格用于中位数/忽略最高价计算 (借鉴 Trading Card Info 脚本) var allPrices = [], regularPrices = [], foilPrices = []; unique.forEach(function(c) { if (c.sellPrice !== null && c.sellPrice > 0) { totalCost += c.sellPrice; priceCount++; allPrices.push(c.sellPrice); if (c.foil) { foilTotalCost += c.sellPrice; foilPriceCount++; foilPrices.push(c.sellPrice); } else { regularTotalCost += c.sellPrice; regularPriceCount++; regularPrices.push(c.sellPrice); } } }); // v1.3.2: 计算中位数、忽略最高价均价、预计税后总收入 (借鉴 Trading Card Info 脚本统计模式) function calcMedian(arr) { if (!arr.length) return 0; var s = arr.slice().sort(function(a, b) { return a - b; }); var mid = Math.floor(s.length / 2); return s.length % 2 !== 0 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2); } function calcAvgNoMax(arr) { if (arr.length === 0) return 0; if (arr.length === 1) return arr[0]; var s = arr.slice().sort(function(a, b) { return a - b; }); s.pop(); // 移除最高价 return Math.round(s.reduce(function(sum, p) { return sum + p; }, 0) / s.length); } // 预计税后总收入: 全套均价 * ceil(卡牌数/2) 的到手价 (用已有的 _calculateSellerReceives) var avgPrice = priceCount > 0 ? Math.round(totalCost / priceCount) : 0; var dropCount = unique.length > 0 ? Math.ceil(unique.length / 2) : 0; var grossIncome = avgPrice * dropCount; var netIncome = grossIncome > 0 ? _calculateSellerReceives(grossIncome, CONFIG.DEFAULT_PUBLISHER_FEE_PERCENT) : 0; return { appId: appId, cards: unique, // 向后兼容: 合并数组 regularCards: regularCards, // v1.2.14: 普通卡分组 foilCards: foilCards, // v1.2.14: 闪卡分组 cardCount: unique.length, regularCount: regularCards.length, foilCount: foilCards.length, totalCost: totalCost, priceCount: priceCount, avgPrice: avgPrice, // v1.3.2: 新增统计字段 (借鉴 Trading Card Info 脚本) medianPrice: calcMedian(allPrices), // 中位数价格 (分), 抗极端高价干扰 avgNoMaxPrice: calcAvgNoMax(allPrices), // 忽略最高价后的均价 (分) netIncome: netIncome, // 预计税后总收入 (分), 使用精确手续费计算 regularMedianPrice: calcMedian(regularPrices), regularAvgNoMaxPrice: calcAvgNoMax(regularPrices), foilMedianPrice: calcMedian(foilPrices), foilAvgNoMaxPrice: calcAvgNoMax(foilPrices), regularTotalCost: regularTotalCost, regularAvgPrice: regularPriceCount > 0 ? Math.round(regularTotalCost / regularPriceCount) : 0, foilTotalCost: foilTotalCost, foilAvgPrice: foilPriceCount > 0 ? Math.round(foilTotalCost / foilPriceCount) : 0, updatedAt: Date.now(), }; } // v1.2.14: 抓取单组卡牌(普通或闪卡),参考 steam-store-card-prices.fetchCardGroup async function _fetchCardGroup(appId, foil) { var cards = [], total = Infinity, start = 0, count = 100; while (start < total && cards.length < 300) { var text; try { text = await fmRequest(buildMarketApiUrlEx(appId, foil, start, count)); } catch (e) { console.warn('[SBC-Pro] 市场请求失败 appId=' + appId + ' foil=' + foil + ':', e.message); break; } try { var data = JSON.parse(text); } catch (e) { break; } if (!data || data.success === false) break; var results = Array.isArray(data.results) ? data.results : []; total = Number.isFinite(Number(data.total_count)) ? Number(data.total_count) : results.length; results.forEach(function(item) { var card = _normalizeCard(item, foil); if (card) cards.push(card); }); if (results.length < count) break; start += count; } // 去重 + 排序 var seen = {}, unique = []; cards.forEach(function(c) { if (!seen[c.hashName]) { seen[c.hashName] = true; unique.push(c); } }); unique.sort(function(a, b) { return a.name.localeCompare(b.name, undefined, { numeric: true }); }); return unique; } // v1.2.14-fix8: 从市场返回的价格文本中提取货币符号,设置 primaryCurrency // 市场 API 返回的 sellPriceText 始终用用户钱包货币(如 "₹ 12.80" / "$12.80") function detectCurrencyFromPriceText(priceText) { if (!priceText || primaryCurrencyResolved) return; var text = String(priceText).trim(); if (!text) return; // 优先识别 ¥(区分 CNY/JPY) if (/¥/.test(text) && !/JP¥|JPY/i.test(text)) { primaryCurrency = CURRENCY_MAP[resolveYen(text)]; primaryCurrencyResolved = true; return; } // 按顺序匹配其他货币 for (var j = 0; j < CURRENCIES.length; j++) { if (CURRENCIES[j].id === 'USD') { if (/\$\s*\d/.test(text)) { primaryCurrency = CURRENCIES[j]; primaryCurrencyResolved = true; return; } continue; } if (CURRENCIES[j].match.test(text)) { primaryCurrency = CURRENCIES[j]; primaryCurrencyResolved = true; return; } } } // v1.2.14: 规范化卡牌对象,参考 steam-store-card-prices.normalizeCard function _normalizeCard(item, foil) { var hashName = (item.hash_name || '').trim(); if (!hashName) return null; var desc = item.asset_description || {}; var iconPath = desc.icon_url_large || desc.icon_url || ''; var iconUrl = iconPath ? 'https://community.fastly.steamstatic.com/economy/image/' + iconPath + '/256fx256f' : ''; var sellPrice = Number.isFinite(Number(item.sell_price)) ? Number(item.sell_price) : null; var sellPriceText = (item.sell_price_text || item.sale_price_text || '').trim(); // v1.2.14-fix8: 从市场价格文本检测用户钱包货币(最可靠——市场 API 始终返回钱包货币) detectCurrencyFromPriceText(sellPriceText); var listings = parseInt(item.sell_listings, 10) || 0; var publisherFeePercent = _readPublisherFeeFromItem(item); var netPrice = _calculateSellerReceives(sellPrice, publisherFeePercent); // v1.5.0: 清理卡牌名称 — 移除 "集换式卡牌"/"Trading Card"/"闪亮" 等类型后缀,避免文字换行 var rawName = (item.name || desc.market_name || desc.name || hashName); var name = rawName .replace(/\s*\(Foil\s*Trading\s*Card\)\s*$/i, '') // English: (Foil Trading Card) .replace(/\s*\(Trading\s*Card\)\s*$/i, '') // English: (Trading Card) .replace(/\s*\(Foil\)\s*$/i, '') // English: (Foil) .replace(/\s*\(闪亮的集换式卡牌\)\s*$/, '') // Chinese: (闪亮的集换式卡牌) .replace(/\s*\(集换式卡牌\)\s*$/, '') // Chinese: (集换式卡牌) .replace(/\s*\(闪亮\)\s*$/, '') // Chinese: (闪亮) .replace(/\s*\(闪亮的集换式卡牌.*?\)\s*$/, '') // Chinese: (闪亮的集换式卡牌...) 兜底 .replace(/\s*\(集换式卡牌.*?\)\s*$/, '') // Chinese: (集换式卡牌...) 兜底 .replace(/^\d+-/, '') // hash_name 回退: 移除 appid 前缀 .trim(); return { foil: !!foil, name: name, hashName: hashName, classId: String(desc.classid || '').trim(), type: String(desc.type || '').trim(), iconUrl: iconUrl, sellPrice: sellPrice, sellPriceText: sellPriceText, salePriceText: String(item.sale_price_text || '').trim(), listings: listings, netPrice: netPrice, publisherFeePercent: publisherFeePercent, // 求购价(默认空,按需 enrichBuyOrder 填充) buyPrice: null, buyOrderCount: 0, buyPriceError: false, marketUrl: MARKET_LISTING_PAGE + encodeURIComponent(hashName), }; } // v1.2.14: 从市场搜索结果读取发行商手续费比例,参考 steam-store-card-prices.readPublisherFeeFromItem function _readPublisherFeeFromItem(item) { var desc = item && item.asset_description ? item.asset_description : {}; var ownerActions = desc.owner_actions || []; for (var i = 0; i < ownerActions.length; i++) { var link = ownerActions[i].link || ''; var m = link.match(/publisherFeePercentDbl=([0-9.]+)/); if (m) { var v = Number(m[1]); if (Number.isFinite(v) && v >= 0) return v / 100; } } return CONFIG.DEFAULT_PUBLISHER_FEE_PERCENT; } // v1.2.14: 计算卖家到手价(扣除 Steam 交易手续费 + 发行商手续费) // 参考 steam-store-card-prices.calculateSellerReceives / calculateFeeAmount function _calculateSellerReceives(buyerPaysCents, publisherFeePercent) { var amount = Math.round(Number(buyerPaysCents)); if (!Number.isFinite(amount) || amount <= 0) return null; publisherFeePercent = publisherFeePercent == null ? CONFIG.DEFAULT_PUBLISHER_FEE_PERCENT : publisherFeePercent; // Steam 钱包手续费结构(简化版,与 Steam 官方计算一致) // wallet_fee_base = 0, wallet_fee_percent = 0.05 (5% Steam 交易费) var steamFeePercent = CONFIG.STEAM_TRANSACTION_FEE_PERCENT; var estimatedReceived = parseInt(amount / (steamFeePercent + publisherFeePercent + 1), 10); if (!Number.isFinite(estimatedReceived)) estimatedReceived = amount; estimatedReceived = Math.max(1, estimatedReceived); // 迭代逼近(与 Steam 官方算法一致) var iterations = 0, everUndershot = false; var fees = _calcFees(estimatedReceived, publisherFeePercent, steamFeePercent); while (fees.amount !== amount && iterations < 100) { if (fees.amount > amount) { if (everUndershot) { fees = _calcFees(estimatedReceived - 1, publisherFeePercent, steamFeePercent); fees.steamFee += amount - fees.amount; fees.fees += amount - fees.amount; fees.amount = amount; break; } estimatedReceived--; } else { everUndershot = true; estimatedReceived++; } estimatedReceived = Math.max(1, estimatedReceived); fees = _calcFees(estimatedReceived, publisherFeePercent, steamFeePercent); iterations++; } return amount > fees.fees ? amount - fees.fees : 1; } function _calcFees(receivedAmount, publisherFee, steamFeePercent) { var steamFee = Math.max(Math.floor(receivedAmount * steamFeePercent), 1); var publisherFeeAmount = publisherFee > 0 ? Math.max(Math.floor(receivedAmount * publisherFee), 1) : 0; return { steamFee: steamFee, publisherFee: publisherFeeAmount, fees: steamFee + publisherFeeAmount, amount: receivedAmount + steamFee + publisherFeeAmount, }; } // v1.2.14: 抓取求购价(解析挂单页 HTML),参考 steam-store-card-prices.enrichBuyOrder / parseBuyOrderHtml // v1.3.9: 增加 cc 参数确保挂单页返回用户钱包货币;从 HTML 补充货币检测 async function _enrichBuyOrder(card) { try { // v1.3.9: 附加 cc 参数,确保挂单页以用户钱包国家/地区渲染(印度卢比等非 USD 地区) var fetchUrl = card.marketUrl; if (fetchUrl.indexOf('cc=') === -1) { var cc = detectPrimaryCountryCode(); if (cc) fetchUrl += (fetchUrl.indexOf('?') === -1 ? '?' : '&') + 'cc=' + cc; } var html = await _requestTextWithRetry(fetchUrl, CONFIG.MARKET_BUY_ORDER_RETRY, CONFIG.MARKET_BUY_ORDER_RETRY_DELAY); var normalized = String(html || '').replace(/\\/g, ''); var buyPrice = _readNumber(normalized, /"amtMaxBuyOrder"\s*:\s*(\d+)/); var buyOrderCount = _readNumber(normalized, /"cBuyOrders"\s*:\s*(\d+)/); var publisherFeeMatch = normalized.match(/"publisherFeePct"\s*:\s*([0-9.]+)/); var publisherFeePercent = card.publisherFeePercent; if (publisherFeeMatch) { var v = Number(publisherFeeMatch[1]); if (Number.isFinite(v) && v >= 0) publisherFeePercent = v / 100; } // v1.3.9: 从挂单页 HTML 补充货币检测 // 挂单页包含 g_strCurrencyCode / sell_price_text 等货币信息 _detectCurrencyFromListingHtml(normalized); return Object.assign({}, card, { publisherFeePercent: publisherFeePercent, netPrice: _calculateSellerReceives(card.sellPrice, publisherFeePercent), buyPrice: buyPrice || 0, buyOrderCount: buyOrderCount || 0, buyPriceError: false, }); } catch (e) { return Object.assign({}, card, { buyPrice: null, buyOrderCount: 0, buyPriceError: true, buyPriceErrorMessage: e && e.message ? e.message : '未知错误', }); } } // v1.3.9: 从市场挂单页 HTML 中提取货币信息,补充 detectPrimaryCurrency 检测 // 挂单页 HTML 包含多种货币线索:g_strCurrencyCode、sell_price_text、价格展示文本等 function _detectCurrencyFromListingHtml(normalized) { if (primaryCurrencyResolved) return; // 1) 尝试从 g_strCurrencyCode 提取 var ccMatch = normalized.match(/g_strCurrencyCode\s*=\s*['"]([A-Z]{3})['"]/); if (ccMatch && CURRENCY_MAP[ccMatch[1]]) { primaryCurrency = CURRENCY_MAP[ccMatch[1]]; primaryCurrencyResolved = true; return; } // 2) 尝试从 wallet_currency 数字 ID 提取 var wcMatch = normalized.match(/"wallet_currency"\s*:\s*(\d+)/); if (wcMatch) { try { var win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; if (typeof win.GetCurrencyCode === 'function') { var code = win.GetCurrencyCode(Number(wcMatch[1])); if (code && CURRENCY_MAP[code]) { primaryCurrency = CURRENCY_MAP[code]; primaryCurrencyResolved = true; return; } } } catch (e) {} // 回退:硬编码常见 currency ID → code 映射 var currencyIdMap = {1:'USD',2:'GBP',3:'EUR',4:'CHF',5:'RUB',6:'PLN',7:'BRL',8:'JPY',9:'NOK',10:'IDR',11:'MYR',12:'PHP',13:'SGD',14:'THB',15:'VND',16:'KRW',17:'TRY',18:'UAH',19:'MXN',20:'CAD',21:'AUD',22:'NZD',23:'CNY',24:'INR',25:'CLP',26:'PEN',27:'COP',28:'ZAR',29:'HKD',30:'TWD',31:'SAR',32:'AED',33:'ARS',34:'ILS',35:'KZT',36:'KWD',37:'QAR',38:'CRC',39:'UYU'}; var mappedCode = currencyIdMap[Number(wcMatch[1])]; if (mappedCode && CURRENCY_MAP[mappedCode]) { primaryCurrency = CURRENCY_MAP[mappedCode]; primaryCurrencyResolved = true; return; } } // 3) 尝试从 sell_price_text / sale_price_text 提取货币符号 var sptMatch = normalized.match(/"(?:sell|sale)_price_text"\s*:\s*"([^"]+)"/); if (sptMatch && sptMatch[1]) { detectCurrencyFromPriceText(sptMatch[1]); if (primaryCurrencyResolved) return; } // 4) 尝试从页面价格展示文本提取(如 "Starting at: ₹ 10.00" / "₹ 10.00 起价") var priceDisplayMatch = normalized.match(/(?:Starting at|起价|最低价)\s*[::]?\s*([₹$£€¥¥₩₴₫₱₸฿Rp]+\s*[\d,.]+)/i); if (priceDisplayMatch && priceDisplayMatch[1]) { detectCurrencyFromPriceText(priceDisplayMatch[1]); } } function _readNumber(text, pattern) { var match = String(text || '').match(pattern); if (!match) return null; var value = Number(match[1]); return Number.isFinite(value) ? value : null; } // v1.2.14: 带重试的文本请求(参考 steam-store-card-prices.requestTextWithRetry) async function _requestTextWithRetry(url, retryCount, retryDelay) { var lastError = null; for (var attempt = 0; attempt <= retryCount; attempt++) { try { return await fmRequest(url); } catch (e) { lastError = e; if (attempt >= retryCount) break; if (!/HTTP (?:429|5\d\d)|网络请求失败|请求超时/.test(String(e && e.message || ''))) break; await delay(retryDelay * (attempt + 1)); } } throw lastError || new Error('请求失败'); } // v1.2.14: 为卡牌列表批量抓取求购价(并发控制),参考 steam-store-card-prices.mapLimit // v1.3.1 R2: 使用 concurrentPool 替代手写 worker pool,获得 429 降并发 + 指数退避重试 async function _enrichBuyOrdersBatch(cards, concurrency) { if (!cards || cards.length === 0) return cards; concurrency = concurrency || CONFIG.MARKET_BUY_ORDER_CONCURRENCY; var tasks = cards.map(function(card) { return function() { return _enrichBuyOrder(card); }; }); var results = await concurrentPool(tasks, concurrency); // concurrentPool 返回 { index, status, value/reason } 结构,提取 value return results.map(function(r) { if (r.status === 'fulfilled') return r.value; return null; // 失败的卡牌返回 null,调用方需容错 }); } /** * v1.2.10: 优先级排序 —— 把"可合卡"游戏 / 卡牌缺口最多的游戏排到前面 * 原理: 用户最关心的就是"哪些游戏能马上合卡",以及"哪些游戏最缺卡", * 这两类游戏拿到价格后能立即看到价值评分变化,所以优先请求。 * 评分 = 缺口数 × 3 + (可合卡? 100 : 0) + (已完成? 0 : 10) */ function prioritizeAppIdsForMarket(appIds) { if (!Array.isArray(appIds) || appIds.length === 0) return appIds || []; // v1.5.5: 预建 appId→badge 索引,避免每个 appId 线性扫描 allBadges var badgeByAppId = {}; for (var bi = 0; bi < allBadges.length; bi++) { var bb = allBadges[bi]; if (bb.appId && !badgeByAppId[bb.appId]) badgeByAppId[bb.appId] = bb; } var withScore = appIds.map(function(appId) { var badge = badgeByAppId[appId] || null; var score = 0; if (badge) { // 卡牌缺口数 (cardsRemaining 字段;若不存在则用 maxCards - currentCards 兜底) var remaining = Number.isFinite(badge.cardsRemaining) ? badge.cardsRemaining : Math.max(0, (badge.maxCards || 0) - (badge.currentCards || 0)); score += Math.min(remaining, 20) * 3; // 缺口越多越优先,封顶 60 if (badge.isCraftable) score += 100; // 可合卡 优先 else if (!badge.isCompleted) score += 10; // 未完成加 10 } return { appId: appId, score: score }; }); withScore.sort(function(a, b) { return b.score - a.score; }); return withScore.map(function(x) { return x.appId; }); } /** * v1.2.10: 节流(rAF + 时间窗)合并局部重渲染,避免每个价格响应都触发全量 render * 把卡片网格里"已渲染"且"价格已就位"的徽章批量更新一次。 */ var _renderRafPending = false; var _renderThrottleLastAt = 0; var _pendingPriceAppIds = new Set(); // v1.5.5: 待更新的 appId 集合(增量更新,避免全量扫描) function scheduleIncrementalRender(appId) { // v1.5.5: 记录本次需要更新的 appId;无参数(兼容旧调用)时做全量 if (appId !== undefined && appId !== null) _pendingPriceAppIds.add(String(appId)); if (_renderRafPending) return; _renderRafPending = true; var fire = function() { _renderRafPending = false; var now = Date.now(); if (now - _renderThrottleLastAt < CONFIG.MARKET_RERENDER_THROTTLE_MS) { // 距上次渲染太近,再排一次 setTimeout(scheduleIncrementalRender, CONFIG.MARKET_RERENDER_THROTTLE_MS); _renderRafPending = true; return; } _renderThrottleLastAt = now; var pending = _pendingPriceAppIds; _pendingPriceAppIds = new Set(); try { if (typeof computeValueScores === 'function') computeValueScores(); // 局部更新:重新渲染 stats(估值/已加载数)+ 价值角标(已渲染的卡片元素) var statsEl = document.querySelector('.sbc-stats'); if (statsEl) { var newStats = createStats(); if (newStats) statsEl.replaceWith(newStats); } // v1.5.5: 仅更新有待更新价格的卡片(有 pending 集合时),否则全量 if (pending.size > 0) { updatePriceBadgesIncremental(pending); } else { updatePriceBadgesInPlace(); } } catch (e) { console.warn('[SBC-Pro] scheduleIncrementalRender error:', e); } }; if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fire); else setTimeout(fire, 16); } /** * v1.5.5: 增量更新指定 appId 的价格/价值角标 — O(k) 复杂度,k 为待更新数量 * 通过 data-app-id 直接定位卡片,不再全量扫描 DOM */ function updatePriceBadgesIncremental(appIds) { try { appIds.forEach(function(appId) { var mp = marketPrices[appId]; var cards = document.querySelectorAll('.sbc-card[data-app-id="' + _cssEscape(appId) + '"]'); for (var i = 0; i < cards.length; i++) { _updateCardPriceBadge(cards[i], appId, mp); _updateCardValueBadge(cards[i], appId); } }); } catch (e) { /* 静默 */ } } /** * v1.2.10: 就地更新价格/价值角标(不重建卡片 DOM,避免丢失滚动位置 & 动画) * 角标结构: .sbc-card > .sbc-card-img-wrap > [..., .sbc-card-market-badge] * 仅在价格/价值就位时新增/更新;否则隐藏现有 badge。 */ // v1.5.5: 单卡片价格角标更新(供全量/增量共用) function _updateCardPriceBadge(card, appId, mp) { var priceEl = card.querySelector('.sbc-card-market-badge'); var imgWrap = card.querySelector('.sbc-card-img-wrap') || card; if (mp && mp.totalCost > 0) { if (!priceEl) { priceEl = document.createElement('div'); priceEl.className = 'sbc-card-market-badge'; imgWrap.appendChild(priceEl); } priceEl.style.display = ''; priceEl.textContent = formatPrice(mp.totalCost); // v1.3.2: tooltip 增加中位价和税后收入 (借鉴 Trading Card Info 脚本统计模式) priceEl.title = '总卡价 ' + formatPrice(mp.totalCost) + ' · 均 ' + formatPrice(mp.avgPrice || 0) + (mp.medianPrice > 0 ? ' · 中位 ' + formatPrice(mp.medianPrice) : '') + (mp.netIncome > 0 ? ' · 税后 ' + formatPrice(mp.netIncome) : ''); } else if (priceEl) { // 没有价格数据 → 隐藏(等数据回来再显示) priceEl.style.display = 'none'; } } // v1.5.5: 单卡片价值角标更新(修复原实现只更新价格、不更新价值角标的问题) function _updateCardValueBadge(card, appId) { var vs = valueScores[appId]; var isValuable = vs && vs.score >= 50; var isLocked = card.classList.contains('sbc-card--locked'); var badgeEl = card.querySelector('.sbc-card-value-badge'); if (isValuable && !isLocked) { if (!badgeEl) { badgeEl = document.createElement('div'); badgeEl.className = 'sbc-card-value-badge'; var imgWrap = card.querySelector('.sbc-card-img-wrap') || card; imgWrap.appendChild(badgeEl); } badgeEl.style.display = ''; badgeEl.textContent = '★ ' + vs.score + '分'; card.classList.add('sbc-card--valuable'); } else { if (badgeEl) badgeEl.style.display = 'none'; card.classList.remove('sbc-card--valuable'); } } function updatePriceBadgesInPlace() { try { var cards = document.querySelectorAll('.sbc-card[data-app-id]'); for (var i = 0; i < cards.length; i++) { var card = cards[i]; var appId = card.getAttribute('data-app-id'); if (!appId) continue; _updateCardPriceBadge(card, appId, marketPrices[appId]); _updateCardValueBadge(card, appId); } } catch (e) { /* 静默 */ } } /** * v1.2.10: 断点续传 —— 把"已完成/失败"清单写到缓存, * 下次刷新时跳过这些,避免每次都重头跑。 * v1.5.0: 迁移到 IndexedDB (marketResume 仓库, 24h TTL) */ var MARKET_RESUME_TTL = 24 * 60 * 60 * 1000; // 24h async function readMarketResume() { if (IDB.isReady()) { var data = await IDB.get('marketResume', CONFIG.MARKET_RESUME_KEY); if (data) { if (Date.now() - (data.savedAt || 0) > MARKET_RESUME_TTL) return null; return data; } } // GM 回退 try { var raw = GM_getValue(CONFIG.MARKET_RESUME_KEY, ''); if (!raw) return null; var data2 = JSON.parse(raw); if (!data2 || Date.now() - (data2.savedAt || 0) > MARKET_RESUME_TTL) return null; // 迁移到 IDB if (IDB.isReady()) await IDB.set('marketResume', CONFIG.MARKET_RESUME_KEY, data2, MARKET_RESUME_TTL); return data2; } catch (e) { return null; } } async function writeMarketResume() { var payload = { savedAt: Date.now(), done: Array.from(marketDone), success: marketProgress.successCount, total: marketProgress.total }; if (IDB.isReady()) { await IDB.set('marketResume', CONFIG.MARKET_RESUME_KEY, payload, MARKET_RESUME_TTL); } try { GM_setValue(CONFIG.MARKET_RESUME_KEY, JSON.stringify(payload)); } catch (e) {} } async function clearMarketResume() { if (IDB.isReady()) { await IDB.del('marketResume', CONFIG.MARKET_RESUME_KEY); } try { GM_setValue(CONFIG.MARKET_RESUME_KEY, ''); } catch (e) {} } /** * v1.2.10: 单一 in-flight 包装: 直接调用 _fetchMarketPricesImpl, * 由 loadMarketPrices 负责缓存 _marketLoadPromise。 * 这样调用 fetchMarketPrices(appIds) 多次是安全的(共享同一个 promise)。 */ async function fetchMarketPrices(appIds) { if (!appIds || appIds.length === 0) return; if (_marketLoadPromise) { console.log('[SBC-Pro] 市场价格:已在加载中,挂到现有任务'); return _marketLoadPromise; } return _fetchMarketPricesImpl(appIds); } /** * v1.2.10: 实际执行市场价格抓取(被 fetchMarketPrices 包裹,提供 single-flight 语义) * v1.3.1 R2: 使用 concurrentPool 滑动窗口替代批量循环 + negCache 负缓存 * v1.5.3: SWR(Stale-While-Revalidate)模式 — stale缓存(36-48H)立即使用,后台增量刷新 * 每个游戏缓存独立过期(抖动TTL),避免同一时间大量失效 */ async function _fetchMarketPricesImpl(appIds) { // v1.5.0: 确保 IDB 已初始化 await IDB.init(); marketLoading = true; marketInflight.clear(); marketDone.clear(); marketConsecutiveFails = 0; marketPauseUntil = 0; // v1.2.10: 读取断点续传(本次 session 内已成功过的,跳过) // v1.5.0: 改用 IDB 异步读取 var resume = await readMarketResume(); if (resume && Array.isArray(resume.done)) { resume.done.forEach(function(id) { marketDone.add(String(id)); }); } // v1.5.3: SWR — 记录 stale 缓存(36-48H),立即使用但后台增量刷新 var staleAppIds = []; // 辅助函数: 处理缓存命中(含货币检测 + stale标记) function handleCacheHit(appId, cached) { marketPrices[appId] = cached; marketDone.add(String(appId)); if (!primaryCurrencyResolved && cached.cards) { for (var ci = 0; ci < cached.cards.length; ci++) { if (cached.cards[ci].sellPriceText) { detectCurrencyFromPriceText(cached.cards[ci].sellPriceText); if (primaryCurrencyResolved) break; } } } // v1.5.3: stale 缓存(36-48H)标记为待后台刷新 if (isCacheStale(cached)) { staleAppIds.push(appId); } } // 1. v1.5.0: 批量从 IDB 缓存加载 (优先级最高,减少 GM 同步开销) var toFetch = appIds.slice(); if (IDB.isReady()) { var batchCached = await bulkReadMarketCache(toFetch); toFetch.forEach(function(appId) { var cached = batchCached[getCacheKey(appId)]; if (cached) handleCacheHit(appId, cached); }); } else { // GM 回退: 逐个同步读取 toFetch.forEach(function(appId) { var cached = null; try { var raw = GM_getValue(getCacheKey(appId), null); if (raw) { var data = JSON.parse(raw); if (data.v && data.v >= CONFIG.MARKET_CACHE_VERSION && data.updatedAt && Date.now() - data.updatedAt <= CONFIG.MARKET_CACHE_TTL) { cached = data; } } } catch (e) {} if (cached) handleCacheHit(appId, cached); }); } // 2. 跳过已完成/正在请求 + v1.3.1 R2: negCache 负缓存检查 var needFetch = toFetch.filter(function(appId) { if (marketPrices[appId]) return false; if (marketDone.has(String(appId))) return false; if (marketInflight.has(String(appId))) return false; // v1.3.1 R2: 30 分钟内请求失败的 appId 跳过 if (negCache.has('market_' + appId)) return false; return true; }); // 3. 优先级排序:可合卡 / 缺口大 排前 needFetch = prioritizeAppIdsForMarket(needFetch); marketProgress = { total: needFetch.length, done: 0, successCount: 0, startedAt: Date.now() }; var cacheHitCount = toFetch.length - needFetch.length; var freshCount = cacheHitCount - staleAppIds.length; if (needFetch.length === 0) { marketLoading = false; console.log('[SBC-Pro] 市场价格:全部命中缓存(共 ' + toFetch.length + ' 款,fresh ' + freshCount + ' · stale ' + staleAppIds.length + ' 待后台刷新)'); // v1.5.3: 即使全部命中缓存,也启动 stale 后台增量刷新 if (staleAppIds.length > 0) { refreshStaleMarketPrices(staleAppIds); } return marketPrices; } console.log('[SBC-Pro] 市场价格:需请求 ' + needFetch.length + ' 款(总 ' + toFetch.length + ' 款,缓存命中 ' + cacheHitCount + ' [fresh ' + freshCount + ' · stale ' + staleAppIds.length + '])'); // v1.3.1 R2: 构建 concurrentPool 任务列表 var tasks = needFetch.map(function(appId) { return async function() { marketInflight.add(String(appId)); var result = null; var succeeded = false; try { result = await fetchCardsForApp(appId); if (result && result.cardCount > 0) { marketPrices[appId] = result; writeMarketCache(appId, result); marketProgress.successCount++; succeeded = true; } else { marketPrices[appId] = result || { appId: appId, cards: [], cardCount: 0, totalCost: 0, priceCount: 0, avgPrice: 0, error: 'no cards', updatedAt: Date.now() }; } } catch (e) { marketPrices[appId] = { appId: appId, cards: [], cardCount: 0, totalCost: 0, priceCount: 0, avgPrice: 0, error: e.message, updatedAt: Date.now() }; // v1.3.1 R2: 写入负缓存,30 分钟内不再重试 negCache.set('market_' + appId, { error: e.message, time: Date.now() }); console.warn('[SBC-Pro] 市场请求失败 appId=' + appId + ':', e.message); } marketProgress.done++; marketDone.add(String(appId)); marketInflight.delete(String(appId)); if (succeeded) { marketConsecutiveFails = 0; } else { marketConsecutiveFails++; } // v1.2.10: 成功后立即触发节流后的局部重渲染 // v1.5.5: 带上 appId 做增量更新,避免每次成功都全量扫描 DOM if (succeeded) scheduleIncrementalRender(appId); return result; }; }); // v1.3.1 R2: 使用 concurrentPool 滑动窗口并发抓取 // v1.5.3: 并发数提升到6(48H TTL下请求频率大幅降低,可安全提速) await concurrentPool(tasks, CONFIG.MARKET_REQUEST_CONCURRENCY, function(completed, total) { // 进度日志(每10个或完成时);v1.5.5: 断点续传改为每50个落盘,减少 IDB 写入 if (completed % 10 === 0 || completed === total) { var elapsed = ((Date.now() - marketProgress.startedAt) / 1000).toFixed(1); console.log('[SBC-Pro] 市场价格进度: ' + completed + '/' + total + ' · 成功 ' + marketProgress.successCount + ' · 耗时 ' + elapsed + 's'); if (completed % 50 === 0 || completed === total) writeMarketResume(); } // v1.2.10: 实时刷新进度条(节流,每 5 个) if (completed % 5 === 0 || completed === total) { if (typeof showMarketProgress === 'function') { showMarketProgress({ total: marketProgress.total, done: completed, success: marketProgress.successCount }); } } }); marketLoading = false; // 完成后清理断点 + 关闭进度条 await clearMarketResume(); if (typeof showMarketProgress === 'function') showMarketProgress(null); // v1.5.3: 主请求完成后,后台增量刷新 stale 缓存(静默,低并发,不影响UX) if (staleAppIds.length > 0) { refreshStaleMarketPrices(staleAppIds); } return marketPrices; } /** * v1.5.3: 后台增量刷新 stale 缓存 (Stale-While-Revalidate) * - 低并发(3),静默执行,无进度条 * - 成功后更新缓存并触发局部重渲染 * - 失败时保留旧缓存(stale 仍可用直到 48H 过期) * v1.5.4: 增加 _staleRefreshing 去重,避免详情浮窗和批量加载同时触发同一 appId 的重复刷新 */ var _staleRefreshing = new Set(); function refreshStaleMarketPrices(staleAppIds) { if (!staleAppIds || staleAppIds.length === 0) return; // v1.5.5: 页面不可见(后台标签页)时跳过,把网络与CPU留给前台;缓存48H内仍可用 if (typeof document !== 'undefined' && document.visibilityState === 'hidden') { console.log('[SBC-Pro] 页面隐藏,推迟 ' + staleAppIds.length + ' 款 stale 缓存刷新'); return; } // v1.5.4: 过滤掉正在刷新中的 appId,避免重复请求 var toRefresh = staleAppIds.filter(function(appId) { return !_staleRefreshing.has(String(appId)); }); if (toRefresh.length === 0) return; // 标记为刷新中 toRefresh.forEach(function(appId) { _staleRefreshing.add(String(appId)); }); console.log('[SBC-Pro] 市场价格:后台增量刷新 ' + toRefresh.length + ' 款 stale 缓存(低并发静默)'); var refreshTasks = toRefresh.map(function(appId) { return async function() { try { var result = await fetchCardsForApp(appId); if (result && result.cardCount > 0) { // v1.5.5: 仅当价格确实变化时才写缓存/重渲染,减少无谓 IDB 写入与重排 var prev = marketPrices[appId]; var changed = !prev || prev.totalCost !== result.totalCost || prev.cardCount !== result.cardCount || prev.avgPrice !== result.avgPrice; marketPrices[appId] = result; if (changed) { writeMarketCache(appId, result); scheduleIncrementalRender(appId); } else { // 价格未变:仍更新 updatedAt 延长缓存有效期,避免频繁重抓 prev.updatedAt = result.updatedAt; writeMarketCache(appId, prev); } } } catch (e) { // 静默失败 — stale 缓存仍可用直到 48H 过期 } finally { // v1.5.4: 无论成功失败都释放去重锁 _staleRefreshing.delete(String(appId)); } }; }); // fire-and-forget: 不 await,不阻塞主流程 concurrentPool(refreshTasks, CONFIG.MARKET_REFRESH_CONCURRENCY).catch(function() { console.log('[SBC-Pro] 市场价格:后台刷新已完成(部分失败已忽略)'); }); } function getMarketPriceForApp(appId) { return marketPrices[appId] || null; } function formatPrice(cents) { if (cents === null || cents === undefined) return 'N/A'; // 优先用 Steam 内置 v_currencyformat(格式更地道,含货币符号/小数位/千分位) try { var win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; var walletInfo = win.g_rgWalletInfo || {}; var currencyCode = ''; if (typeof win.GetCurrencyCode === 'function' && walletInfo.wallet_currency != null) { currencyCode = win.GetCurrencyCode(walletInfo.wallet_currency); } if (!currencyCode && win.g_strCurrencyCode) currencyCode = win.g_strCurrencyCode; if (currencyCode && typeof win.v_currencyformat === 'function') { return win.v_currencyformat(Math.round(cents), currencyCode, walletInfo.wallet_country || 'US'); } } catch (e) {} // v1.3.9: 货币未检测时,从已加载的 marketPrices 数据中补充检测 // 徽章页无 g_strCurrencyCode/g_rgWalletInfo 等全局变量, detectPrimaryCurrency() 可能回退到默认 CNY // 遍历 marketPrices 中的 sellPriceText(市场 API 始终返回钱包货币)进行检测 if (!primaryCurrencyResolved) { for (var appId in marketPrices) { var mp = marketPrices[appId]; if (mp && mp.cards) { for (var ci = 0; ci < mp.cards.length; ci++) { if (mp.cards[ci].sellPriceText) { detectCurrencyFromPriceText(mp.cards[ci].sellPriceText); if (primaryCurrencyResolved) break; } } } if (primaryCurrencyResolved) break; } } // v1.3.9: 货币已检测但 v_currencyformat 可用时,用检测到的货币 code 调用 v_currencyformat // (徽章页可能 v_currencyformat 可用但 g_rgWalletInfo 不可用的情况) if (primaryCurrencyResolved && primaryCurrency && primaryCurrency.id) { try { var win2 = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; if (typeof win2.v_currencyformat === 'function') { var cc2 = detectPrimaryCountryCode(); return win2.v_currencyformat(Math.round(cents), primaryCurrency.id, cc2 || 'US'); } } catch (e) {} } // 回退:用主货币 symbol 手动拼接(区分小数点/逗号货币,dc 表示 decimal comma) var cur = detectPrimaryCurrency(); var amount = (cents / 100).toFixed(2); if (cur.dc) amount = amount.replace(/\./, ','); return cur.symbol + amount; } // ============================================================ // 价值分析引擎 (v1.2.2: 重构评分模型) // ------------------------------------------------------------ // 核心修复:升级潜力维度基于「游戏最大等级」而非全局最大等级 // 例:Max15 游戏的 Lv.3 (潜力 0.59) > Max5 游戏的 Lv.3 (潜力 0.47) // 新增维度:性价比(每元 XP),鼓励高 XP 低成本合卡 // 优化曲线:卡牌成本改用半饱和曲线,低价区区分度更高 // ============================================================ function computeValueScores() { var scores = {}; // maxXp 仅用于 xpScore 的归一化(保留原行为以兼容已有数据范围) var maxXp = 0; allBadges.forEach(function(b) { if (b.xp > maxXp) maxXp = b.xp; }); allBadges.forEach(function(badge) { var appId = badge.appId; if (!appId) return; // ── 1. 经验值得分(绝对归一化) ── // 用 maxXp 归一化,避免与升级潜力重复计权 var xpScore = maxXp > 0 ? badge.xp / maxXp : 0; // ── 2. 升级潜力得分(核心修复 v1.2.3) ── // 使用 getMaxBadgeLevel: 游戏普通5级/闪卡1级/活动勋章不定 var maxBadgeLvl = getMaxBadgeLevel(badge); if (!maxBadgeLvl || maxBadgeLvl <= 0) { // 数据库未加载时的启发式回退:至少 5 级(Steam 普通徽章默认上限) // 若当前等级已超过 5(说明该游戏支持更高等级),按 level+2 估算上限 maxBadgeLvl = Math.max(5, badge.level + 2); } // maxBadgeLvl = -1 表示无上限(活动勋章),取当前等级+5作为估算上限 var effectiveMaxLvl = maxBadgeLvl === -1 ? badge.level + 5 : maxBadgeLvl; var remainingLevels = Math.max(0, effectiveMaxLvl - badge.level); var potentialRatio = effectiveMaxLvl > 0 ? remainingLevels / effectiveMaxLvl : 0; var completionRatio = effectiveMaxLvl > 0 ? badge.level / effectiveMaxLvl : 0; var upgradePotentialScore = CONFIG.UPGRADE_POTENTIAL_REMAIN_WEIGHT * potentialRatio + CONFIG.UPGRADE_POTENTIAL_DONE_WEIGHT * completionRatio; // ── 3. 卡牌成本得分(半饱和曲线,低价区高区分度) ── // cardPriceScore = 1 / (1 + avgPrice / HALF_SAT) // avgPrice=0 → 1.0 avgPrice=HALF_SAT → 0.5 avgPrice=大 → 趋近 0 var cardPriceScore = 0; var avgPrice = 0; var marketData = getMarketPriceForApp(appId); if (marketData && marketData.totalCost > 0 && marketData.cardCount > 0) { avgPrice = marketData.totalCost / marketData.cardCount; cardPriceScore = 1 / (1 + avgPrice / CONFIG.CARD_PRICE_HALF_SAT); } // ── 4. 性价比得分(每元 XP,高 XP 低成本优先) ── // costEfficiencyScore = min(1, (xp / avgPrice) / BENCH) // xp=100,avgPrice=20 → 5/5=1.0(满) xp=100,avgPrice=100 → 1/5=0.2 var costEfficiencyScore = 0; if (avgPrice > 0) { costEfficiencyScore = Math.min(1, (badge.xp / avgPrice) / CONFIG.COST_EFFICIENCY_BENCH); } // ── 5. 卡牌收集进度(沉没成本激励) ── var completionBonus = badge.progress / 100; // ── 6. 剩余掉落奖励(即时可行动) ── var dropBonus = Math.min(1, badge.drops / 10); // ── 加权汇总 ── var w = CONFIG.VALUE_SCORE_WEIGHTS; var total = w.upgradePotential * upgradePotentialScore + w.xp * xpScore + w.cardPrice * cardPriceScore + w.costEfficiency * costEfficiencyScore + w.completionBonus * completionBonus + w.dropBonus * dropBonus; scores[appId] = { score: Math.round(total * 100), xpScore: Math.round(xpScore * 100), upgradePotentialScore: Math.round(upgradePotentialScore * 100), cardPriceScore: Math.round(cardPriceScore * 100), costEfficiencyScore: Math.round(costEfficiencyScore * 100), completionBonus: Math.round(completionBonus * 100), dropBonus: Math.round(dropBonus * 100), // 元信息(供排行展示使用) maxLevelForApp: effectiveMaxLvl, remainingLevels: remainingLevels, avgPrice: Math.round(avgPrice), }; }); valueScores = scores; return scores; } function getValuableBadges(limit) { limit = limit || 20; var scored = []; allBadges.forEach(function(badge) { if (!badge.appId) return; var scoreData = valueScores[badge.appId]; if (!scoreData) return; scored.push({ badge: badge, score: scoreData.score, marketData: getMarketPriceForApp(badge.appId), // v1.2.2: 携带评分明细,供排行展示 scoreData: scoreData, }); }); scored.sort(function(a, b) { return b.score - a.score; }); return scored.slice(0, limit); } function getTradeRecommendations() { var recs = []; allBadges.forEach(function(badge) { if (!badge.appId) return; var marketData = getMarketPriceForApp(badge.appId); if (!marketData || !marketData.cards.length) return; var totalPrice = 0, cardsWithPrice = 0; marketData.cards.forEach(function(c) { if (c.sellPrice !== null) { totalPrice += c.sellPrice; cardsWithPrice++; } }); var avgCardPrice = cardsWithPrice > 0 ? totalPrice / cardsWithPrice : 0; var reasons = []; if (avgCardPrice > 200) reasons.push('高卡价'); if (badge.drops > 0) reasons.push('可掉落' + badge.drops + '张'); if (badge.progress >= 50 && badge.progress < 100) reasons.push('接近完成'); if (badge.level >= 3) reasons.push('高等级'); if (avgCardPrice > 0 && avgCardPrice <= 100) reasons.push('低成本合成'); if (reasons.length >= 2) { recs.push({ badge: badge, avgCardPrice: Math.round(avgCardPrice), reasons: reasons, score: reasons.length * 25 + (avgCardPrice > 0 ? Math.min(25, Math.round(25 * (1 - avgCardPrice / 500))) : 0), }); } }); recs.sort(function(a, b) { return b.score - a.score; }); return recs.slice(0, 20); } function getLevelDistribution() { var dist = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; allBadges.forEach(function(b) { var lvl = Math.min(5, Math.max(0, b.level - 1)); dist[lvl] = (dist[lvl] || 0) + 1; }); return dist; } function getXpDistribution() { var dist = { '0-100': 0, '101-200': 0, '201-300': 0, '301-400': 0, '401-500': 0, '500+': 0 }; allBadges.forEach(function(b) { if (b.xp <= 100) dist['0-100']++; else if (b.xp <= 200) dist['101-200']++; else if (b.xp <= 300) dist['201-300']++; else if (b.xp <= 400) dist['301-400']++; else if (b.xp <= 500) dist['401-500']++; else dist['500+']++; }); return dist; } // ============================================================ // 样式注入 // ============================================================ // ============================================================ // 自动合卡模块 // ============================================================ function getCraftableBadges() { // v1.2.14-fix10: 可合卡 = Steam 原生合卡按钮(hasCraftButton) 或 collected>=total // 移除 fix9 错误逻辑(原把所有 level>0 的已合成徽章都计入,导致 150 个全算可合卡) return allBadges.filter(function(badge) { if (!badge.appId) return false; // v1.2.14-fix10: 优先用 Steam 原生合卡按钮判断(最准确) if (badge.hasCraftButton) return true; // 兜底:collected>=total 且 total>0(从进度文本提取的) if (badge.isCompleted && badge.total > 0) return true; return false; }); } // ============================================================ // v1.2.4: 统一游戏汇总 (卡牌+徽章按游戏重新组合) // 将所有与同一游戏相关的徽章(普通合成各等级 + 闪卡合成各等级)合并为单个游戏模型 // ============================================================ function buildGameSummary() { var gameMap = {}; allBadges.forEach(function(b) { // 跳过活动勋章和无 appId 的徽章 if (!b.appId || b.badgeType === 'activity') return; if (!gameMap[b.appId]) { gameMap[b.appId] = { appId: b.appId, gameName: b.gameName, gameImg: b.badgeImg, url: b.url, playtime: b.playtime, // 普通合成数据 normalBadges: [], normalLevel: 0, normalCards: 0, normalTotal: 0, // 闪卡合成数据 foilBadges: [], foilLevel: 0, foilCards: 0, foilTotal: 0, // 综合数据 totalXp: 0, totalDrops: 0, maxCraftable: 0, craftableApps: [], marketData: null, valueScore: 0 }; } var g = gameMap[b.appId]; g.totalXp += b.xp; g.totalDrops += b.drops; if (b.isFoil) { g.foilBadges.push(b); if (b.level > g.foilLevel) g.foilLevel = b.level; g.foilCards += b.collected; g.foilTotal += b.total; } else { g.normalBadges.push(b); if (b.level > g.normalLevel) g.normalLevel = b.level; g.normalCards += b.collected; g.normalTotal += b.total; } }); // 计算可合卡次数和市场数据 // v1.2.14-fix10: 增加基于 Steam 原生合卡按钮(hasCraftButton)的可合卡判断 Object.values(gameMap).forEach(function(g) { var cardCount = getCardCountForApp(g.appId); // 普通可合卡次数: 收集/单次合卡数 向下取整 if (cardCount > 0) { g.normalCraftableTimes = Math.floor(g.normalCards / cardCount); g.cardCountPerCraft = cardCount; g.foilCraftableTimes = Math.floor(g.foilCards / cardCount); } else { g.normalCraftableTimes = 0; g.cardCountPerCraft = 0; g.foilCraftableTimes = 0; } // v1.2.14-fix10: 统计该游戏下有 Steam 原生合卡按钮的徽章数(最准确的可合卡信号) g.craftButtonCount = 0; g.normalBadges.forEach(function(b) { if (b.hasCraftButton) g.craftButtonCount++; }); g.foilBadges.forEach(function(b) { if (b.hasCraftButton) g.craftButtonCount++; }); // maxCraftable 优先用 craftButtonCount(原生按钮),否则用累计卡牌数计算 g.maxCraftable = g.craftButtonCount > 0 ? g.craftButtonCount : (g.normalCraftableTimes + g.foilCraftableTimes); // 状态分类 // 0=locked, 1=progress, 2=ready(可合卡), 3=maxed(已满级普通且闪卡达成) var normalMaxed = g.normalLevel >= CONFIG.GAME_BADGE_MAX_LEVEL; var foilHas = g.foilLevel > 0 || g.foilCards > 0; if (normalMaxed && (!cardDatabase[g.appId] || g.foilCards === 0 || g.foilLevel >= CONFIG.FOIL_BADGE_MAX_LEVEL)) { g.status = 'maxed'; } else if (g.craftButtonCount > 0 || g.maxCraftable > 0) { g.status = 'ready'; } else if (g.normalLevel > 0 || g.foilLevel > 0 || g.normalCards > 0 || g.foilCards > 0) { g.status = 'progress'; } else { g.status = 'locked'; } // 进度百分比 g.normalProgress = g.normalTotal > 0 ? Math.round((g.normalCards / g.normalTotal) * 100) : 0; g.foilProgress = g.foilTotal > 0 ? Math.round((g.foilCards / g.foilTotal) * 100) : 0; g.overallProgress = ((g.normalProgress + g.foilProgress) / 2) || 0; // 市场数据 g.marketData = getMarketPriceForApp(g.appId); // 价值评分(取最高) if (g.normalBadges.length > 0 && valueScores[g.appId]) { g.valueScore = valueScores[g.appId].score || 0; } }); return Object.values(gameMap); } /** * v1.2.4: 统一游戏汇总视图 * 按游戏聚合显示普通+闪卡双轨进度,按状态分类筛选 */ function renderUnifiedGameView() { var gridWrap = document.getElementById('sbc-grid-wrap'); if (!gridWrap) return; LazyImageLoader.clear(); gridWrap.innerHTML = ''; var games = buildGameSummary(); // 搜索过滤 var searchInput = document.getElementById('sbc-search-input'); var searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''; if (searchTerm) { games = games.filter(function(g) { return g.gameName.toLowerCase().indexOf(searchTerm) >= 0; }); } // 状态统计 var stats = { all: games.length, ready: 0, progress: 0, locked: 0, maxed: 0 }; games.forEach(function(g) { stats[g.status] = (stats[g.status] || 0) + 1; }); // 当前分类 tab(默认 all) var currentTab = (document.getElementById('sbc-grid-wrap').dataset.ugvTab) || CONFIG.UNIFIED_VIEW.DEFAULT_TAB; // 顶部 tab 栏 var toolbar = document.createElement('div'); toolbar.className = 'sbc-ugv-toolbar'; var tabs = [ { key: 'all', label: '全部', num: stats.all, icon: '📋' }, { key: 'ready', label: '可合卡', num: stats.ready, icon: '✓' }, { key: 'progress', label: '收集中', num: stats.progress, icon: '◐' }, { key: 'locked', label: '未解锁', num: stats.locked, icon: '🔒' }, { key: 'maxed', label: '已满级', num: stats.maxed, icon: '★' } ]; tabs.forEach(function(tab) { var btn = document.createElement('button'); btn.className = 'sbc-ugv-tab' + (currentTab === tab.key ? ' active' : ''); btn.dataset.tab = tab.key; btn.innerHTML = '' + tab.icon + ' ' + tab.label + '' + tab.num + ''; btn.addEventListener('click', function() { document.getElementById('sbc-grid-wrap').dataset.ugvTab = tab.key; renderUnifiedGameView(); }); toolbar.appendChild(btn); }); gridWrap.appendChild(toolbar); // 分类筛选 var filtered = currentTab === 'all' ? games : games.filter(function(g) { return g.status === currentTab; }); // 排序 var sortBy = (document.getElementById('sbc-sort-select') || {}).value || 'default'; switch (sortBy) { case 'name_asc': filtered.sort(function(a, b) { return a.gameName.localeCompare(b.gameName); }); break; case 'name_desc': filtered.sort(function(b, a) { return b.gameName.localeCompare(a.gameName); }); break; case 'progress_desc': filtered.sort(function(a, b) { return b.overallProgress - a.overallProgress; }); break; case 'progress_asc': filtered.sort(function(a, b) { return a.overallProgress - b.overallProgress; }); break; case 'level_desc': filtered.sort(function(a, b) { return (b.normalLevel + b.foilLevel) - (a.normalLevel + a.foilLevel); }); break; case 'level_asc': filtered.sort(function(a, b) { return (a.normalLevel + a.foilLevel) - (b.normalLevel + b.foilLevel); }); break; case 'xp_desc': filtered.sort(function(a, b) { return b.totalXp - a.totalXp; }); break; case 'xp_asc': filtered.sort(function(a, b) { return a.totalXp - b.totalXp; }); break; case 'value_desc': filtered.sort(function(a, b) { return b.valueScore - a.valueScore; }); break; default: // 默认排序: ready > progress > locked, 然后按卡牌进度 filtered.sort(function(a, b) { var order = { ready: 0, progress: 1, maxed: 2, locked: 3 }; if (order[a.status] !== order[b.status]) return order[a.status] - order[b.status]; return b.overallProgress - a.overallProgress; }); } if (filtered.length === 0) { var empty = document.createElement('div'); empty.className = 'sbc-ugv-empty'; empty.innerHTML = '📋该分类下没有游戏'; gridWrap.appendChild(empty); return; } var grid = document.createElement('div'); grid.className = 'sbc-ugv-grid'; filtered.forEach(function(g, i) { grid.appendChild(createUnifiedGameCard(g, i * 20)); }); gridWrap.appendChild(grid); } /** * v1.2.4: 创建单个游戏汇总卡片 */ function createUnifiedGameCard(g, delay) { var card = document.createElement('div'); card.className = 'sbc-ugv-card sbc-card-enter'; card.style.animationDelay = delay + 'ms'; // 状态标签 var statusTag = ''; if (g.status === 'ready') { statusTag = '⚡ 可合 ' + g.maxCraftable + ' 次'; } else if (g.status === 'locked') { statusTag = '🔒 未解锁'; } else if (g.status === 'maxed') { statusTag = '★ 已满级'; } // 普通合成轨道 var normalTrack = ''; if (g.normalBadges.length > 0) { var nCls = g.normalLevel >= CONFIG.GAME_BADGE_MAX_LEVEL ? 'sbc-ugv-bar-fill--complete' : ''; normalTrack = '' + '' + '普通合成 · Lv.' + g.normalLevel + '/' + CONFIG.GAME_BADGE_MAX_LEVEL + '' + '' + g.normalCards + '/' + g.normalTotal + (g.normalCraftableTimes > 0 ? ' · 合成 ' + g.normalCraftableTimes : '') + '' + '' + '' + ''; } // 闪卡合成轨道 var foilTrack = ''; if (g.foilBadges.length > 0) { var fCls = g.foilLevel >= CONFIG.FOIL_BADGE_MAX_LEVEL ? 'sbc-ugv-bar-fill--complete' : 'sbc-ugv-bar-fill--foil'; foilTrack = '' + '' + '★ 闪卡 · Lv.' + g.foilLevel + '/' + CONFIG.FOIL_BADGE_MAX_LEVEL + '' + '' + g.foilCards + '/' + g.foilTotal + (g.foilCraftableTimes > 0 ? ' · 合成 ' + g.foilCraftableTimes : '') + '' + '' + '' + ''; } // 头部(点击跳转徽章页) // v1.3.5: 优先使用游戏封面(sglv-cover-fallback.lib.js 三级降级),无 appId 时降级到徽章图 var headImgHtml = ''; if (g.appId) { headImgHtml = getCoverImgTag(g.appId, { lazy: true }); } else if (g.gameImg) { headImgHtml = ''; } else { headImgHtml = ''; } var head = '' + '' + headImgHtml + '' + '' + '' + escapeHtml(g.gameName) + '' + '' + (g.cardCountPerCraft > 0 ? '单次合卡 ' + g.cardCountPerCraft + ' 张' : '无卡牌数据') + (g.playtime ? ' · ' + escapeHtml(g.playtime) : '') + '' + '' + (statusTag || '') + ''; // 主体(双轨进度) var body = '' + (normalTrack + foilTrack || '无卡牌数据') + ''; // 底部(统计 + 详情按钮) // v1.2.14-fix2: 改为浮窗大屏展示,不再内联展开 var marketVal = (g.marketData && g.marketData.totalCost) ? formatPrice(g.marketData.totalCost) : 'N/A'; var foot = '' + '' + '' + g.totalXp + ' XP' + (g.totalDrops > 0 ? '掉落 ' + g.totalDrops + '' : '') + (g.marketData ? '估值 ' + marketVal + '' : '') + '' + '' + '徽章/卡牌详情 ' + '' + ''; card.innerHTML = head + body + foot; // v1.2.14: 聚合该游戏所有徽章的已拥有卡牌图(用于"已拥有"标记) var aggregatedCardImgs = []; g.normalBadges.concat(g.foilBadges).forEach(function(b) { if (b.cardImgs && b.cardImgs.length) aggregatedCardImgs = aggregatedCardImgs.concat(b.cardImgs); }); card._aggregatedBadge = { cardImgs: aggregatedCardImgs, appId: g.appId, gameName: g.gameName }; // 事件绑定 var headEl = card.querySelector('.sbc-ugv-card-head'); if (headEl && g.url) headEl.addEventListener('click', function(e) { e.stopPropagation(); window.open(g.url, '_blank'); }); // v1.2.14-fix2: 点击"徽章/卡牌详情"按钮打开大屏浮窗 var popupBtn = card.querySelector('[data-action="open-popup"]'); if (popupBtn) { popupBtn.addEventListener('click', function(e) { e.stopPropagation(); // 构造 gameData 传给浮窗 var gameData = { appId: g.appId, gameName: g.gameName, totalXp: g.totalXp, totalDrops: g.totalDrops, badges: g.normalBadges.concat(g.foilBadges), }; showGameDetailPopup(gameData); }); } return card; } // v1.2.14: 统一游戏汇总 - 卡牌区 HTML(复用详情浮窗的 buildCardSection) function buildUnifiedGameCardsHtml(g, marketData, state, viewMode) { viewMode = viewMode || 'cover'; var cardCount = getCardCountForApp(g.appId); function buildToolbar(hasCards) { var toggle = '' + '封面' + '列表' + ''; var buyOrderBtn = '求购价'; return '' + '合成卡牌' + (hasCards ? ' (' + marketData.cards.length + ' 张)' : (cardCount > 0 ? ' · 单套 ' + cardCount + ' 张' : '')) + '' + (hasCards ? toggle + buyOrderBtn : '') + ''; } if (marketData && marketData.cards && marketData.cards.length > 0) { // 聚合已拥有卡牌图(用于"已拥有"标记) var ownedImgs = []; g.normalBadges.concat(g.foilBadges).forEach(function(b) { if (b.cardImgs && b.cardImgs.length) ownedImgs = ownedImgs.concat(b.cardImgs); }); var syntheticBadge = { cardImgs: ownedImgs, appId: g.appId, gameName: g.gameName }; // v1.2.14-fix8: 不再补充市场缺失的卡牌为单独条目(已拥有卡牌在列表中高亮即可) var regularCards = marketData.regularCards || marketData.cards.filter(function(c) { return !c.foil; }); var foilCards = marketData.foilCards || marketData.cards.filter(function(c) { return c.foil; }); if (regularCards.length === 0 && foilCards.length === 0) regularCards = marketData.cards.slice(); var html = buildToolbar(true); html += ''; if (regularCards.length > 0) { html += buildCardSection('普通卡', regularCards, marketData.regularTotalCost, marketData.regularAvgPrice, viewMode, syntheticBadge); } if (foilCards.length > 0) { html += buildCardSection('闪卡', foilCards, marketData.foilTotalCost, marketData.foilAvgPrice, viewMode, syntheticBadge, true); } html += ''; return html; } if (state && state.loading) { return buildToolbar(false) + '正在读取社区市场卡牌图片与价格...'; } if (state && state.error) { return buildToolbar(false) + '读取失败: ' + escapeHtml(state.error) + '' + '重试'; } // 默认: 提示可加载 return buildToolbar(false) + '点击展开后自动加载社区市场卡牌数据' + (cardCount > 0 ? '(单套 ' + cardCount + ' 张)' : '') + ''; } // v1.2.14: 统一游戏汇总 - 按需加载卡牌市场数据 // v1.2.14-fix: 异步回调时读取 card._ugvCardsViewMode 最新值,避免过期 viewMode function loadUnifiedGameCards(g, card) { var cardsSection = card.querySelector('.sbc-ugv-detail-section--cards'); if (!cardsSection) return; cardsSection.innerHTML = buildUnifiedGameCardsHtml(g, null, { loading: true }, card._ugvCardsViewMode || 'cover'); ensureDetailMarketData(g.appId).then(function(data) { // 卡片可能已被重渲染,重新获取 var sec = card.querySelector('.sbc-ugv-detail-section--cards'); if (!sec) return; var currentViewMode = card._ugvCardsViewMode || 'cover'; if (!data || !data.cards || data.cards.length === 0) { sec.innerHTML = buildUnifiedGameCardsHtml(g, null, { error: '未找到卡牌数据' }, currentViewMode); return; } sec.innerHTML = buildUnifiedGameCardsHtml(g, data, null, currentViewMode); }).catch(function(err) { var sec = card.querySelector('.sbc-ugv-detail-section--cards'); if (!sec) return; var currentViewMode = card._ugvCardsViewMode || 'cover'; var message = err && err.message ? err.message : '网络请求失败'; sec.innerHTML = buildUnifiedGameCardsHtml(g, null, { error: message }, currentViewMode); }); } // v1.2.14: 统一游戏汇总 - 重渲染卡牌区(切换视图时) function rerenderUnifiedGameCards(g, card, viewMode) { var cardsSection = card.querySelector('.sbc-ugv-detail-section--cards'); if (!cardsSection) return; var marketData = getMarketPriceForApp(g.appId); cardsSection.innerHTML = buildUnifiedGameCardsHtml(g, marketData, null, viewMode); } // v1.2.14: 统一游戏汇总 - 加载求购价 function loadUnifiedGameBuyOrders(g, card, viewMode) { var marketData = getMarketPriceForApp(g.appId); if (!marketData || !marketData.cards || marketData.cards.length === 0) return; var btn = card.querySelector('[data-ugv-cards-buyorder]'); if (btn) { btn.textContent = '抓取中...'; btn.disabled = true; } _enrichBuyOrdersBatch(marketData.cards.slice(), CONFIG.MARKET_BUY_ORDER_CONCURRENCY).then(function(enriched) { var byHash = {}; enriched.forEach(function(c) { byHash[c.hashName] = c; }); marketData.cards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.cards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); if (marketData.regularCards) { marketData.regularCards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.regularCards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); } if (marketData.foilCards) { marketData.foilCards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.foilCards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); } writeMarketCache(g.appId, marketData); // v1.2.14-fix: 读取 card._ugvCardsViewMode 最新值,避免过期 viewMode rerenderUnifiedGameCards(g, card, card._ugvCardsViewMode || 'cover'); }).catch(function() { var b = card.querySelector('[data-ugv-cards-buyorder]'); if (b) { b.textContent = '求购价(失败,点击重试)'; b.disabled = false; } }); } function updateCraftableBadges() { craftableBadges = getCraftableBadges(); } // ============================================================ // UI 构建 // ============================================================ function createAllDbButtonInner() { if (cardDbLoading) { return '加载中...'; } var count = cardDbLoaded ? Object.keys(cardDatabase).length : 0; var label = cardDbLoaded ? '全部游戏 (' + count + ')' : '全部游戏'; return ' ' + label; } function createToolbar() { var toolbar = document.createElement('div'); toolbar.className = 'sbc-toolbar'; toolbar.innerHTML = '' + '' + '' + '' + '' + '' + '默认排序' + '按名称 A-Z按名称 Z-A' + '按进度 ↓按进度 ↑' + '按等级 ↓ (v1.2.3)按等级 ↑ (v1.2.3)' + '按经验值 ↓按经验值 ↑' + '按价值 ↓ (Pro)' + '按掉落卡牌 ↓' + '' + '' + '全部徽章' + '活动勋章 (v1.2.3)游戏勋章 (v1.2.3)' + '闪卡徽章 (v1.2.3)' + '进行中已完成' + '可合卡' + '有卡牌掉落' + '未解锁 (Pro)' + '高价值 (Pro)' + '已拥有卡牌游戏 (v1.2.1)' + '未拥有卡牌游戏 (v1.2.1)' + '' + '' + '' + '' + createAllDbButtonInner() + '' + '' + ' 总览' + '' + '' + ' 汇总' + '' + '' + ' 分组' + '' + '' + ' 活动' + '' + '' + '' + ' 分析' + '' + '' + ' 加载价格' + '' + '' + ' 原版' + '' + '' + '' + ' JSON' + '' + '' + ' CSV' + '' + ''; return toolbar; } function createStats() { var total = filteredBadges.length; var completed = filteredBadges.filter(function(b) { return b.isCompleted; }).length; var inProgress = total - completed; var locked = filteredBadges.filter(function(b) { return b.isLocked; }).length; var totalXp = filteredBadges.reduce(function(s, b) { return s + b.xp; }, 0); var totalDrops = filteredBadges.reduce(function(s, b) { return s + b.drops; }, 0); var totalLevel = filteredBadges.reduce(function(s, b) { return s + b.level; }, 0); var pricedAppIds = {}, totalMarketValue = 0, marketPricedCount = 0; filteredBadges.forEach(function(b) { if (!b.appId || pricedAppIds[b.appId]) return; var mp = getMarketPriceForApp(b.appId); if (mp && mp.totalCost > 0) { pricedAppIds[b.appId] = true; totalMarketValue += mp.totalCost; marketPricedCount++; } }); var stats = document.createElement('div'); stats.className = 'sbc-stats'; stats.innerHTML = '共 ' + total + ' 枚' + '活动: ' + activityBadges.length + '' + '游戏: ' + gameBadges.length + '' + '闪卡: ' + allBadges.filter(function(b){return b.isFoil}).length + '' + '已完成: ' + completed + '' + '进行中: ' + inProgress + '' + (locked > 0 ? '未解锁: ' + locked + '' : '') + '经验: ' + totalXp.toLocaleString() + '' + '等级: ' + totalLevel.toLocaleString() + '' + (totalDrops > 0 ? '掉落: ' + totalDrops + '' : '') + (craftableBadges.length > 0 ? '可合卡: ' + craftableBadges.length + '' : '') + (marketPricedCount > 0 ? '估值: ' + formatPrice(totalMarketValue) + '' : '') + (cardDbLoaded ? (function() { var totalCardGames = Object.keys(cardDatabase).length; var ownedCount = Object.keys(ownedAppIds).filter(function(id) { return cardDatabase[id]; }).length; var unownedCount = totalCardGames - ownedCount; var coverage = totalCardGames > 0 ? Math.round((ownedCount / totalCardGames) * 100) : 0; return '卡牌游戏: ' + ownedCount + '/' + totalCardGames + ' (' + coverage + '%)' + (unownedCount > 0 ? '未拥有: ' + unownedCount + '' : ''); })() : ''); // 点击统计数字快速筛选 stats.querySelectorAll('.sbc-stats-filter').forEach(function(el) { el.addEventListener('click', function() { var filterSelect = document.getElementById('sbc-filter-select'); if (!filterSelect) return; filterSelect.value = el.dataset.filter; // v1.2.3: 如果当前在活动视图,切回网格视图以显示筛选结果 if (viewMode === 'activity') { viewMode = 'grid'; var ab = document.getElementById('sbc-mode-activity'); var gb = document.getElementById('sbc-mode-grid'); if (ab) ab.className = 'sbc-btn'; if (gb) gb.className = 'sbc-btn sbc-btn--accent'; } applyFiltersAndSort(); }); }); return stats; } function createCard(badge, delay) { delay = delay || 0; var card = document.createElement('a'); var valueScore = valueScores[badge.appId] || { score: 0 }; var marketData = getMarketPriceForApp(badge.appId); var isValuable = valueScore.score >= 50; var isLocked = badge.isLocked; // v1.2.14-fix10: isCraftable 用 hasCraftButton 或 isCompleted var isCraftable = badge.appId && (badge.hasCraftButton || (badge.isCompleted && badge.total > 0)); card.className = 'sbc-card sbc-card-enter' + (isLocked ? ' sbc-card--locked' : '') + (isValuable && !isLocked ? ' sbc-card--valuable' : '') + (isCraftable ? ' sbc-card--craftable' : '') + (badge.isFoil ? ' sbc-card-foil-border' : ''); card.href = badge.url; card.setAttribute('data-index', badge.index); // v1.2.10: 价格/价值角标的就地更新需要 data-app-id if (badge.appId) card.setAttribute('data-app-id', badge.appId); card.style.animationDelay = delay + 'ms'; var progressColor = badge.isCompleted ? 'var(--sbc-success)' : 'var(--sbc-accent)'; var showBadgeName = badge.badgeName && badge.badgeName !== badge.gameName; // v1.2.3: 勋章类型角标 var typeBadgeHtml = ''; if (badge.badgeType === 'activity') { typeBadgeHtml = '活动'; } else if (badge.isFoil) { typeBadgeHtml = '闪卡'; } // 图片区域(参考 steam-game-library-viewer 设计:渐变背景 + contain 显示 + 底部遮罩 + grid 多卡牌) // v1.3.1: 使用 data-src 懒加载,由 LazyImageLoader.scan() 统一注册 IntersectionObserver var imgContent = ''; if (badge.badgeImg) { imgContent = ''; } else if (badge.cardImgs && badge.cardImgs.length > 0) { // 多卡牌用 grid 布局,更整齐美观(≤2 张 2 列,否则 3 列) var displayCards = badge.cardImgs.slice(0, 6); var cols = displayCards.length <= 2 ? displayCards.length : 3; imgContent = '' + displayCards.map(function(src) { return ''; }).join(''); if (badge.total > badge.cardImgs.length) { imgContent += '+' + (badge.total - badge.cardImgs.length) + ''; } imgContent += ''; } else if (badge.lockedIconImg) { imgContent = ''; } else { imgContent = '' + '' + '未解锁'; } // 价值角标 var valueBadgeHtml = (isValuable && !isLocked) ? '★ ' + valueScore.score + '分' : ''; // 价格角标 var marketBadgeHtml = (marketData && marketData.totalCost > 0) ? '' + formatPrice(marketData.totalCost) + '' : ''; // 合卡角标 var craftBadgeHtml = isCraftable ? '可合卡' : ''; card.innerHTML = '' + imgContent + (badge.level > 1 ? 'Lv.' + badge.level + '' : '') + '' + badge.xp + ' XP' + typeBadgeHtml + valueBadgeHtml + marketBadgeHtml + craftBadgeHtml + '' + '' + '' + '' + escapeHtml(badge.gameName) + '' + '详情' + '' + (showBadgeName ? '' + escapeHtml(badge.badgeName) + '' : '') + (badge.total > 0 ? '' + badge.collected + '/' + badge.total + '' + badge.progress + '%' : '') + '' + (badge.playtime ? '' + escapeHtml(badge.playtime) + '' : '') + (badge.drops > 0 ? '' + badge.drops + ' 张可掉落' : '无掉落') + '' + ''; // v1.2.3: 卡牌详情按钮点击 var detailBtn = card.querySelector('.sbc-card-detail-btn'); if (detailBtn) { detailBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); showCardDetail(badge); }); } return card; } function createPagination(totalItems, totalPages, onNavigate) { if (totalPages <= 1 && onNavigate === renderGrid) return document.createElement('div'); const pagination = document.createElement('div'); pagination.className = 'sbc-pagination'; // v1.3.1: 翻页后平滑滚动到顶部 const navigateAndScroll = function() { onNavigate(); if (onNavigate === renderGrid) { smoothScrollToTop(CONFIG.SCROLL_TO_TOP_OFFSET); } }; const mkBtn = (text, disabled, onClick) => { const btn = document.createElement('button'); btn.className = 'sbc-page-btn' + (text === currentPage ? ' active' : ''); btn.textContent = text; btn.disabled = disabled; btn.addEventListener('click', onClick); return btn; }; pagination.appendChild(mkBtn('<', currentPage === 1, () => { if (currentPage > 1) { currentPage--; navigateAndScroll(); } })); const maxVisible = 7; let sp = Math.max(1, currentPage - Math.floor(maxVisible / 2)); const ep = Math.min(totalPages, sp + maxVisible - 1); if (ep - sp + 1 < maxVisible) sp = Math.max(1, ep - maxVisible + 1); const mkDots = () => { const s = document.createElement('span'); s.className = 'sbc-page-info'; s.textContent = '...'; return s; }; const mkPg = num => mkBtn(num, false, () => { currentPage = num; navigateAndScroll(); }); if (sp > 1) { pagination.appendChild(mkPg(1)); if (sp > 2) pagination.appendChild(mkDots()); } for (let i = sp; i <= ep; i++) pagination.appendChild(mkPg(i)); if (ep < totalPages) { if (ep < totalPages - 1) pagination.appendChild(mkDots()); pagination.appendChild(mkPg(totalPages)); } pagination.appendChild(mkBtn('>', currentPage === totalPages, () => { if (currentPage < totalPages) { currentPage++; navigateAndScroll(); } })); const info = document.createElement('span'); info.className = 'sbc-page-info'; const s = (currentPage - 1) * CONFIG.CARDS_PER_PAGE + 1; info.textContent = `${s}-${Math.min(currentPage * CONFIG.CARDS_PER_PAGE, totalItems)} / ${totalItems}`; pagination.appendChild(info); return pagination; } function renderGrid() { var gridWrap = document.getElementById('sbc-grid-wrap'); if (!gridWrap) return; // v1.3.1: 清理上一视图的懒加载观察 LazyImageLoader.clear(); gridWrap.innerHTML = ''; if (filteredBadges.length === 0) { gridWrap.innerHTML = '未找到匹配的徽章'; return; } // v1.3.1: 使用 DocumentFragment 批量构建 DOM var fragment = document.createDocumentFragment(); var grid = document.createElement('div'); grid.className = 'sbc-grid'; var start = (currentPage - 1) * CONFIG.CARDS_PER_PAGE; var end = Math.min(start + CONFIG.CARDS_PER_PAGE, filteredBadges.length); var batch = filteredBadges.slice(start, end); // v1.3.1: rAF 分批渲染,避免大量卡片一次性渲染阻塞主线程 var batchSize = CONFIG.CARD_RENDER_BATCH_SIZE; var batchIdx = 0; function renderBatch() { var frag = document.createDocumentFragment(); var limit = Math.min(batchIdx + batchSize, batch.length); for (var i = batchIdx; i < limit; i++) { frag.appendChild(createCard(batch[i], (i - batchIdx) * 30)); } grid.appendChild(frag); batchIdx = limit; if (batchIdx < batch.length) { requestAnimationFrame(renderBatch); } else { // 所有卡片渲染完毕,扫描懒加载图片 LazyImageLoader.scan(grid); } } fragment.appendChild(grid); gridWrap.appendChild(fragment); renderBatch(); gridWrap.appendChild(createPagination(filteredBadges.length, Math.ceil(filteredBadges.length / CONFIG.CARDS_PER_PAGE), renderGrid)); } function renderGroupedView() { var gridWrap = document.getElementById('sbc-grid-wrap'); if (!gridWrap) return; LazyImageLoader.clear(); gridWrap.innerHTML = ''; if (filteredBadges.length === 0) { gridWrap.innerHTML = '未找到匹配的徽章'; return; } var gameGroups = {}; filteredBadges.forEach(function(b) { var key = b.gameName; var g = gameGroups[key]; if (!g) { g = { gameName: b.gameName, appId: b.appId, badges: [], totalXp: 0, totalDrops: 0, completedCount: 0, maxLevel: 0, collectedCards: 0, totalCards: 0, ownedCardUrls: [] }; gameGroups[key] = g; } g.badges.push(b); g.totalXp += b.xp; g.totalDrops += b.drops; if (b.isCompleted) g.completedCount++; if (b.level > g.maxLevel) g.maxLevel = b.level; // v1.2.6: 收集已拥有卡牌的 URL(用于逐卡匹配) if (b.cardImgs && b.cardImgs.length > 0) { b.cardImgs.forEach(function(u) { g.ownedCardUrls.push(normalizeCardUrl(u)); }); } // 累加卡牌进度(只统计普通合成) if (!b.isFoil) { g.collectedCards += b.collected; g.totalCards += b.total; } }); var groups = Object.values(gameGroups); // v1.2.6: 再次确保已拥有卡牌 URL 列表已收集(防御性处理) groups.forEach(function(g) { if (!g.ownedCardUrls) g.ownedCardUrls = []; if (g.ownedCardUrls.length === 0) { g.badges.forEach(function(b) { if (b.cardImgs && b.cardImgs.length > 0) { b.cardImgs.forEach(function(u) { g.ownedCardUrls.push(normalizeCardUrl(u)); }); } }); } }); // v1.2.3: 计算完成率 groups.forEach(function(g) { g.completedRate = g.badges.length > 0 ? Math.round((g.completedCount / g.badges.length) * 100) : 0; g.cardsOwned = g.collectedCards; g.cardsTotal = g.totalCards; }); var sortBy = (document.getElementById('sbc-sort-select') || {}).value || 'default'; switch (sortBy) { case 'name_asc': groups.sort(function(a, b) { return a.gameName.localeCompare(b.gameName); }); break; case 'name_desc': groups.sort(function(a, b) { return b.gameName.localeCompare(a.gameName); }); break; case 'progress_desc': groups.sort(function(a, b) { return b.completedRate - a.completedRate; }); break; case 'progress_asc': groups.sort(function(a, b) { return a.completedRate - b.completedRate; }); break; case 'level_desc': groups.sort(function(a, b) { return b.maxLevel - a.maxLevel; }); break; case 'level_asc': groups.sort(function(a, b) { return a.maxLevel - b.maxLevel; }); break; case 'xp_desc': groups.sort(function(a, b) { return b.totalXp - a.totalXp; }); break; case 'xp_asc': groups.sort(function(a, b) { return a.totalXp - b.totalXp; }); break; case 'drops_desc': groups.sort(function(a, b) { return b.totalDrops - a.totalDrops; }); break; default: groups.sort(function(a, b) { return b.badges.length - a.badges.length; }); } groups.forEach(function(group, gi) { var el = document.createElement('div'); el.className = 'sbc-game-group'; el.style.animationDelay = (gi * 50) + 'ms'; var rate = group.completedRate; // v1.2.6: 获取该游戏的所有卡牌(来自市场) var marketData = group.appId ? getMarketPriceForApp(group.appId) : null; var allCards = marketData && marketData.cards ? marketData.cards.slice() : []; var totalCards = allCards.length; var ownedCount = 0; // v1.2.6: 精确逐卡拥有判断(对每个 marketData.cards[i].iconUrl 做归一化 URL 匹配) allCards.forEach(function(c) { if (c.iconUrl && group.ownedCardUrls.indexOf(normalizeCardUrl(c.iconUrl)) >= 0) ownedCount++; }); // 如果徽章页面是 100% 收集(已合成),且卡牌市场数据有,但我们无逐卡图(说明卡牌已被合成为徽章),全部标为已拥有 var isAllSynthesized = (group.completedCount > 0 && group.collectedCards > 0 && group.collectedCards >= group.totalCards); // v1.3.8: isAllSynthesized 仅在无逐卡 URL 数据时生效,有逐卡数据时以 URL 匹配为准 var useSynthFallback = isAllSynthesized && group.ownedCardUrls.length === 0; if (useSynthFallback) ownedCount = totalCards; el.innerHTML = '' + '' + '' + escapeHtml(group.gameName) + '' + '' + group.badges.length + ' 枚徽章' + (totalCards > 0 ? ' · ' + ownedCount + '/' + totalCards + ' 张卡' + (ownedCount === totalCards ? ' ✓ 已集齐' : (ownedCount === 0 ? '' : ' ' + Math.round(ownedCount / totalCards * 100) + '%')) + '' : '') + '' + '' + '' + '已合成: ' + group.completedCount + '/' + group.badges.length + '' + (totalCards > 0 ? '卡牌: ' + ownedCount + '/' + totalCards + '' : '') + '经验值: ' + group.totalXp + '' + (group.totalDrops > 0 ? '掉落: ' + group.totalDrops + '' : '') + '' + // v1.4.0: 浮窗详情按钮(打开统一详情浮窗,含KPI+徽章列表+8列卡牌) '' + '' + '浮窗详情' + '' + '' + '' + '' + // 徽章列表(原版风格) '徽章 点击"详情"展开卡牌封面 · 点击行打开 Steam 徽章页' + '' + // 卡牌集(7 列紧凑) (totalCards > 0 ? '卡牌集 ' + '' + ownedCount + '/' + totalCards + ' 已拥有 · 未拥有灰度显示' + '' + '' : '') + ''; var hdr = el.querySelector('.sbc-game-group-header'); var cnt = el.querySelector('.sbc-game-group-content'); var tgl = el.querySelector('.sbc-game-group-toggle'); var detailBtn = el.querySelector('[data-action="open-group-popup"]'); hdr.addEventListener('click', function(e) { // v1.2.14-fix2: 详情按钮单独处理,不触发展开/折叠 if (detailBtn && detailBtn.contains(e.target)) return; var c = cnt.classList.toggle('collapsed'); tgl.classList.toggle('collapsed', c); }); // v1.2.14-fix2: 详情按钮打开大屏浮窗 if (detailBtn) { detailBtn.addEventListener('click', function(e) { e.stopPropagation(); var gameData = { appId: group.appId, gameName: group.gameName, totalXp: group.totalXp, totalDrops: group.totalDrops, badges: group.badges, }; showGameDetailPopup(gameData); }); } // v1.2.6: 徽章表格(原版风格) var table = el.querySelector('.sbc-badge-table'); group.badges.sort(function(a, b) { if (a.isFoil !== b.isFoil) return a.isFoil ? -1 : 1; return b.level - a.level; }); group.badges.forEach(function(b) { table.appendChild(createBadgeTableRow(b, true)); }); // v1.2.6: 卡牌网格(7列紧凑) var cardsContainer = el.querySelector('.sbc-card-grid--cards'); if (cardsContainer && totalCards > 0) { allCards.forEach(function(card, i) { // v1.3.8: useSynthFallback 仅在无逐卡 URL 数据时生效,有逐卡数据时以 URL 匹配为准 var isOwned = useSynthFallback || (group.ownedCardUrls.indexOf(normalizeCardUrl(card.iconUrl)) >= 0); cardsContainer.appendChild(createGameCardTile(card, isOwned, i * 15)); }); } gridWrap.appendChild(el); }); } /** * v1.2.6: 归一化卡牌图片 URL 用于跨源匹配 * marketData.iconUrl 形如: https://community.fastly.steamstatic.com/economy/image/IKEwfx.../64fx64f * badge.cardImgs 形如: https://community.fastly.steamstatic.com/economy/image/IKEwfx... * 统一去除尾部尺寸后缀 */ function normalizeCardUrl(url) { if (!url) return ''; var s = String(url); // v1.2.14-fix8: 提取 economy/image/ 后的核心 hash 作为匹配 key // 市场 URL: https://community.fastly.steamstatic.com/economy/image/HASH/64fx64f // 徽章页 URL: https://community.cloudflare.steamstatic.com/economy/image/HASH/62fx62f // 两者 hash 相同即可匹配 var match = s.match(/economy\/image\/([a-zA-Z0-9]+)/i); if (match) return match[1].toLowerCase(); // 退化:移除协议/域名/尺寸后缀/查询参数,只保留路径主体 return s.replace(/^https?:\/\//i, '') .replace(/^[a-z0-9.-]+\//i, '') .replace(/^economy\/image\//i, '') .replace(/\/\d+fx\d+f$/i, '') .replace(/\?.*$/, '') .replace(/\/$/, '') .toLowerCase(); } /** * v1.3.4: 升级卡牌图片 URL 到高分辨率 * Steam 经济图片 URL 形如: .../economy/image/HASH/{size} * 原始尺寸多为 62fx62f 或 64fx64f(缩略图),在卡片网格中放大后模糊 * 替换为 256fx256f 获得清晰显示,不影响 normalizeCardUrl 匹配(按 hash 匹配) */ function upgradeCardImageUrl(url) { if (!url) return url; var s = String(url); if (s.indexOf('economy/image/') < 0) return s; if (/\/\d+fx\d+f(\?|$)/i.test(s)) { return s.replace(/\/\d+fx\d+f(\?|$)/i, '/256fx256f$1'); } if (/\?/.test(s)) { return s.replace(/\?/, '/256fx256f?'); } return s + '/256fx256f'; } /** * v1.2.6: 原版风格徽章行(参考 Steam 徽章页结构) * 结构: [徽章图标] [徽章名 + 等级] [经验值] [卡牌进度条] [状态] */ function createBadgeTableRow(badge, useInlineDetail) { var row = document.createElement('a'); row.className = 'sbc-badge-row' + (badge.isFoil ? ' sbc-badge-row--foil' : '') + (badge.isCompleted ? ' sbc-badge-row--completed' : '') + (badge.isLocked ? ' sbc-badge-row--locked' : ''); if (badge.url) { row.href = badge.url; row.target = '_blank'; row.rel = 'noopener'; } // 徽章图标 var badgeImgHtml = badge.badgeImg ? '' : (badge.lockedIconImg ? '' : '' + (badge.isFoil ? '★' : '◐') + ''); // 进度条 var progressBar = ''; if (badge.total > 0) { var progressColor = badge.isCompleted ? 'var(--sbc-success)' : (badge.isFoil ? 'var(--sbc-gold)' : 'var(--sbc-accent)'); var pct = badge.progress; progressBar = '' + '' + badge.collected + '/' + badge.total + '' + '' + '' + pct + '%' + ''; } else { progressBar = '—'; } // 卡牌缩略图(已拥有卡牌) var cardThumbs = ''; if (badge.cardImgs && badge.cardImgs.length > 0) { var imgs = badge.cardImgs.slice(0, 4); cardThumbs = '' + imgs.map(function(src) { return ''; }).join('') + (badge.cardImgs.length > 4 ? '+' + (badge.cardImgs.length - 4) + '' : '') + ''; } // 状态 var statusText = badge.isCompleted ? '已合成' : (badge.isLocked ? '未开始' : '收集中'); var statusClass = badge.isCompleted ? 'sbc-badge-row-status--done' : (badge.isLocked ? 'sbc-badge-row-status--locked' : 'sbc-badge-row-status--progress'); row.innerHTML = '' + badgeImgHtml + '' + '' + '' + '' + escapeHtml(badge.badgeName) + '' + (badge.isFoil ? '★ 闪卡' : '') + '' + (badge.unlockTime ? '' + escapeHtml(badge.unlockTime) + (badge.playtime ? ' · ' + escapeHtml(badge.playtime) : '') + '' : '') + '' + 'Lv.' + badge.level + '' + '' + badge.xp + ' XP' + '' + progressBar + '' + cardThumbs + '' + '' + statusText + '' + '' + '' + '详情' + '' + ''; // v1.2.14-fix2: 详情按钮事件(阻止冒泡,避免触发外层链接跳转) // v1.4.0: 分组视图使用内联卡牌封面视图,其他视图使用浮窗 var detailBtn = row.querySelector('[data-badge-detail]'); if (detailBtn) { detailBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (useInlineDetail) { toggleInlineCardView(badge, row); } else { showCardDetail(badge); } }); } return row; } // v1.4.0: 分组视图徽章行内联卡牌封面视图(替代浮窗) // 点击"详情"按钮在徽章行下方展开/折叠卡牌封面网格 function toggleInlineCardView(badge, row) { // 已展开则折叠 var existing = row.nextElementSibling; if (existing && existing.classList.contains('sbc-badge-row-inline-cards')) { existing.remove(); return; } // 创建内联容器 var container = document.createElement('div'); container.className = 'sbc-badge-row-inline-cards'; container.innerHTML = '正在读取卡牌数据…'; row.parentNode.insertBefore(container, row.nextSibling); // 渲染卡牌封面 function renderInlineCards(data) { if (!data || !data.cards || data.cards.length === 0) { container.innerHTML = '暂无卡牌数据'; return; } var regularCards = data.regularCards || data.cards.filter(function(c) { return !c.foil; }); var foilCards = data.foilCards || data.cards.filter(function(c) { return c.foil; }); if (regularCards.length === 0 && foilCards.length === 0) regularCards = data.cards.slice(); var html = '卡牌封面 (' + data.cards.length + ' 张)' + (data.totalCost > 0 ? ' · 全套 ' + formatPrice(data.totalCost) : '') + (data.avgPrice > 0 ? ' · 均价 ' + formatPrice(data.avgPrice) : '') + ''; html += ''; if (regularCards.length > 0) { regularCards.forEach(function(c) { html += buildCardCoverItem(c, badge); }); } if (foilCards.length > 0) { foilCards.forEach(function(c) { html += buildCardCoverItem(c, badge); }); } html += ''; container.innerHTML = html; } // 优先使用内存/缓存数据,否则按需抓取 var marketData = getMarketPriceForApp(badge.appId); if (marketData && marketData.cards && marketData.cards.length > 0) { renderInlineCards(marketData); // v1.5.4: SWR — stale缓存(36-48H)后台静默刷新 if (isCacheStale(marketData)) { refreshStaleMarketPrices([badge.appId]); } } else { ensureDetailMarketData(badge.appId).then(function(data) { renderInlineCards(data); }).catch(function() { container.innerHTML = '读取失败,请重试'; }); } } /** * v1.2.8: 在分组视图中展示单个卡牌单元(原版风格 + 信息栏) * @param {Object} card - 市场卡牌对象 {name, iconUrl, sellPrice, sellPriceText, type, ...} * @param {boolean} isOwned - 该卡牌是否已拥有(精确匹配) * @param {number} animDelay - 动画延迟 */ function createGameCardTile(card, isOwned, animDelay) { var tile = document.createElement('div'); tile.className = 'sbc-card-tile' + (isOwned ? '' : ' sbc-card-tile--unowned'); tile.style.animationDelay = (animDelay || 0) + 'ms'; tile.title = card.name + (isOwned ? ' (已拥有)' : ' (未拥有)') + (card.sellPriceText ? ' · ' + card.sellPriceText : ''); // 价签(空时显示 —) var hasPrice = card.sellPrice !== null && card.sellPrice !== undefined && card.sellPrice > 0; var priceHtml = '' + '' + (hasPrice ? escapeHtml(card.sellPriceText || formatPrice(card.sellPrice)) : '—') + '' + (card.type ? '' + escapeHtml(card.type) + '' : '') + ''; tile.innerHTML = '' + (card.iconUrl ? '' : '🎴') + (isOwned ? '✓' : '·') + '' + '' + '' + escapeHtml(card.name) + '' + priceHtml + ''; return tile; } function renderCardView() { var container = document.getElementById('sbc-root'); if (!container) return; container.innerHTML = ''; container.appendChild(createStats()); var wrap = document.createElement('div'); wrap.id = 'sbc-grid-wrap'; container.appendChild(wrap); // v1.3.1: 视图过渡动画 wrap.classList.add('sbc-view-enter'); setTimeout(function() { wrap.classList.remove('sbc-view-enter'); }, CONFIG.VIEW_TRANSITION_MS); if (viewMode === 'alldb') renderAllGamesView(); else if (viewMode === 'activity') renderActivityView(); else if (viewMode === 'unified') renderUnifiedGameView(); else if (viewMode === 'grouped') renderGroupedView(); else renderGrid(); } // ============================================================ // v1.2.3: 活动勋章全量对比视图 // ============================================================ function renderActivityView() { var gridWrap = document.getElementById('sbc-grid-wrap'); if (!gridWrap) return; LazyImageLoader.clear(); gridWrap.innerHTML = ''; // 匹配用户已获取的活动勋章 var obtainedMap = {}; activityBadges.forEach(function(b) { var key = b.appId || b.badgeName || b.gameName; obtainedMap[key] = b; // 也尝试用名称匹配 var matched = findActivityBadge(b); if (matched) { obtainedMap[matched.name] = b; if (matched.appId) obtainedMap[matched.appId] = b; } }); var totalActivity = STEAM_ACTIVITY_BADGES.length; var obtainedCount = 0; var retiredCount = 0; var retiredObtained = 0; STEAM_ACTIVITY_BADGES.forEach(function(ab) { var key = ab.appId || ab.name; if (obtainedMap[key] || obtainedMap[ab.name]) { obtainedCount++; if (ab.isRetired) retiredObtained++; } if (ab.isRetired) retiredCount++; }); // 汇总信息 var summary = document.createElement('div'); summary.className = 'sbc-activity-summary'; summary.innerHTML = '' + totalActivity + '全服活动勋章' + '' + obtainedCount + '已获取' + '' + (totalActivity - obtainedCount) + '未获取' + '' + retiredCount + '绝版勋章' + '' + retiredObtained + '/' + retiredCount + '绝版已获取'; gridWrap.appendChild(summary); // 搜索过滤 var searchInput = document.getElementById('sbc-search-input'); var searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''; var filterSelect = document.getElementById('sbc-filter-select'); var filterBy = filterSelect ? filterSelect.value : 'all'; var filtered = STEAM_ACTIVITY_BADGES.filter(function(ab) { if (searchTerm && ab.name.toLowerCase().indexOf(searchTerm) === -1) return false; if (filterBy === 'activity') return true; return true; }); // 按年份倒序排列 filtered.sort(function(a, b) { return (b.year || 0) - (a.year || 0); }); var grid = document.createElement('div'); grid.className = 'sbc-activity-grid'; filtered.forEach(function(ab, i) { var key = ab.appId || ab.name; var userBadge = obtainedMap[key] || obtainedMap[ab.name]; var isObtained = !!userBadge; var card = document.createElement('div'); card.className = 'sbc-activity-card sbc-card-enter' + (isObtained ? ' sbc-activity-card--obtained' : '') + (ab.isRetired ? ' sbc-activity-card--retired' : ''); card.style.animationDelay = (i * 15) + 'ms'; // 类型标签 var typeLabels = { 'sale': '特卖', 'collection': '收藏', 'awards': '颁奖', 'nomination': '提名', 'replay': '回顾', 'community': '社区', 'event': '活动' }; var typeLabel = typeLabels[ab.type] || ab.type; // 等级信息 var levelText = ''; if (userBadge) { levelText = '已达成 Lv.' + userBadge.level; } else { levelText = ab.maxLevel === -1 ? '无上限' : (ab.maxLevel > 0 ? '最高 Lv.' + ab.maxLevel : '未知'); } // 图标:优先用户已有图标,其次 SteamCDN 异步加载,兜底占位 SVG var iconHtml = ''; if (userBadge && userBadge.badgeImg) { iconHtml = ''; } else if (userBadge && userBadge.lockedIconImg) { iconHtml = ''; } else { // v1.2.12: 未获取的勋章显示分类占位 SVG iconHtml = getActivityPlaceholderIcon(ab.type); } card.innerHTML = '' + '' + iconHtml + '' + '' + '' + escapeHtml(ab.name) + '' + '' + (ab.year > 0 ? ab.year + ' 年' : '通用') + (ab.hasFoil ? ' · 有闪卡' : '') + '' + '' + '' + '' + '' + typeLabel + '' + (isObtained ? '已获取' : '未获取') + '' + '' + levelText + ''; // v1.2.12: 异步从 SteamCDN 加载真实图标(仅对未使用用户已有图标的卡片) if (ab.appId && !(userBadge && (userBadge.badgeImg || userBadge.lockedIconImg))) { loadActivityIconAsync(card, ab); } grid.appendChild(card); }); gridWrap.appendChild(grid); } // ============================================================ // v1.4.0: 统一详情浮窗(合并 showCardDetail + showGameDetailPopup) // 单徽章模式: showCardDetail(badge) // 游戏分组模式: showCardDetail(null, gameData) // ============================================================ function showCardDetail(badge, gameData) { var existing = document.getElementById('sbc-detail-overlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.className = 'sbc-detail-overlay'; overlay.id = 'sbc-detail-overlay'; overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.classList.remove('active'); }); var panel = document.createElement('div'); panel.className = 'sbc-detail-panel'; panel.addEventListener('click', function(e) { e.stopPropagation(); }); // v1.4.0: 统一模式判断 var isGroupMode = !!gameData; var appId, gameName, badges = [], groupInfo = null; if (isGroupMode) { appId = gameData.appId; gameName = gameData.gameName; badges = gameData.badges || []; panel.setAttribute('data-app-id', appId || ''); groupInfo = { badgeCount: badges.length, completedCount: badges.filter(function(b) { return b.isCompleted; }).length, totalXp: gameData.totalXp || 0, totalDrops: gameData.totalDrops || 0, }; } else { appId = badge.appId; gameName = badge.gameName; panel.setAttribute('data-app-id', appId || ''); // v1.4.0: 单徽章模式下查找同游戏的其他徽章 if (typeof allBadges !== 'undefined' && appId) { badges = allBadges.filter(function(b) { return b.appId === appId; }); } } // 聚合已拥有卡牌图 var ownedImgs = []; if (isGroupMode) { badges.forEach(function(b) { if (b.cardImgs && b.cardImgs.length) ownedImgs = ownedImgs.concat(b.cardImgs); }); } else if (badge.cardImgs && badge.cardImgs.length) { ownedImgs = badge.cardImgs.slice(); } // 创建 syntheticBadge (统一数据结构用于卡牌渲染) var syntheticBadge = isGroupMode ? { appId: appId, gameName: gameName, cardImgs: ownedImgs, badgeImg: badges[0] ? badges[0].badgeImg : '', badgeName: '', isLocked: false, level: 0, xp: 0, collected: 0, total: 0, url: badges[0] ? badges[0].url : '', badgeType: badges[0] ? badges[0].badgeType : 'game', isFoil: false, } : badge; var detailViewMode = 'cover'; var buyOrderState = { loading: false, loaded: false }; // 获取市场数据 var marketData = getMarketPriceForApp(appId); var needFetch = shouldFetchDetailMarket(syntheticBadge, marketData); // v1.5.4: SWR — 数据存在但stale(36-48H)时,立即展示旧数据并后台静默刷新 if (!needFetch && marketData && isCacheStale(marketData)) { refreshStaleMarketPrices([appId]); } function rerenderCards(state) { var wrap = panel.querySelector('.sbc-detail-cards-wrap'); if (!wrap) return; var currentData = getMarketPriceForApp(appId) || marketData; wrap.innerHTML = buildDetailCardsHtml(syntheticBadge, currentData, state || null, detailViewMode); } // 头部副标题 var subtitle = ''; if (isGroupMode) { subtitle = badges.length + ' 枚徽章'; if (appId) subtitle += ' · AppID: ' + appId; if (gameData.totalXp) subtitle += ' · ' + gameData.totalXp + ' XP'; if (gameData.totalDrops) subtitle += ' · 掉落 ' + gameData.totalDrops; } else { subtitle = badge.badgeName && badge.badgeName !== gameName ? badge.badgeName : ''; if (appId) subtitle += (subtitle ? ' · ' : '') + 'AppID: ' + appId; } var regionBadge = buildRegionBadge(); // 标题栏图片(三级降级) var detailHeaderImg = ''; if (appId) { detailHeaderImg = getCoverImgTag(appId, { lazy: false }); } else if (syntheticBadge.badgeImg) { detailHeaderImg = ''; } else { detailHeaderImg = ''; } // v1.4.0: 构建徽章列表 HTML (有多枚徽章时显示) // v1.5.2: 单枚徽章也显示(已合成徽章需展示等级/XP/合成时间等信息) function buildBadgesListHtml() { if (!badges || badges.length === 0) return ''; var sorted = badges.slice().sort(function(a, b) { if (a.isFoil !== b.isFoil) return a.isFoil ? -1 : 1; return b.level - a.level; }); var html = '' + '徽章列表 (' + sorted.length + ')' + ''; html += ''; sorted.forEach(function(b) { // v1.5.2: 已合成徽章显示"已合成"状态标签 var synthTag = b.isCompleted ? '已合成' : ''; var metaParts = []; metaParts.push(b.isLocked ? '未解锁' : 'Lv.' + b.level); metaParts.push(b.xp + ' XP'); if (b.isFoil) metaParts.push('闪卡'); if (b.unlockTime) metaParts.push(b.unlockTime); html += '' + '' + (b.badgeImg ? '' : '') + '' + '' + '' + '' + escapeHtml(b.badgeName || b.gameName) + '' + synthTag + '' + '' + escapeHtml(metaParts.join(' · ')) + '' + '' + (badges.length > 1 ? '详情' : '') + ''; }); html += ''; return html; } // v1.4.0: 统一浮窗 body = KPI信息网格 + 徽章列表(可选) + 卡牌列表 var badgesListHtml = buildBadgesListHtml(); panel.innerHTML = '' + detailHeaderImg + '' + escapeHtml(gameName) + regionBadge + '' + (subtitle ? '' + escapeHtml(subtitle) + '' : '') + '' + buildDetailActionsHtml(syntheticBadge, marketData) + '✕' + '' + '' + '' + buildDetailInfoItems(syntheticBadge, marketData, groupInfo) + '' + (badgesListHtml ? '' + badgesListHtml + '' : '') + '' + '' + buildDetailCardsHtml(syntheticBadge, marketData, needFetch ? { loading: true } : null, detailViewMode) + '' + '' + ''; overlay.appendChild(panel); document.body.appendChild(overlay); overlay.classList.add('active'); var closeBtn = panel.querySelector('#sbc-detail-close'); if (closeBtn) closeBtn.addEventListener('click', function() { overlay.classList.remove('active'); }); // v1.4.0: 统一事件委托(失败重试 / 封面列表切换 / 求购价加载 / 徽章详情) panel.addEventListener('click', function(e) { var target = e.target && e.target.closest ? e.target.closest('[data-detail-retry], [data-view], [data-detail-buyorder], [data-gdp-badge-detail]') : null; if (!target) return; // 徽章详情(打开单徽章浮窗) if (target.hasAttribute('data-gdp-badge-detail')) { var idx = parseInt(target.getAttribute('data-idx')); var b = (typeof allBadges !== 'undefined') ? allBadges.find(function(x) { return x.index === idx; }) : null; if (b) { overlay.classList.remove('active'); setTimeout(function() { showCardDetail(b); }, 150); } return; } // 失败重试 if (target.hasAttribute('data-detail-retry')) { rerenderCards({ loading: true }); loadDetailMarketData(syntheticBadge, panel, overlay, function() { return detailViewMode; }, groupInfo); return; } // 封面/列表切换 var viewBtn = target.hasAttribute('data-view') ? target : (target.closest('[data-view]') || null); if (viewBtn && viewBtn.getAttribute('data-view')) { detailViewMode = viewBtn.getAttribute('data-view'); rerenderCards(null); return; } // 求购价加载 if (target.hasAttribute('data-detail-buyorder')) { if (buyOrderState.loading) return; loadDetailBuyOrders(syntheticBadge, panel, detailViewMode, buyOrderState, rerenderCards); return; } }); document.addEventListener('keydown', function escD(e) { if (e.key === 'Escape') { overlay.classList.remove('active'); document.removeEventListener('keydown', escD); } }); // 按需抓取市场数据 if (needFetch) loadDetailMarketData(syntheticBadge, panel, overlay, function() { return detailViewMode; }, groupInfo); } // v1.2.14: 为详情浮窗加载求购价(按需触发,避免预抓取限流) function loadDetailBuyOrders(badge, panel, viewMode, buyOrderState, rerenderCards) { var marketData = getMarketPriceForApp(badge.appId); if (!marketData || !marketData.cards || marketData.cards.length === 0) { rerenderCards(null); return; } buyOrderState.loading = true; // 立即显示加载中提示(在工具栏按钮上) var btn = panel.querySelector('[data-detail-buyorder]'); if (btn) { btn.textContent = '抓取中...'; btn.disabled = true; } _enrichBuyOrdersBatch(marketData.cards.slice(), CONFIG.MARKET_BUY_ORDER_CONCURRENCY).then(function(enriched) { // 回写到 marketData(同步内存 + GM 缓存) var byHash = {}; enriched.forEach(function(c) { byHash[c.hashName] = c; }); marketData.cards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.cards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); // 同步分组 if (marketData.regularCards) { marketData.regularCards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.regularCards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); } if (marketData.foilCards) { marketData.foilCards.forEach(function(c, i) { var e = byHash[c.hashName]; if (e) marketData.foilCards[i] = Object.assign({}, c, { buyPrice: e.buyPrice, buyOrderCount: e.buyOrderCount, buyPriceError: e.buyPriceError, netPrice: e.netPrice, publisherFeePercent: e.publisherFeePercent, }); }); } writeMarketCache(badge.appId, marketData); buyOrderState.loading = false; buyOrderState.loaded = true; rerenderCards(null); }).catch(function() { buyOrderState.loading = false; var b = panel.querySelector('[data-detail-buyorder]'); if (b) { b.textContent = '求购价(失败,点击重试)'; b.disabled = false; } }); } // v1.2.14-fix7: 构建地区旗帜+货币符号徽章(详情浮窗标题右侧) // v1.2.14-fix8: 改用 SVG 国旗(参考 Steam-Spending-History-Classifier) function buildRegionBadge() { var cur = detectPrimaryCurrency(); var cc = detectPrimaryCountryCode(); var flagSvg = (cur && cur.id && CURRENCY_FLAGS[cur.id]) ? CURRENCY_FLAGS[cur.id] : ''; var currencyText = cur ? (cur.symbol + ' ' + cur.id) : ''; var title = cc ? ('钱包地区: ' + cc + (cur ? ' · 货币: ' + cur.id + ' (' + cur.label + ')' : '')) : (cur ? ('货币: ' + cur.id) : ''); if (!flagSvg && !currencyText) return ''; return '' + (flagSvg ? '' + flagSvg + '' : '') + (currencyText ? '' + escapeHtml(currencyText) + '' : '') + ''; } // ============================================================ // v1.4.0: showGameDetailPopup 已合并到 showCardDetail (统一浮窗) // 调用 showCardDetail(null, gameData) 进入游戏分组模式 // ============================================================ function showGameDetailPopup(gameData) { showCardDetail(null, gameData); } // v1.2.14: 是否需要为详情浮窗按需抓取市场数据(有 appId 且本地无有效卡牌数据) function shouldFetchDetailMarket(badge, marketData) { if (!badge || !badge.appId) return false; return !(marketData && marketData.cards && marketData.cards.length > 0); } // v1.2.14: 信息网格(抓取成功后可重渲染,补齐"全套卡牌均价") // v1.4.0: 支持游戏分组模式(groupInfo 参数),展示聚合 KPI function buildDetailInfoItems(badge, marketData, groupInfo) { // v1.4.0: 游戏分组模式 - 展示聚合数据 if (groupInfo) { var gInfo = ''; gInfo += '徽章' + groupInfo.badgeCount + ' 枚'; gInfo += '已合成' + groupInfo.completedCount + '/' + groupInfo.badgeCount + ''; if (groupInfo.totalXp > 0) { gInfo += '总经验值' + groupInfo.totalXp + ' XP'; } if (groupInfo.totalDrops > 0) { gInfo += '待掉落' + groupInfo.totalDrops + ' 张'; } if (marketData && marketData.totalCost > 0) { gInfo += '全套均价' + formatPrice(marketData.avgPrice || 0) + ''; if (marketData.medianPrice > 0) { gInfo += '中位价' + formatPrice(marketData.medianPrice) + ''; } if (marketData.netIncome > 0) { gInfo += '预计税后收入' + formatPrice(marketData.netIncome) + ''; } } return gInfo; } var cardCount = getCardCountForApp(badge.appId); var maxBadgeLvl = getMaxBadgeLevel(badge); // v1.2.14-fix6: 精简为 5 个核心 KPI(合并相关项,减少垂直占用) var typeDetailMap = { 'game': '游戏勋章', 'foil': '游戏闪卡勋章', 'event': '季节/活动勋章', 'community': '社区勋章', 'activity': '活动/社区勋章' }; var infoItems = ''; // 1. 勋章类型 + 合成方式(合并) var typeText = typeDetailMap[badge.badgeType] || '游戏勋章'; if (badge.badgeType === 'game') { typeText += ' · ' + (badge.isFoil ? '闪卡' : '普通'); } infoItems += '勋章类型' + typeText + ''; // 2. 当前等级 + 最高等级(合并) var levelText = badge.isLocked ? '未解锁' : 'Lv.' + badge.level; if (!badge.isLocked && maxBadgeLvl > 0) { levelText += ' / ' + (maxBadgeLvl === -1 ? '∞' : 'Lv.' + maxBadgeLvl); } infoItems += '等级' + levelText + ''; // 3. 经验值 if (!badge.isLocked) { infoItems += '经验值' + badge.xp + ' XP'; } else { infoItems += '状态未拥有'; } // 4. 收集进度 + 单次合卡所需(合并) if (!badge.isLocked && badge.total > 0) { var progressText = badge.collected + '/' + badge.total; if (cardCount > 0) progressText += ' · 需 ' + cardCount + ' 张'; infoItems += '收集进度' + progressText + ''; } else if (cardCount > 0) { infoItems += '合卡所需' + cardCount + ' 张'; } // 5. 全套卡牌均价 + 中位价 (v1.3.2: 借鉴 Trading Card Info 脚本增加中位数统计) if (marketData && marketData.totalCost > 0) { infoItems += '全套均价' + formatPrice(marketData.avgPrice || 0) + ''; // v1.3.2: 中位价 (抗极端高价干扰) if (marketData.medianPrice > 0) { infoItems += '中位价' + formatPrice(marketData.medianPrice) + ''; } // v1.3.2: 忽略最高价均价 if (marketData.avgNoMaxPrice > 0) { infoItems += '均价(去最高)' + formatPrice(marketData.avgNoMaxPrice) + ''; } // v1.3.2: 预计税后总收入 (使用精确手续费计算) if (marketData.netIncome > 0) { infoItems += '预计税后收入' + formatPrice(marketData.netIncome) + ''; } } return infoItems; } // v1.2.14: 卡牌区渲染(state: null | {loading:true} | {error:'msg'}) // viewMode: 'cover' | 'list' (默认 cover) // v1.2.14-fix: 只渲染当前激活视图(避免 hidden CSS 不生效),fallback 路径也支持双视图 function buildDetailCardsHtml(badge, marketData, state, viewMode) { viewMode = viewMode || 'cover'; // 工具栏(封面/列表切换 + 求购价加载按钮) function buildToolbar(cardCount, hasFoil) { var toggle = '' + '封面' + '列表' + ''; var buyOrderBtn = '求购价'; return '' + '卡牌列表' + (cardCount > 0 ? ' (' + cardCount + ' 张)' : '') + '' + (cardCount > 0 ? toggle + buyOrderBtn : '') + ''; } // v1.2.14-fix8: 不再补充市场缺失的卡牌为单独条目 // 原逻辑: badge.cardImgs 中市场没有的卡牌会作为 "已拥有卡牌 #N" 单独列出 // 新逻辑: 已拥有卡牌通过 URL 匹配在市场卡牌列表中高亮即可 // URL 匹配由 normalizeCardUrl 提取 economy/image/ 后的 hash 实现,域名/尺寸差异不影响 if (marketData && marketData.cards && marketData.cards.length > 0) { var regularCards = marketData.regularCards || marketData.cards.filter(function(c) { return !c.foil; }); var foilCards = marketData.foilCards || marketData.cards.filter(function(c) { return c.foil; }); // 若无 foil 分组数据(旧缓存),则全部视为普通卡 if (regularCards.length === 0 && foilCards.length === 0) regularCards = marketData.cards.slice(); var html = buildToolbar(marketData.cards.length, foilCards.length > 0); html += ''; if (regularCards.length > 0) { html += buildCardSection('普通卡', regularCards, marketData.regularTotalCost, marketData.regularAvgPrice, viewMode, badge); } if (foilCards.length > 0) { html += buildCardSection('闪卡', foilCards, marketData.foilTotalCost, marketData.foilAvgPrice, viewMode, badge, true); } html += ''; return html; } if (state && state.loading) { return buildToolbar(0, false) + '正在读取社区市场卡牌图片与价格...'; } // v1.2.14-fix4: 市场数据为空(无在售卡牌)——显示提示+重试+已拥有卡牌 if (state && state.empty) { var emptyHtml = buildToolbar(0, false); emptyHtml += '' + '市场暂无在售卡牌数据' + '重试' + ''; // 仍然展示已拥有卡牌(若有) if (badge.cardImgs && badge.cardImgs.length > 0) { emptyHtml += ''; emptyHtml += ''; emptyHtml += '已拥有卡牌 ' + badge.cardImgs.length + ' 张'; if (viewMode === 'list') { emptyHtml += ''; badge.cardImgs.forEach(function(src) { var syntheticCard = { foil: false, name: '', iconUrl: src, sellPrice: null, netPrice: null, buyPrice: null, listings: 0, marketUrl: '' }; emptyHtml += buildCardListItem(syntheticCard, badge); }); emptyHtml += ''; } else { emptyHtml += ''; badge.cardImgs.forEach(function(src) { var syntheticCard = { foil: false, iconUrl: src, sellPrice: null, netPrice: null, buyPrice: null, listings: 0, marketUrl: '' }; emptyHtml += buildCardCoverItem(syntheticCard, badge); }); emptyHtml += ''; } emptyHtml += ''; } return emptyHtml; } if (state && state.error) { return buildToolbar(0, false) + '读取失败: ' + escapeHtml(state.error) + '' + '重试'; } // v1.2.14-fix: fallback 路径也支持封面/列表双视图切换 if (badge.cardImgs && badge.cardImgs.length > 0) { var owned = buildToolbar(badge.cardImgs.length, false); owned += ''; owned += ''; owned += '已拥有卡牌 ' + badge.cardImgs.length + ' 张'; // 只渲染当前激活的视图 if (viewMode === 'list') { owned += ''; badge.cardImgs.forEach(function(src) { var syntheticCard = { foil: false, name: '', iconUrl: src, sellPrice: null, netPrice: null, buyPrice: null, listings: 0, marketUrl: '' }; owned += buildCardListItem(syntheticCard, badge); }); owned += ''; } else { owned += ''; badge.cardImgs.forEach(function(src) { var syntheticCard = { foil: false, iconUrl: src, sellPrice: null, netPrice: null, buyPrice: null, listings: 0, marketUrl: '' }; owned += buildCardCoverItem(syntheticCard, badge); }); owned += ''; } owned += ''; return owned; } return '暂无卡牌数据,请先加载市场价格'; } // v1.2.14: 渲染单个卡牌分组(普通/闪卡) // v1.2.14-fix: 只渲染当前激活视图,避免 hidden CSS 不生效 function buildCardSection(label, cards, totalCost, avgPrice, viewMode, badge, isFoil) { var headerInfo = cards.length + ' 张'; if (totalCost > 0) headerInfo += ' · 全套 ' + formatPrice(totalCost); if (avgPrice > 0) headerInfo += ' · 均价 ' + formatPrice(avgPrice); // v1.3.2: 追加中位价信息 (借鉴 Trading Card Info 脚本统计模式) if (badge && badge.appId) { var md = getMarketPriceForApp(badge.appId); if (md) { var med = isFoil ? md.foilMedianPrice : md.regularMedianPrice; if (med > 0) headerInfo += ' · 中位 ' + formatPrice(med); } } var html = '' + '' + escapeHtml(label) + ' ' + escapeHtml(headerInfo) + ''; // v1.2.14-fix: 只渲染当前激活的视图(不再同时渲染两个视图用 hidden 切换) if (viewMode === 'list') { html += ''; cards.forEach(function(c) { html += buildCardListItem(c, badge); }); html += ''; } else { html += ''; cards.forEach(function(c) { html += buildCardCoverItem(c, badge); }); html += ''; } html += ''; return html; } // v1.2.14: 封面视图单个卡牌 // v1.2.14-fix: 未拥有卡牌添加 --unowned 类用于灰色遮罩 // v1.3.8: 修复默认 isOwned=true 导致无拥有数据时全部标记为已拥有,改为默认 false function buildCardCoverItem(c, badge) { var isOwned = false; if (badge && badge.cardImgs && badge.cardImgs.length > 0 && c.iconUrl) { isOwned = badge.cardImgs.some(function(src) { return normalizeCardUrl(src) === normalizeCardUrl(c.iconUrl); }); } var ownedMark = isOwned ? '✓' : ''; // v1.5.0: 闪卡用左上角金色图标标记,替代名称中的"闪亮"文字 var foilBadge = c.foil ? '★' : ''; var item = '' + (c.iconUrl ? '' : '🎴') + ownedMark + foilBadge + '' + escapeHtml(c.name || '已拥有卡牌') + '' + buildCardPriceBlock(c, 'cover') + ''; return c.marketUrl ? '' + item + '' : item; } // v1.2.14: 列表视图单个卡牌(参考 steam-store-card-prices.renderCardRow) // v1.2.14-fix: 未拥有卡牌添加 --unowned 类用于灰色遮罩 // v1.3.8: 修复默认 isOwned=true 导致无拥有数据时全部标记为已拥有,改为默认 false function buildCardListItem(c, badge) { var isOwned = false; if (badge && badge.cardImgs && badge.cardImgs.length > 0 && c.iconUrl) { isOwned = badge.cardImgs.some(function(src) { return normalizeCardUrl(src) === normalizeCardUrl(c.iconUrl); }); } var ownedMark = isOwned ? '✓' : '·'; var icon = c.iconUrl ? '' : '🎴'; var nameHtml = '' + escapeHtml(c.name || '已拥有卡牌') + (c.foil ? ' 闪' : '') + ''; return '' + ownedMark + icon + '' + nameHtml + '' + buildCardPriceBlock(c, 'list') + ''; } // v1.2.14: 卡牌价格块(出售价/到手价/求购价) function buildCardPriceBlock(c, mode) { var hasSell = c.sellPrice !== null && c.sellPrice !== undefined && c.sellPrice > 0; var sellText = hasSell ? (c.sellPriceText || formatPrice(c.sellPrice)) : '—'; var netText = (hasSell && c.netPrice !== null && c.netPrice > 0) ? formatPrice(c.netPrice) : ''; var buyText = ''; var buyMeta = ''; if (c.buyPriceError) { buyText = '求购失败'; } else if (c.buyPrice !== null && c.buyPrice > 0) { buyText = '求 ' + formatPrice(c.buyPrice) + ''; if (c.buyOrderCount > 0) buyMeta = '' + c.buyOrderCount + ' 求购'; } else if (c.buyPrice === 0) { buyText = '无求购'; } else { buyText = '求购—'; } if (mode === 'list') { return '' + '卖 ' + escapeHtml(sellText) + '' + (netText ? '到 ' + escapeHtml(netText) + '' : '') + buyText + (c.listings > 0 ? '' + c.listings + ' 出售' : '') + buyMeta + ''; } // cover mode return '' + '' + escapeHtml(sellText) + '' + (netText ? '到 ' + escapeHtml(netText) + '' : '') + (c.buyPrice !== null ? '' + (c.buyPrice > 0 ? '求 ' + formatPrice(c.buyPrice) : '无求购') + '' : '') + (c.listings > 0 ? '' + c.listings + ' 个出售' : '') + buyMeta + ''; } // v1.2.14: 标题栏右上角快捷链接组(原底部操作区,fix14 上移) function buildDetailActionsHtml(badge, marketData) { var links = []; if (badge.url) { links.push('徽章页'); } if (badge.appId) { links.push('商店'); } if (marketData && marketData.cards && marketData.cards.length > 0) { links.push('社区市场'); } return links.length > 0 ? '' + links.join('') + '' : ''; } // v1.2.14: 详情浮窗按需抓取 —— 内存 → IDB/GM 缓存 → 实时请求,单游戏单飞 // v1.5.0: 缓存读取改为异步 (IDB 优先) // v1.5.4: 集成 SWR 模式 — stale 缓存(36-48H)立即返回,后台静默刷新 var _detailMarketInflight = {}; async function ensureDetailMarketData(appId) { // 1. 内存缓存 var existing = getMarketPriceForApp(appId); if (existing && existing.cards && existing.cards.length > 0) { // v1.5.4: SWR — stale 缓存(36-48H)立即返回,后台静默刷新 if (isCacheStale(existing)) { refreshStaleMarketPrices([appId]); } return existing; } // 2. IDB/GM 缓存 (异步读取) var cached = await readMarketCache(appId); if (cached && cached.cards && cached.cards.length > 0) { marketPrices[appId] = cached; // v1.5.4: SWR — stale 缓存(36-48H)立即返回,后台静默刷新 if (isCacheStale(cached)) { refreshStaleMarketPrices([appId]); } return cached; } // 3. 实时请求 (单飞去重) if (_detailMarketInflight[appId]) return _detailMarketInflight[appId]; _detailMarketInflight[appId] = fetchCardsForApp(appId).then(function(data) { if (data && data.cards && data.cards.length > 0) { marketPrices[appId] = data; writeMarketCache(appId, data); } return data; }).finally(function() { delete _detailMarketInflight[appId]; }); return _detailMarketInflight[appId]; } // v1.2.14-fix: 改为接收 getViewMode 回调,避免异步加载期间用户切换视图后用过期 viewMode 渲染 // v1.4.0: 新增 groupInfo 参数,支持分组模式 KPI 重渲染 function loadDetailMarketData(badge, panel, overlay, getViewMode, groupInfo) { var appId = badge.appId; ensureDetailMarketData(appId).then(function(data) { // 浮窗已关闭或已切换到其他游戏,放弃更新 if (!overlay.isConnected || !overlay.classList.contains('active')) return; if (panel.getAttribute('data-app-id') !== String(appId)) return; var currentViewMode = typeof getViewMode === 'function' ? getViewMode() : 'cover'; var infoGrid = panel.querySelector('.sbc-detail-info-grid'); if (infoGrid) infoGrid.innerHTML = buildDetailInfoItems(badge, data, groupInfo || null); var wrap = panel.querySelector('.sbc-detail-cards-wrap'); if (wrap) { // v1.2.14-fix4: 市场数据为空(无在售卡牌)时传 { empty: true } 状态, // 而非 null(否则会静默 fallback 到只显示已拥有卡牌,与其他详情页不一致) var state = null; if (!data || !data.cards || data.cards.length === 0) { state = { empty: true }; } wrap.innerHTML = buildDetailCardsHtml(badge, data, state, currentViewMode); } // v1.2.14-fix14: 快捷链接在标题栏右上角(不在 body),刷新时更新标题栏的链接组 var quicklinks = panel.querySelector('.sbc-detail-quicklinks'); if (quicklinks && quicklinks.parentNode) { var newLinksHtml = buildDetailActionsHtml(badge, data); if (newLinksHtml) { var tmp = document.createElement('div'); tmp.innerHTML = newLinksHtml; var newLinks = tmp.firstElementChild; if (newLinks) quicklinks.parentNode.replaceChild(newLinks, quicklinks); } } // 同步刷新背景卡片的角标/统计 // v1.5.5: 增量更新该 appId,不再全量扫描 scheduleIncrementalRender(appId); }).catch(function(err) { if (!overlay.isConnected) return; if (panel.getAttribute('data-app-id') !== String(appId)) return; var currentViewMode = typeof getViewMode === 'function' ? getViewMode() : 'cover'; var wrap = panel.querySelector('.sbc-detail-cards-wrap'); if (wrap) { var message = err && err.message ? err.message : '网络请求失败'; wrap.innerHTML = buildDetailCardsHtml(badge, null, { error: message }, currentViewMode); } }); } // ============================================================ // v1.2.1: 全部卡牌游戏视图 // ============================================================ function renderAllGamesView() { var gridWrap = document.getElementById('sbc-grid-wrap'); if (!gridWrap) return; LazyImageLoader.clear(); gridWrap.innerHTML = ''; // 如果数据库未加载,显示加载提示 if (!cardDbLoaded) { gridWrap.innerHTML = '' + '' + '正在加载全部卡牌游戏数据库...' + '加载卡牌数据库' + ''; var loadBtn = document.getElementById('sbc-load-carddb-btn'); if (loadBtn) { loadBtn.addEventListener('click', function() { loadCardDatabase().then(function() { if (cardDbLoaded) renderAllGamesView(); }); }); } return; } var allGames = getAllCardGames(); if (allGames.length === 0) { gridWrap.innerHTML = '卡牌数据库为空'; return; } // 搜索过滤 var searchInput = document.getElementById('sbc-search-input'); var searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''; var filterSelect = document.getElementById('sbc-filter-select'); var filterBy = filterSelect ? filterSelect.value : 'all'; var filtered = allGames.filter(function(game) { if (searchTerm && game.name.toLowerCase().indexOf(searchTerm) === -1 && game.appId.indexOf(searchTerm) === -1) return false; if (filterBy === 'unowned') return !game.isOwned; if (filterBy === 'owned') return game.isOwned; return true; }); // 排序 var sortSelect = document.getElementById('sbc-sort-select'); var sortBy = sortSelect ? sortSelect.value : 'default'; switch (sortBy) { case 'name_asc': filtered.sort(function(a, b) { return a.name.localeCompare(b.name); }); break; case 'name_desc': filtered.sort(function(a, b) { return b.name.localeCompare(a.name); }); break; case 'xp_desc': case 'progress_desc': filtered.sort(function(a, b) { var ba = a.badge || {}, bb = b.badge || {}; return (bb.progress || 0) - (ba.progress || 0); }); break; case 'xp_asc': case 'progress_asc': filtered.sort(function(a, b) { var ba = a.badge || {}, bb = b.badge || {}; return (ba.progress || 0) - (bb.progress || 0); }); break; case 'value_desc': filtered.sort(function(a, b) { var sa = a.badge ? (valueScores[a.appId] ? valueScores[a.appId].score : 0) : 0; var sb = b.badge ? (valueScores[b.appId] ? valueScores[b.appId].score : 0) : 0; return sb - sa; }); break; case 'drops_desc': filtered.sort(function(a, b) { var ba = a.badge || {}, bb = b.badge || {}; return (bb.drops || 0) - (ba.drops || 0); }); break; default: // 默认:已拥有的排前面,然后按名称 filtered.sort(function(a, b) { if (a.isOwned !== b.isOwned) return a.isOwned ? -1 : 1; return a.name.localeCompare(b.name); }); } // 分页 var totalPages = Math.ceil(filtered.length / CONFIG.CARDS_PER_PAGE); if (currentPage > totalPages) currentPage = 1; var grid = document.createElement('div'); grid.className = 'sbc-grid'; var start = (currentPage - 1) * CONFIG.CARDS_PER_PAGE; var end = Math.min(start + CONFIG.CARDS_PER_PAGE, filtered.length); filtered.slice(start, end).forEach(function(game, i) { grid.appendChild(createDbCard(game, i * 20)); }); gridWrap.appendChild(grid); // v1.3.1: 扫描懒加载图片 LazyImageLoader.scan(grid); // 分页 if (totalPages > 1) { gridWrap.appendChild(createAllGamesPagination(filtered.length, totalPages)); } } function createDbCard(game, animDelay) { var card = document.createElement('a'); card.className = 'sbc-db-card sbc-card-enter' + (game.isOwned ? ' sbc-db-card--owned' : ' sbc-db-card--unowned'); card.href = 'https://store.steampowered.com/app/' + game.appId; card.target = '_blank'; card.style.animationDelay = (animDelay || 0) + 'ms'; var statusBadge = game.isOwned ? '已拥有' : '未拥有'; var levelBadge = game.size > 0 ? '' + game.size + ' 张卡' : ''; // 如果已拥有且有徽章图片,使用徽章图片 // v1.2.14-fix12: 未拥有的游戏使用 Steam 商店封面图(capsule_236x174.jpg,公开 CDN 无需认证) // v1.3.1: 使用 data-src 懒加载 var imgContent = ''; if (game.isOwned && game.badge) { if (game.badge.badgeImg) { imgContent = ''; } else if (game.badge.lockedIconImg) { imgContent = ''; } } // v1.2.14-fix12: 未拥有的游戏(或已拥有但无徽章图片)用 Steam 商店封面图 // v1.3.5: 引用 sglv-cover-fallback.lib.js 三级降级(多CDN→appdetails API→SVG占位图), // 替代原单 CDN capsule + 内联 onerror 方案,解决封面加载失败显示破图的问题 if (!imgContent && game.appId) { var dbCoverUrl; if (window.SGLVCoverFallback && window.SGLVCoverFallback.getCoverUrl) { dbCoverUrl = window.SGLVCoverFallback.getCoverUrl(game.appId); } else { dbCoverUrl = 'https://cdn.cloudflare.steamstatic.com/steam/apps/' + game.appId + '/capsule_236x174.jpg'; } imgContent = ''; } if (!imgContent) { // 无 appId 的兜底(理论上不会走到这里) imgContent = '' + '' + '' + '' + ''; } // 进度信息(仅已拥有的显示) var progressHtml = ''; if (game.isOwned && game.badge && game.badge.total > 0) { var progressColor = game.badge.isCompleted ? 'var(--sbc-success)' : 'var(--sbc-accent)'; progressHtml = '' + '' + game.badge.collected + '/' + game.badge.total + '' + '' + '' + game.badge.progress + '%' + ''; } var xpHtml = ''; if (game.isOwned && game.badge && game.badge.xp > 0) { xpHtml = '' + game.badge.xp + ' XP' + game.appId + ''; } else { xpHtml = '' + game.size + ' 张卡' + game.appId + ''; } card.innerHTML = '' + imgContent + statusBadge + levelBadge + '' + '' + '' + '' + escapeHtml(game.name) + '' + '详情' + '' + progressHtml + xpHtml + ''; // v1.2.4: 详情按钮点击 — 构建虚拟 badge 对象供 showCardDetail 使用 var detailBtn = card.querySelector('.sbc-db-card-detail-btn'); if (detailBtn) { detailBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // 构建虚拟 badge 对象 var virtualBadge = game.badge || { index: -1, appId: game.appId, gameName: game.name, badgeName: game.name, badgeImg: '', lockedIconImg: '', url: 'https://steamcommunity.com/my/gamecards/' + game.appId, xp: 0, level: 0, unlockTime: '', progressText: '', collected: 0, total: 0, progress: 0, drops: 0, dropsText: '', playtime: '', cardImgs: [], isCompleted: false, isLocked: !game.isOwned, badgeType: 'game', isFoil: false, border: '0' }; showCardDetail(virtualBadge); }); } return card; } function createAllGamesPagination(totalItems, totalPages) { return createPagination(totalItems, totalPages, renderAllGamesView); } // ============================================================ // Pro: 分析浮窗 // ============================================================ function createAnalysisPanel() { var existing = document.getElementById('sbc-analysis-overlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.className = 'sbc-analysis-overlay'; overlay.id = 'sbc-analysis-overlay'; overlay.addEventListener('click', function(e) { if (e.target === overlay) toggleAnalysisPanel(); }); var panel = document.createElement('div'); panel.className = 'sbc-analysis-panel'; panel.addEventListener('click', function(e) { e.stopPropagation(); }); panel.innerHTML = '' + '徽章分析中心Pro' + '✕' + '' + '' + '概览统计' + '价值排行' + '市场价格' + '交易推荐' + '热门市场' + 'AI 洞察' + '' + ''; overlay.appendChild(panel); document.body.appendChild(overlay); panel.querySelectorAll('.sbc-ap-tab').forEach(function(tab) { tab.addEventListener('click', function() { panel.querySelectorAll('.sbc-ap-tab').forEach(function(t) { t.classList.remove('active'); }); tab.classList.add('active'); analysisActiveTab = tab.dataset.tab; renderAnalysisContent(); }); }); document.getElementById('sbc-ap-close').addEventListener('click', toggleAnalysisPanel); document.addEventListener('keydown', function escH(e) { if (e.key === 'Escape' && analysisVisible) toggleAnalysisPanel(); }); renderAnalysisContent(); } function renderAnalysisContent() { var content = document.getElementById('sbc-ap-content'); if (!content) return; switch (analysisActiveTab) { case 'overview': renderOverview(content); break; case 'values': renderValues(content); break; case 'prices': renderPrices(content); break; case 'trade': renderTrade(content); break; case 'popular': renderPopularMarket(content); break; case 'aiinsight': renderAiInsight(content); break; } // v1.3.1: 扫描懒加载图片 LazyImageLoader.scan(content); } function renderOverview(content) { var total = allBadges.length; var completed = allBadges.filter(function(b) { return b.isCompleted; }).length; var locked = allBadges.filter(function(b) { return b.isLocked; }).length; var totalXp = allBadges.reduce(function(s, b) { return s + b.xp; }, 0); var totalLvl = allBadges.reduce(function(s, b) { return s + b.level; }, 0); var drops = allBadges.reduce(function(s, b) { return s + b.drops; }, 0); var rate = total > 0 ? Math.round((completed / total) * 100) : 0; var avgLvl = total > 0 ? (totalLvl / total).toFixed(1) : 0; var levelDist = getLevelDistribution(); var lvlColors = ['#e74c3c', '#f59e0b', '#66c0f4', '#10b981', '#a855f7', '#c7a02e']; var lvlLabels = ['Lv.1', 'Lv.2', 'Lv.3', 'Lv.4', 'Lv.5', '闪亮']; var lvlTotal = Object.values(levelDist).reduce(function(s, v) { return s + v; }, 0); var barHtml = '', legendHtml = ''; Object.entries(levelDist).forEach(function(e, i) { var cnt = e[1]; var pct = lvlTotal > 0 ? (cnt / lvlTotal * 100) : 0; if (cnt > 0) barHtml += '' + (pct >= 5 ? cnt : '') + ''; legendHtml += '' + lvlLabels[i] + ': ' + cnt + ''; }); var xpDist = getXpDistribution(); var xpTotal = Object.values(xpDist).reduce(function(s, v) { return s + v; }, 0); var xpColors = ['#e74c3c', '#f59e0b', '#66c0f4', '#10b981', '#a855f7', '#c7a02e']; var xpBar = ''; Object.keys(xpDist).forEach(function(k, i) { var cnt = xpDist[k]; var pct = xpTotal > 0 ? (cnt / xpTotal * 100) : 0; if (cnt > 0) xpBar += '' + (pct >= 8 ? cnt : '') + ''; }); var pricedCnt = 0, marketVal = 0, seen = {}; allBadges.forEach(function(b) { if (!b.appId || seen[b.appId]) return; var mp = getMarketPriceForApp(b.appId); if (mp && mp.totalCost > 0) { seen[b.appId] = true; pricedCnt++; marketVal += mp.totalCost; } }); var topXp = allBadges.reduce(function(a, b) { return a.xp > b.xp ? a : b; }, allBadges[0] || {}); var topDrop = allBadges.reduce(function(a, b) { return a.drops > b.drops ? a : b; }, allBadges[0] || {}); content.innerHTML = '' + '总徽章数' + total + '已完成 ' + completed + ' · 完成率 ' + rate + '%' + '总经验值' + totalXp.toLocaleString() + '平均等级 Lv.' + avgLvl + '' + '可掉落卡牌' + drops + '未解锁 ' + locked + ' 个徽章' + '市场估值' + (pricedCnt > 0 ? formatPrice(marketVal) : 'N/A') + '已评估 ' + pricedCnt + ' 个游戏' + '' + (cardDbLoaded ? (function() { var totalCardGames = Object.keys(cardDatabase).length; var unownedList = getUnownedCardGames(); var unownedCardGames = unownedList.length; var ownedCardGames = totalCardGames - unownedCardGames; var coverage = totalCardGames > 0 ? Math.round((ownedCardGames / totalCardGames) * 100) : 0; // 取未拥有游戏中卡牌等级最高的 Top 5 var topUnowned = unownedList.slice().sort(function(a, b) { return b.size - a.size; }).slice(0, 5); var topUnownedHtml = topUnowned.map(function(g, i) { return '' + '' + (i + 1) + '' + getGameCoverHtml(g.appId) + '' + escapeHtml(g.name) + '' + '' + g.size + ' 张卡' + '' + '未拥有'; }).join(''); return '卡牌游戏覆盖率 (v1.2.1)' + '' + '全部卡牌游戏' + totalCardGames.toLocaleString() + 'SteamCardExchange 数据库' + '已拥有' + ownedCardGames + '覆盖率 ' + coverage + '%' + '未拥有' + unownedCardGames + '可探索的新游戏' + '' + '' + coverage + '% 覆盖率' + (topUnownedHtml ? '未拥有游戏中最高等级 Top 5:' + topUnownedHtml + '' : '') + ''; })() : '') + '等级分布' + barHtml + '' + legendHtml + '' + '经验值分布' + xpBar + '' + '快速洞察' + '1' + getGameCoverHtml(topXp.appId) + '最高经验值徽章' + escapeHtml(topXp.gameName) + ' (' + topXp.xp + ' XP)' + '2' + getGameCoverHtml(topDrop.appId) + '最多掉落卡牌' + escapeHtml(topDrop.gameName) + ' (' + topDrop.drops + ' 张)' + ''; } function renderValues(content) { var valuable = getValuableBadges(30); if (!valuable.length) { content.innerHTML = '暂无价值分析数据,请先加载市场价格'; return; } var rows = valuable.map(function(it, i) { var rc = i === 0 ? ' sbc-ap-rank-num--gold' : i === 1 ? ' sbc-ap-rank-num--silver' : i === 2 ? ' sbc-ap-rank-num--bronze' : ''; var sc = it.score >= 70 ? ' sbc-ap-rank-score--high' : it.score >= 40 ? ' sbc-ap-rank-score--mid' : ' sbc-ap-rank-score--low'; // v1.2.2: 升级潜力展示 var sd = it.scoreData || {}; var maxLvl = sd.maxLevelForApp || 0; var remain = sd.remainingLevels != null ? sd.remainingLevels : 0; var potScore = sd.upgradePotentialScore || 0; var levelDisplay = 'Lv.' + it.badge.level; // 潜力评级标签 var tagClass, tagText; if (remain <= 0) { tagClass = 'sbc-ap-rank-tag--craft'; tagText = '已满级'; } else if (potScore >= 60) { tagClass = 'sbc-ap-rank-tag--buy'; tagText = '高潜力'; } else if (potScore >= 45) { tagClass = 'sbc-ap-rank-tag--drop'; tagText = '中潜力'; } else { tagClass = 'sbc-ap-rank-tag--trade'; tagText = '接近满级'; } // hover 详情 var detail = 'Lv.' + it.badge.level + (maxLvl > 0 ? ' · ' + maxLvl + '张卡' : '') + ' · 剩余' + remain + '级' + ' · 升级潜力' + potScore + ' · XP' + sd.xpScore + ' · 卡价' + sd.cardPriceScore + ' · 性价比' + sd.costEfficiencyScore + ' · 进度' + sd.completionBonus + ' · 掉落' + sd.dropBonus + (sd.avgPrice > 0 ? ' · 均价' + formatPrice(sd.avgPrice) : ''); return '' + '' + (i + 1) + '' + getGameCoverHtml(it.badge.appId) + '' + escapeHtml(it.badge.gameName) + '' + '' + levelDisplay + '' + '' + it.score + ' 分' + '' + tagText + ''; }).join(''); content.innerHTML = '价值排行榜(综合评分)' + '' + 'v1.2.2 评分维度:升级潜力(基于游戏最大等级,Lv.3/Max15 > Lv.3/Max5)、经验值、卡牌成本、性价比(每元XP)、收集进度、掉落数量' + '悬停查看各维度明细 · 等级列显示 Lv.当前/最大' + '' + rows + ''; } function renderPrices(content) { var priced = []; var seen = {}; allBadges.forEach(function(b) { if (!b.appId || seen[b.appId]) return; var mp = getMarketPriceForApp(b.appId); if (mp && mp.cards && mp.cards.length > 0) { seen[b.appId] = true; priced.push({ badge: b, market: mp }); } }); priced.sort(function(a, b) { return b.market.totalCost - a.market.totalCost; }); if (!priced.length) { // v1.5.0: 增强提示 — 加载中和无数据区分显示 content.innerHTML = (marketLoading ? '正在加载社区市场价格...' : '' + '暂无市场价格数据' + '市场价格数据尚未加载或加载失败。点击下方按钮刷新市场价格。' + '') + '刷新市场价格'; return; } var rows = priced.slice(0, 30).map(function(it, i) { var coverUrl = it.badge.appId ? (SBC_CDN_BASE + it.badge.appId + '/capsule_184x69.jpg') : SBC_COVER_PLACEHOLDER; return '' + '' + (i + 1) + '' + '' + '' + escapeHtml(it.badge.gameName) + '' + '' + it.market.cardCount + ' 张' + '' + (it.market.avgPrice > 0 ? formatPrice(it.market.avgPrice) : 'N/A') + '' + // v1.3.2: 新增中位价和税后收入列 (借鉴 Trading Card Info 脚本统计模式) '' + (it.market.medianPrice > 0 ? formatPrice(it.market.medianPrice) : 'N/A') + '' + '' + (it.market.netIncome > 0 ? formatPrice(it.market.netIncome) : 'N/A') + '' + '' + (it.market.totalCost > 0 ? formatPrice(it.market.totalCost) : 'N/A') + ''; }).join(''); content.innerHTML = '' + '卡牌市场价格' + '刷新价格' + '展示 ' + priced.length + ' 个游戏 · 更新于 ' + new Date(priced[0].market.updatedAt).toLocaleString() + 'v1.3.2: 税后收入采用精确手续费计算(含可变发行商费率)' + '#游戏卡牌数均价中位价税后收入总价' + rows + ''; } function renderTrade(content) { var recs = getTradeRecommendations(); if (!recs.length) { // v1.5.0: 增强提示 — 区分加载中和无数据 if (marketLoading) { content.innerHTML = '' + '' + '正在加载市场价格数据,请稍候…' + ''; } else { content.innerHTML = '' + '暂无交易推荐数据' + '需要先加载市场价格数据才能生成交易推荐。点击下方按钮加载市场价格。' + '' + '加载市场价格'; } return; } var rows = recs.map(function(it, i) { var rc = i === 0 ? ' sbc-ap-rank-num--gold' : i === 1 ? ' sbc-ap-rank-num--silver' : i === 2 ? ' sbc-ap-rank-num--bronze' : ''; var tc = '', tt = ''; if (it.avgCardPrice > 200) { tc = ' sbc-ap-rank-tag--buy'; tt = '关注'; } else if (it.badge.drops > 0 && it.badge.progress >= 50) { tc = ' sbc-ap-rank-tag--craft'; tt = '合成'; } else if (it.avgCardPrice <= 100) { tc = ' sbc-ap-rank-tag--trade'; tt = '购买'; } else { tc = ' sbc-ap-rank-tag--drop'; tt = '掉落'; } return '' + '' + (i + 1) + '' + getGameCoverHtml(it.badge.appId) + '' + escapeHtml(it.badge.gameName) + '' + '' + (it.avgCardPrice > 0 ? formatPrice(it.avgCardPrice) : 'N/A') + '' + '' + it.badge.progress + '%' + '' + tt + ''; }).join(''); content.innerHTML = '交易与合成推荐综合卡牌价格、掉落数量、完成进度推荐值得交易的徽章' + rows + ''; } function toggleAnalysisPanel() { analysisVisible = !analysisVisible; var existing = document.getElementById('sbc-analysis-overlay'); if (analysisVisible) { if (!existing) { computeValueScores(); createAnalysisPanel(); } else { existing.style.display = 'flex'; renderAnalysisContent(); } var btn = document.getElementById('sbc-analysis-btn'); if (btn) btn.classList.add('active'); } else { if (existing) existing.style.display = 'none'; var btn2 = document.getElementById('sbc-analysis-btn'); if (btn2) btn2.classList.remove('active'); } } // ============================================================ // 筛选和排序 // ============================================================ function applyFiltersAndSort() { var searchInput = document.getElementById('sbc-search-input'); var sortSelect = document.getElementById('sbc-sort-select'); var filterSelect = document.getElementById('sbc-filter-select'); var searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''; var sortBy = sortSelect ? sortSelect.value : 'default'; var filterBy = filterSelect ? filterSelect.value : 'all'; // v1.2.1: 如果是全部游戏视图,直接重新渲染(筛选逻辑在 renderAllGamesView 内部) // v1.2.3: 活动勋章视图同理 if (viewMode === 'alldb' || viewMode === 'activity') { currentPage = 1; renderCardView(); if (analysisVisible) renderAnalysisContent(); return; } filteredBadges = allBadges.filter(function(badge) { if (searchTerm && badge.gameName.toLowerCase().indexOf(searchTerm) === -1 && badge.badgeName.toLowerCase().indexOf(searchTerm) === -1) return false; switch (filterBy) { case 'activity': return badge.badgeType === 'event' || badge.badgeType === 'community' || badge.badgeType === 'activity'; case 'game': return badge.badgeType === 'game' || badge.badgeType === 'foil'; case 'foil': return badge.isFoil; case 'in_progress': return !badge.isCompleted; case 'completed': return badge.isCompleted; // v1.2.14-fix10: craftable 筛选用 hasCraftButton 或 isCompleted case 'craftable': return !!badge.appId && (badge.hasCraftButton || (badge.isCompleted && badge.total > 0)); case 'has_drops': return badge.drops > 0; case 'locked': return badge.isLocked; case 'valuable': return valueScores[badge.appId] && valueScores[badge.appId].score >= 50; // v1.2.1: 在普通视图中,owned/unowned 仅显示有/无 appId 的(均不适用普通视图,主要在 alldb 视图中使用) case 'owned': return !!badge.appId; case 'unowned': return !badge.appId; default: return true; } }); switch (sortBy) { case 'name_asc': filteredBadges.sort(function(a, b) { return a.gameName.localeCompare(b.gameName); }); break; case 'name_desc': filteredBadges.sort(function(a, b) { return b.gameName.localeCompare(a.gameName); }); break; case 'progress_desc': filteredBadges.sort(function(a, b) { return b.progress - a.progress; }); break; case 'progress_asc': filteredBadges.sort(function(a, b) { return a.progress - b.progress; }); break; // v1.2.3: 按徽章等级排序(闪卡优先于普通) case 'level_desc': filteredBadges.sort(function(a, b) { if (a.isFoil !== b.isFoil) return a.isFoil ? -1 : 1; return b.level - a.level; }); break; case 'level_asc': filteredBadges.sort(function(a, b) { if (a.isFoil !== b.isFoil) return b.isFoil ? -1 : 1; return a.level - b.level; }); break; case 'xp_desc': filteredBadges.sort(function(a, b) { return b.xp - a.xp; }); break; case 'xp_asc': filteredBadges.sort(function(a, b) { return a.xp - b.xp; }); break; case 'value_desc': filteredBadges.sort(function(a, b) { var sa = valueScores[a.appId] ? valueScores[a.appId].score : 0; var sb = valueScores[b.appId] ? valueScores[b.appId].score : 0; return sb - sa; }); break; case 'drops_desc': filteredBadges.sort(function(a, b) { return b.drops - a.drops; }); break; } currentPage = 1; renderCardView(); if (analysisVisible) renderAnalysisContent(); } // ============================================================ // 导出功能 // ============================================================ function exportToJSON() { var eventCount = 0, communityCount = 0, foilCount = 0; filteredBadges.forEach(function(b) { if (b.badgeType === 'event') eventCount++; else if (b.badgeType === 'community') communityCount++; else if (b.badgeType === 'foil' || b.isFoil) foilCount++; }); var data = { exportTime: new Date().toISOString(), scriptVersion: '1.2.13', totalBadges: filteredBadges.length, cardDbLoaded: cardDbLoaded, totalCardGames: cardDbLoaded ? Object.keys(cardDatabase).length : 0, activityBadgeCount: activityBadges.length, gameBadgeCount: gameBadges.length, eventBadgeCount: eventCount, communityBadgeCount: communityCount, foilBadgeCount: foilCount, badges: filteredBadges.map(function(b) { var sd = valueScores[b.appId] || {}; return { gameName: b.gameName, badgeName: b.badgeName, appId: b.appId, badgeType: b.badgeType, isFoil: b.isFoil, level: b.level, maxBadgeLevel: getMaxBadgeLevel(b), cardCountPerCraft: getCardCountForApp(b.appId), remainingLevels: sd.remainingLevels != null ? sd.remainingLevels : 0, xp: b.xp, progress: b.progress, collected: b.collected, total: b.total, drops: b.drops, playtime: b.playtime, unlockTime: b.unlockTime, url: b.url, isCompleted: b.isCompleted, isLocked: b.isLocked, valueScore: sd.score || 0, valueScoreDetail: { upgradePotential: sd.upgradePotentialScore || 0, xp: sd.xpScore || 0, cardPrice: sd.cardPriceScore || 0, costEfficiency: sd.costEfficiencyScore || 0, completionBonus: sd.completionBonus || 0, dropBonus: sd.dropBonus || 0 } }; }) }; var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'steam-badges-pro-' + new Date().toISOString().slice(0, 10) + '.json'; document.body.appendChild(a); a.click(); setTimeout(function() { a.remove(); URL.revokeObjectURL(url); }, 500); showToast('JSON 导出成功(含Pro分析数据)', 'success'); } function exportToCSV() { var headers = ['游戏名称', '徽章名称', 'AppID', '勋章类型', '闪卡', '等级', '最高等级', '单次合卡所需', '剩余可升级', '经验值', '进度(%)', '已收集卡牌', '总卡牌', '剩余掉落', '游玩时间', '解锁时间', '链接', '已完成', '锁定状态', '价值评分', '升级潜力', '卡牌成本分', '性价比分']; var rows = filteredBadges.map(function(b) { var sd = valueScores[b.appId] || {}; var maxLvl = getMaxBadgeLevel(b); var cardCount = getCardCountForApp(b.appId); var remain = sd.remainingLevels != null ? sd.remainingLevels : Math.max(0, (maxLvl > 0 ? maxLvl : 5) - b.level); var typeLabelMap = { 'game': '游戏勋章', 'foil': '游戏闪卡', 'event': '季节/活动勋章', 'community': '社区勋章', 'activity': '活动/社区勋章' }; var typeLabel = typeLabelMap[b.badgeType] || '游戏勋章'; return ['"' + b.gameName.replace(/"/g, '""') + '"', '"' + b.badgeName.replace(/"/g, '""') + '"', b.appId, typeLabel, b.isFoil ? '是' : '否', b.level, maxLvl === -1 ? '无上限' : maxLvl, cardCount, remain, b.xp, b.progress, b.collected, b.total, b.drops, '"' + b.playtime.replace(/"/g, '""') + '"', '"' + b.unlockTime.replace(/"/g, '""') + '"', b.url, b.isCompleted ? '是' : '否', b.isLocked ? '是' : '否', sd.score || 0, sd.upgradePotentialScore || 0, sd.cardPriceScore || 0, sd.costEfficiencyScore || 0]; }); var csvContent = '\uFEFF' + [headers.join(','), rows.map(function(r) { return r.join(','); }).join('\n')].join('\n'); var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'steam-badges-pro-' + new Date().toISOString().slice(0, 10) + '.csv'; document.body.appendChild(a); a.click(); setTimeout(function() { a.remove(); URL.revokeObjectURL(url); }, 500); showToast('CSV 导出成功(含Pro分析数据)', 'success'); } // ============================================================ // 视图切换 // ============================================================ function toggleView() { var originalSheet = document.querySelector('.badges_sheet'); var cardView = document.getElementById('sbc-root'); var toggleBtn = document.getElementById('sbc-toggle'); if (!originalSheet || !cardView) return; cardViewVisible = !cardViewVisible; if (cardViewVisible) { originalSheet.style.display = 'none'; cardView.style.display = 'block'; document.querySelectorAll('.profile_paging').forEach(function(el) { el.style.display = 'none'; }); document.querySelectorAll('.profile_badges_sortoptions').forEach(function(el) { el.style.display = 'none'; }); if (toggleBtn) { toggleBtn.classList.remove('sbc-btn--accent'); toggleBtn.innerHTML = ' 原版'; } } else { originalSheet.style.display = ''; cardView.style.display = 'none'; document.querySelectorAll('.profile_paging').forEach(function(el) { el.style.display = ''; }); document.querySelectorAll('.profile_badges_sortoptions').forEach(function(el) { el.style.display = ''; }); if (toggleBtn) { toggleBtn.classList.add('sbc-btn--accent'); toggleBtn.innerHTML = ' 卡片'; } } } // ============================================================ // 市场价格加载 // ============================================================ // v1.3.2: 社区登录状态前置检测 (借鉴 Trading Card Info 脚本 checkLoginStatus) // 缓存登录状态避免重复请求;未登录时提前提示而非让批量请求静默失败 var _communityLoginCache = null; // null=未检测, true=已登录, false=未登录 var _communityLoginChecking = false; function checkCommunityLogin() { if (_communityLoginCache !== null) return Promise.resolve(_communityLoginCache); if (_communityLoginChecking) return Promise.resolve(null); _communityLoginChecking = true; return new Promise(function(resolve) { GM_xmlhttpRequest({ method: 'GET', url: 'https://steamcommunity.com/market/', timeout: 8000, onload: function(r) { _communityLoginChecking = false; try { var doc = new DOMParser().parseFromString(r.responseText, 'text/html'); var loggedIn = !!doc.querySelector('#account_pulldown'); _communityLoginCache = loggedIn; resolve(loggedIn); } catch (e) { _communityLoginCache = null; resolve(null); } }, onerror: function() { _communityLoginChecking = false; _communityLoginCache = null; resolve(null); }, ontimeout: function() { _communityLoginChecking = false; _communityLoginCache = null; resolve(null); }, }); }); } async function loadMarketPrices() { // v1.3.2: 社区登录状态前置检测 (借鉴 Trading Card Info 脚本 checkLoginStatus) // 未登录时提前提示,避免批量请求全部静默失败 var loggedIn = await checkCommunityLogin(); if (loggedIn === false) { showToast('Steam 社区未登录,无法获取卡牌价格', 'error'); return; } // 1) 确保卡牌数据库已加载 if (!cardDbLoaded) { showToast('正在加载卡牌数据库...'); try { await loadCardDatabase(); } catch (e) { showToast('卡牌数据库加载失败', 'error'); return; } } // v1.2.10 修复: 单一 in-flight 任务;已经在加载中就直接挂到现有 promise,避免并发双计数 if (_marketLoadPromise) { console.log('[SBC-Pro] 市场价格:已在加载中,挂到现有任务(来自 loadMarketPrices)'); return _marketLoadPromise; } // 2) 获取账号所有游戏,与卡牌数据库取交集 var appIds = [], seen = {}; var ownedGames = await fetchOwnedGamesFromPage(); if (ownedGames.length > 0) { // 有完整游戏库 → 取交集 ownedGames.forEach(function(g) { if (g.appid && cardDatabase[g.appid] && !seen[g.appid]) { seen[g.appid] = true; appIds.push(g.appid); } }); console.log('[SBC-Pro] 拥有的卡牌游戏: ' + appIds.length + ' 款'); } else { // 回退:用当前页徽章数据 allBadges.forEach(function(b) { if (b.appId && !seen[b.appId]) { seen[b.appId] = true; appIds.push(b.appId); } }); } if (!appIds.length) { showToast('没有找到可查询价格的卡牌游戏', 'error'); return; } showToast('正在加载 ' + appIds.length + ' 款卡牌游戏的市场价格...'); if (typeof showMarketProgress === 'function') showMarketProgress({ total: appIds.length, done: 0, success: 0 }); // 启动任务并缓存 promise _marketLoadPromise = fetchMarketPrices(appIds).finally(function() { _marketLoadPromise = null; }); await _marketLoadPromise; computeValueScores(); renderCardView(); if (analysisVisible) renderAnalysisContent(); var successCount = Object.keys(marketPrices).filter(function(k) { return marketPrices[k] && marketPrices[k].cardCount > 0; }).length; showToast('市场价格加载完成(' + successCount + '/' + appIds.length + ' 个游戏)', 'success'); } // ============================================================ // 主函数 // ============================================================ function init() { console.log('[SBC-Pro] 初始化 Steam 徽章卡片视图 Pro v1.5.5...'); // v1.5.0: 初始化 IndexedDB 缓存层 (异步,不阻塞首屏渲染) IDB.init().then(function() { console.log('[SBC-Pro] IndexedDB 缓存层就绪'); }).catch(function(e) { console.warn('[SBC-Pro] IndexedDB 初始化异常,使用 GM 回退:', e); }); GM_addStyle(CSS); // v1.5.5: IDB 就绪后预热图片缓存索引,之后懒加载图片可同步判定缓存状态 IDB.init().then(function() { warmImageBlobIndex(); }).catch(function() {}); allBadges = extractBadgesFromDOM(); filteredBadges = allBadges.slice(); if (!allBadges.length) { console.log('[SBC-Pro] 未找到徽章数据'); return; } // v1.2.3: 初步勋章分类(数据库加载后会修正) classifyBadges(); updateCraftableBadges(); var container = document.createElement('div'); container.className = 'sbc-root'; container.id = 'sbc-root'; var badgeHeader = document.querySelector('.profile_badges_header'); if (badgeHeader) { // v1.2.14-fix3: 隐藏原版 .profile_small_header_texture (头像+昵称), // 用我们的信息面板覆盖该位置(避免头像/昵称重复展示) var originalHeader = document.querySelector('.profile_small_header_texture'); if (originalHeader) originalHeader.style.display = 'none'; // v1.2.12: 仅隐藏 .profile_xp_block (用我们自己的信息面板替换 XP/等级展示) var originalXp = document.querySelector('.profile_xp_block'); if (originalXp) originalXp.style.display = 'none'; // v1.2.14-fix5: 隐藏原版徽章标题(.profile_badges_header 通常是 "徽章" 大标题) // 我们的工具栏已有视图切换,不需要重复的标题 badgeHeader.style.display = 'none'; // v1.2.14-fix5: 优化顶部多余空白——减少 .profile_small_header_area 的高度 var headerArea = document.querySelector('.profile_small_header_area'); if (headerArea) { headerArea.style.minHeight = 'auto'; headerArea.style.height = 'auto'; headerArea.style.padding = '0'; headerArea.style.margin = '0'; } // v1.2.7: 用户信息头部面板(参考 Steam 原版 + 第三方设计图) // v1.2.10: 移动到徽章标题(.profile_badges_header)之前 userInfo = extractUserInfo(); if (userInfo && (userInfo.personaName || userInfo.steamId)) { var userHeader = createUserHeader(userInfo); if (userHeader) { // v1.2.10: 关键变更 —— 插入到 badgeHeader 之前(而不是之后) badgeHeader.parentNode.insertBefore(userHeader, badgeHeader); // v1.2.7: 刷新按钮事件 var refreshBtn = document.getElementById('sbc-user-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', function() { refreshBtn.classList.add('spinning'); setTimeout(function() { location.reload(); }, 600); }); } // v1.2.10: 设置按钮事件 var settingsBtn = document.getElementById('sbc-user-settings'); if (settingsBtn) { settingsBtn.addEventListener('click', function() { openUserSettings(); }); } // v1.2.10: 异步加载总游玩时长(优先 Steam Web API,再 XML 兜底) if (userInfo.steamId) { loadUserStatsWithApi(userInfo.steamId).then(function(apiData) { if (apiData) { // 用 API 数据更新 KPI 卡片 updateUserHeaderKpis(userHeader, apiData); } else { // 兜底:从 XML profile 拉取 fetchUserPlaytime(userInfo.steamId).then(function(minutes) { userInfo.totalPlaytimeMinutes = minutes; var slot = userHeader.querySelector('[data-sbc-kpi="playtime"]'); if (slot) slot.textContent = formatPlaytime(minutes); }); } }); } // v1.3.0: 异步获取好友数 — 优先 Steam API,降级 HTML 抓取 if (userInfo.steamId || userInfo.profileUrl) { fetchFriendsCount(userInfo.steamId, userInfo.profileUrl).then(function(count) { if (count > 0) { userInfo.totalFriends = count; // 更新好友 KPI 卡片 var friendsCard = userHeader.querySelector('.sbc-user-kpi--teal'); if (friendsCard) { friendsCard.classList.remove('sbc-user-kpi--empty'); var valueEl = friendsCard.querySelector('.sbc-user-kpi-value'); var emptyEl = friendsCard.querySelector('.sbc-user-kpi-empty'); if (emptyEl) emptyEl.remove(); if (valueEl) valueEl.textContent = count.toLocaleString(); } } }); } } } var toolbar = createToolbar(); badgeHeader.parentNode.insertBefore(toolbar, badgeHeader.nextSibling); } var originalSheet = document.querySelector('.badges_sheet'); if (originalSheet) { originalSheet.parentNode.insertBefore(container, originalSheet); originalSheet.style.display = 'none'; } document.querySelectorAll('.profile_paging').forEach(function(el) { el.style.display = 'none'; }); document.querySelectorAll('.profile_badges_sortoptions').forEach(function(el) { el.style.display = 'none'; }); renderCardView(); // v1.2.10: 后台预加载市场价格(让卡片首屏先渲染,再后台拉价格) // 延迟 250ms 给首屏渲染让出主线程 setTimeout(function() { try { if (!cardDbLoaded) { // 加载卡牌数据库 → 加载市场价格(全静默,不弹吐司避免干扰用户) loadCardDatabase().then(function() { return fetchOwnedGamesFromPage(); }).then(function(ownedGames) { var appIds = [], seen = {}; if (ownedGames && ownedGames.length > 0) { ownedGames.forEach(function(g) { if (g.appid && cardDatabase[g.appid] && !seen[g.appid]) { seen[g.appid] = true; appIds.push(g.appid); } }); } else { allBadges.forEach(function(b) { if (b.appId && !seen[b.appId]) { seen[b.appId] = true; appIds.push(b.appId); } }); } if (appIds.length === 0) return null; return fetchMarketPrices(appIds); }).then(function(prices) { if (prices && analysisVisible) { computeValueScores(); renderAnalysisContent(); } }).catch(function(e) { console.warn('[SBC-Pro] 后台预加载价格失败:', e && e.message); }); } } catch (e) { /* 静默 */ } }, CONFIG.MARKET_PAGE_START_DELAY); // 事件绑定 var searchInput = document.getElementById('sbc-search-input'); var sortSelect = document.getElementById('sbc-sort-select'); var filterSelect = document.getElementById('sbc-filter-select'); var toggleBtn = document.getElementById('sbc-toggle'); var exportJsonBtn = document.getElementById('sbc-export-json'); var exportCsvBtn = document.getElementById('sbc-export-csv'); var modeGridBtn = document.getElementById('sbc-mode-grid'); var modeGroupedBtn = document.getElementById('sbc-mode-grouped'); var analysisBtn = document.getElementById('sbc-analysis-btn'); if (searchInput) searchInput.addEventListener('input', debounce(applyFiltersAndSort, CONFIG.DEBOUNCE_DELAY)); if (sortSelect) sortSelect.addEventListener('change', applyFiltersAndSort); if (filterSelect) filterSelect.addEventListener('change', applyFiltersAndSort); if (toggleBtn) toggleBtn.addEventListener('click', toggleView); if (exportJsonBtn) exportJsonBtn.addEventListener('click', exportToJSON); if (exportCsvBtn) exportCsvBtn.addEventListener('click', exportToCSV); if (modeGridBtn) modeGridBtn.addEventListener('click', function() { viewMode = 'grid'; modeGridBtn.className = 'sbc-btn sbc-btn--accent'; if (modeGroupedBtn) modeGroupedBtn.className = 'sbc-btn'; var alldbBtn = document.getElementById('sbc-mode-alldb'); if (alldbBtn) alldbBtn.className = 'sbc-btn'; var actBtn = document.getElementById('sbc-mode-activity'); if (actBtn) actBtn.className = 'sbc-btn'; // v1.2.14-fix13: 用 applyFiltersAndSort 替代 renderCardView,确保搜索词清空后 filteredBadges 重新计算 applyFiltersAndSort(); }); if (modeGroupedBtn) modeGroupedBtn.addEventListener('click', function() { viewMode = 'grouped'; modeGroupedBtn.className = 'sbc-btn sbc-btn--accent'; if (modeGridBtn) modeGridBtn.className = 'sbc-btn'; var alldbBtn = document.getElementById('sbc-mode-alldb'); if (alldbBtn) alldbBtn.className = 'sbc-btn'; var actBtn = document.getElementById('sbc-mode-activity'); if (actBtn) actBtn.className = 'sbc-btn'; var unifiedBtn = document.getElementById('sbc-mode-unified'); if (unifiedBtn) unifiedBtn.className = 'sbc-btn'; // v1.2.14-fix13: 用 applyFiltersAndSort 替代 renderCardView,确保搜索词清空后 filteredBadges 重新计算 applyFiltersAndSort(); }); // v1.2.4: 统一游戏汇总视图 var unifiedBtn = document.getElementById('sbc-mode-unified'); if (unifiedBtn) unifiedBtn.addEventListener('click', function() { viewMode = 'unified'; unifiedBtn.className = 'sbc-btn sbc-btn--accent'; if (modeGridBtn) modeGridBtn.className = 'sbc-btn'; if (modeGroupedBtn) modeGroupedBtn.className = 'sbc-btn'; var alldbBtn2 = document.getElementById('sbc-mode-alldb'); if (alldbBtn2) alldbBtn2.className = 'sbc-btn'; var actBtn2 = document.getElementById('sbc-mode-activity'); if (actBtn2) actBtn2.className = 'sbc-btn'; currentPage = 1; if (!cardDbLoaded && !cardDbLoading) { renderCardView(); loadCardDatabase().then(function() { if (cardDbLoaded) renderCardView(); }); } else { renderCardView(); } }); // v1.2.1: 全部卡牌游戏视图 var alldbBtn = document.getElementById('sbc-mode-alldb'); if (alldbBtn) alldbBtn.addEventListener('click', function() { viewMode = 'alldb'; alldbBtn.className = 'sbc-btn sbc-btn--accent'; if (modeGridBtn) modeGridBtn.className = 'sbc-btn'; if (modeGroupedBtn) modeGroupedBtn.className = 'sbc-btn'; var actBtn = document.getElementById('sbc-mode-activity'); if (actBtn) actBtn.className = 'sbc-btn'; var unifiedBtn2 = document.getElementById('sbc-mode-unified'); if (unifiedBtn2) unifiedBtn2.className = 'sbc-btn'; currentPage = 1; if (!cardDbLoaded && !cardDbLoading) { renderCardView(); // 先显示加载提示 loadCardDatabase().then(function() { if (cardDbLoaded) renderCardView(); }); } else { renderCardView(); } }); // v1.2.3: 活动勋章视图 var actBtn = document.getElementById('sbc-mode-activity'); if (actBtn) actBtn.addEventListener('click', function() { viewMode = 'activity'; actBtn.className = 'sbc-btn sbc-btn--accent'; if (modeGridBtn) modeGridBtn.className = 'sbc-btn'; if (modeGroupedBtn) modeGroupedBtn.className = 'sbc-btn'; var alldbBtn3 = document.getElementById('sbc-mode-alldb'); if (alldbBtn3) alldbBtn3.className = 'sbc-btn'; var unifiedBtn3 = document.getElementById('sbc-mode-unified'); if (unifiedBtn3) unifiedBtn3.className = 'sbc-btn'; currentPage = 1; if (!cardDbLoaded && !cardDbLoading) { classifyBadges(); renderCardView(); loadCardDatabase().then(function() { if (cardDbLoaded) { classifyBadges(); renderCardView(); } }); } else { classifyBadges(); renderCardView(); } }); if (analysisBtn) { analysisBtn.addEventListener('click', async function() { if (!analysisVisible) { if (Object.keys(marketPrices).length === 0) { computeValueScores(); toggleAnalysisPanel(); loadMarketPrices().catch(function(e) { console.warn('[SBC-Pro] 市场价格加载失败:', e); }); } else { computeValueScores(); toggleAnalysisPanel(); } } else { toggleAnalysisPanel(); } }); } // v1.2.10: 工具栏上的"加载价格"按钮 var toolbarFetchBtn = document.getElementById('sbc-toolbar-fetch-prices'); if (toolbarFetchBtn) { toolbarFetchBtn.addEventListener('click', function() { if (marketLoading || _marketLoadPromise) { showToast('市场价格正在加载中,请稍候…', 'info'); return; } toolbarFetchBtn.disabled = true; var origHtml = toolbarFetchBtn.innerHTML; toolbarFetchBtn.innerHTML = ' 加载中'; loadMarketPrices().catch(function(e) { showToast('加载失败: ' + (e && e.message || '未知错误'), 'error'); }).then(function() { toolbarFetchBtn.disabled = false; toolbarFetchBtn.innerHTML = origHtml; }); }); } document.addEventListener('click', function(e) { if (e.target.id === 'sbc-fetch-prices-btn' || e.target.id === 'sbc-fetch-prices-btn-2') { if (marketLoading || _marketLoadPromise) { showToast('市场价格正在加载中,请稍候…', 'info'); return; } loadMarketPrices().catch(function(err) { showToast('市场价格加载失败: ' + err.message, 'error'); }); } }); GM_registerMenuCommand('切换卡片/列表视图', toggleView); GM_registerMenuCommand('切换汇总/分组模式', function() { viewMode = viewMode === 'grid' ? 'grouped' : 'grid'; var gb = document.getElementById('sbc-mode-grid'), gpb = document.getElementById('sbc-mode-grouped'); if (gb) gb.className = viewMode === 'grid' ? 'sbc-btn sbc-btn--accent' : 'sbc-btn'; if (gpb) gpb.className = viewMode === 'grouped' ? 'sbc-btn sbc-btn--accent' : 'sbc-btn'; renderCardView(); }); // v1.2.1: 全部卡牌游戏视图 GM_registerMenuCommand('切换全部卡牌游戏视图 (v1.2.1)', function() { viewMode = 'alldb'; var gb = document.getElementById('sbc-mode-grid'); var gpb = document.getElementById('sbc-mode-grouped'); var ab = document.getElementById('sbc-mode-alldb'); if (gb) gb.className = 'sbc-btn'; if (gpb) gpb.className = 'sbc-btn'; if (ab) ab.className = 'sbc-btn sbc-btn--accent'; currentPage = 1; if (!cardDbLoaded && !cardDbLoading) { renderCardView(); loadCardDatabase().then(function() { if (cardDbLoaded) renderCardView(); }); } else { renderCardView(); } }); GM_registerMenuCommand('加载卡牌数据库 (v1.2.2)', function() { if (cardDbLoaded) { showToast('卡牌数据库已加载', 'info'); return; } loadCardDatabase().then(function() { updateCardMaxLevelBadges(); renderCardView(); }); }); // v1.2.3: 活动勋章全量对比 GM_registerMenuCommand('活动勋章对比 (v1.2.3)', function() { viewMode = 'activity'; var gb = document.getElementById('sbc-mode-grid'); var gpb = document.getElementById('sbc-mode-grouped'); var ab = document.getElementById('sbc-mode-alldb'); var ac = document.getElementById('sbc-mode-activity'); if (gb) gb.className = 'sbc-btn'; if (gpb) gpb.className = 'sbc-btn'; if (ab) ab.className = 'sbc-btn'; if (ac) ac.className = 'sbc-btn sbc-btn--accent'; classifyBadges(); if (!cardDbLoaded && !cardDbLoading) { renderCardView(); loadCardDatabase().then(function() { if (cardDbLoaded) { classifyBadges(); renderCardView(); } }); } else { renderCardView(); } }); // v1.2.4: 统一游戏汇总视图 GM_registerMenuCommand('统一游戏汇总 (v1.2.4)', function() { viewMode = 'unified'; var gb = document.getElementById('sbc-mode-grid'); var gpb = document.getElementById('sbc-mode-grouped'); var ab = document.getElementById('sbc-mode-alldb'); var ac = document.getElementById('sbc-mode-activity'); var ub = document.getElementById('sbc-mode-unified'); if (gb) gb.className = 'sbc-btn'; if (gpb) gpb.className = 'sbc-btn'; if (ab) ab.className = 'sbc-btn'; if (ac) ac.className = 'sbc-btn'; if (ub) ub.className = 'sbc-btn sbc-btn--accent'; if (!cardDbLoaded && !cardDbLoading) { renderCardView(); loadCardDatabase().then(function() { if (cardDbLoaded) renderCardView(); }); } else { renderCardView(); } }); GM_registerMenuCommand('打开分析中心 (Pro)', toggleAnalysisPanel); GM_registerMenuCommand('刷新市场价格 (Pro)', loadMarketPrices); GM_registerMenuCommand('导出 JSON (Pro)', exportToJSON); GM_registerMenuCommand('导出 CSV (Pro)', exportToCSV); GM_registerMenuCommand('键盘快捷键帮助 (v1.3.1)', function() { showToast('/ 搜索 ←→ 翻页 1 总览 2 汇总 3 分组 4 活动 5 全部卡牌 Esc 关闭', 'info', { duration: 8000, stack: false, html: true }); }); // v1.3.1: 注册键盘快捷键 KbdShortcuts.init(); // v1.3.1: 延迟提示快捷键用法 setTimeout(function() { showToast('快捷键: /搜索 ←→翻页 1-5切换视图 Esc关闭', 'info', { duration: 5000, stack: false, html: true }); }, 1500); // v1.3.5: 绑定封面降级事件委托(@require sglv-cover-fallback.lib.js) // 在 document.body 上全局委托,覆盖卡片网格 + 详情浮窗(浮窗 appendChild 到 body) if (window.SGLVCoverFallback) { window.SGLVCoverFallback.bindErrorHandler(document.body); } else { console.warn('[SBC-Pro] SGLVCoverFallback 未加载 — 封面降级不可用 (请检查 @require sglv-cover-fallback.lib.js)'); } console.log('[SBC-Pro] 初始化完成,共 ' + allBadges.length + ' 个徽章,' + craftableBadges.length + ' 个可合卡'); showToast('已加载 ' + allBadges.length + ' 个徽章,' + craftableBadges.length + ' 个可合卡 (Pro v1.3.5)', 'success'); // v1.2.1: 后台异步预加载卡牌数据库(不阻塞 UI) loadCardDatabase().then(function() { updateCardMaxLevelBadges(); // v1.2.14-fix9: 数据库加载后重新计算可合卡(已合成徽章现在能正确识别) updateCraftableBadges(); // 如果用户还在普通视图,更新统计栏 if (viewMode !== 'alldb') { var statsEl = document.querySelector('.sbc-stats'); if (statsEl && statsEl.parentNode) { var newStats = createStats(); statsEl.parentNode.replaceChild(newStats, statsEl); } } }).catch(function(e) { console.warn('[SBC-Pro] 后台预加载卡牌数据库失败(不影响正常使用):', e.message); }); } // ============================================================ // v1.3.6: AI 辅助函数 (参考 SGLV 2.9.84 callAiApi / safeParseAiJson) // v1.3.7: 提示词模板远程加载 (参考 SGLV 2.9.84 loadPromptTemplate) // ============================================================ // v1.3.7: AI 徽章洞察提示词远程模板 URL var AI_BADGE_INSIGHT_PROMPT_URL = 'https://cdn.jsdelivr.net/gh/SmallRob/steam-namespace@main/prompts/ai-badge-insight-template.json'; // v1.3.7: 内置徽章洞察提示词 fallback (远程加载失败时使用) var FALLBACK_BADGE_INSIGHT_PROMPT = '# 角色\n' + '你是一位 Steam 徽章与卡牌市场投资顾问, 精通卡牌合成、徽章升级、市场套利策略和 Steam 经济系统。\n\n' + '# 任务\n' + '基于玩家的 Steam 徽章库和卡牌市场数据, 生成综合市场洞察报告和投资建议。\n\n' + '# 输入数据\n' + '- 徽章总数: {TOTAL_BADGES} (已完成 {COMPLETED_BADGES} · 进行中 {IN_PROGRESS_BADGES} · 未解锁 {LOCKED_BADGES})\n' + '- 总经验值: {TOTAL_XP}\n' + '- 可掉落卡牌: {DROPS} 张\n' + '- 已估值游戏: {PRICED_APPS} 个\n' + '- 卡牌市场总估值: {CURRENCY}{MARKET_VALUE}\n\n' + '## 价值排行 Top 5\n{VALUE_RANKING}\n\n' + '## 游戏卡牌详情 (Top {TOP_GAMES_COUNT})\n{GAMES_DATA}\n\n' + '## 交易推荐\n{TRADE_RECS}\n\n' + '# 输出格式\n' + '严格按以下 JSON 格式输出, 不要输出任何其他文字。\n' + '{\n' + ' "summary": "3-4句话市场总览, 评价徽章库整体价值、合成潜力、套利空间",\n' + ' "dimensions": {\n' + ' "value": 0-100,\n' + ' "potential": 0-100,\n' + ' "liquidity": 0-100,\n' + ' "risk": 0-100,\n' + ' "efficiency": 0-100\n' + ' },\n' + ' "recommendations": [\n' + ' {"name": "游戏名称", "action": "买入|卖出|持有|合成", "reason": "1-2句话具体建议理由, 包含数据支撑"}\n' + ' ],\n' + ' "marketTrend": "1-2句话市场趋势分析",\n' + ' "riskAssessment": "1-2句话风险评估",\n' + ' "bestStrategy": "1句话最优策略建议"\n' + '}\n\n' + '# 要求\n' + '- recommendations 数组包含 4-6 条建议, 优先选择价值最高或最有套利空间的游戏\n' + '- action 只能是: 买入(补全卡牌合成徽章)、卖出(出售多余卡牌获利)、持有(暂不操作)、合成(立即合成徽章升级)\n' + '- dimensions 各维度 0-100: value(整体价值)、potential(升级潜力)、liquidity(市场流动性)、risk(风险程度)、efficiency(性价比)\n' + '- reason 必须包含具体数据支撑\n' + '- 语言简洁有力, 避免空话'; // v1.3.7: 统一提示词模板加载器 (带缓存 + fallback, 参考 SGLV loadPromptTemplate) var _promptCache = {}; function loadPromptTemplate(url, fallback) { if (_promptCache[url]) return Promise.resolve(_promptCache[url]); return new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 15000, onload: function(resp) { try { var data = JSON.parse(resp.responseText); if (data.prompt && data.prompt.trim()) { _promptCache[url] = data.prompt.trim(); resolve(_promptCache[url]); } else if (fallback) { _promptCache[url] = fallback; resolve(fallback); } else { reject(new Error('提示词模板内容为空')); } } catch (e) { if (fallback) { _promptCache[url] = fallback; resolve(fallback); } else reject(new Error('提示词模板解析失败')); } }, onerror: function() { if (fallback) { _promptCache[url] = fallback; resolve(fallback); } else reject(new Error('提示词模板网络请求失败')); }, ontimeout: function() { if (fallback) { _promptCache[url] = fallback; resolve(fallback); } else reject(new Error('提示词模板请求超时')); } }); }); } // v1.3.7: 获取徽章洞察提示词模板 (远程加载 + fallback) function getBadgeInsightPromptTemplate() { return loadPromptTemplate(AI_BADGE_INSIGHT_PROMPT_URL, FALLBACK_BADGE_INSIGHT_PROMPT); } // 统一 AI API POST 调用 (兼容 OpenAI / DeepSeek 格式) function callAiApi(prompt, opts) { opts = opts || {}; var temperature = opts.temperature != null ? opts.temperature : 0.4; var timeout = opts.timeout != null ? opts.timeout : CONFIG.AI_API_TIMEOUT; var maxTokens = opts.maxTokens != null ? opts.maxTokens : CONFIG.AI_API_MAX_TOKENS; var apiKey = getStoredAiApiKey(); if (!apiKey) return Promise.reject(new Error('未配置 AI API Key,请在设置中配置')); var apiUrl = getStoredAiApiUrl(); var model = getStoredAiModel(); return new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: 'POST', url: apiUrl, timeout: timeout, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey }, data: JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], temperature: temperature, max_tokens: maxTokens }), onload: function(resp) { if (resp.status < 200 || resp.status >= 300) { var errDetail = ''; try { var errData = JSON.parse(resp.responseText); errDetail = (errData && errData.error && errData.error.message) || (errData && errData.message) || (errData && errData.error) || ''; } catch (e) { errDetail = (resp.responseText || '').slice(0, 200); } reject(new Error('AI API HTTP ' + resp.status + (errDetail ? ': ' + errDetail : ''))); return; } try { var respData = JSON.parse(resp.responseText); var choice = (respData.choices && respData.choices[0]) || {}; var content = (choice.message && choice.message.content) || ''; if (!content) { reject(new Error('AI 返回为空 (finishReason: ' + (choice.finish_reason || '') + ')')); return; } resolve(content); } catch (e) { reject(e); } }, onerror: function() { reject(new Error('AI API 网络错误')); }, ontimeout: function() { reject(new Error('AI API 请求超时 (' + (timeout / 1000) + 's)')); } }); }); } // 修复被截断的 JSON (AI 响应因 max_tokens 限制被截断) function repairTruncatedJson(str) { var s = str; var inStr = false, esc = false; var stk = []; var lastSafeBoundary = -1; for (var i = 0; i < s.length; i++) { var c = s[i]; if (esc) { esc = false; continue; } if (c === '\\') { esc = true; continue; } if (c === '"') { inStr = !inStr; continue; } if (inStr) continue; if (c === '{' || c === '[') stk.push(c); else if (c === '}' || c === ']') { if (stk.length) { stk.pop(); if (stk.length > 0 && stk[stk.length - 1] === '[') lastSafeBoundary = i + 1; } } } if (inStr) s += '"'; s = s.replace(/[\s]+$/, ''); if (s.length > 0) { var lastCh = s[s.length - 1]; if (lastCh !== '}' && lastCh !== ']' && lastCh !== ',') { if (lastSafeBoundary >= 0) s = s.slice(0, lastSafeBoundary); } } s = s.replace(/,\s*$/, ''); inStr = false; esc = false; var finalStk = []; for (var j = 0; j < s.length; j++) { var ch = s[j]; if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '"') { inStr = !inStr; continue; } if (inStr) continue; if (ch === '{' || ch === '[') finalStk.push(ch); else if (ch === '}' || ch === ']') { if (finalStk.length) finalStk.pop(); } } while (finalStk.length > 0) { var open = finalStk.pop(); s += (open === '{' ? '}' : ']'); } return s; } // 鲁棒 JSON 解析 (处理 AI 返回的 markdown/截断/未转义) function safeParseAiJson(content, expectKey) { if (!content || typeof content !== 'string') throw new Error('AI 返回内容为空'); var cleaned = content.replace(/[\s\S]*?<\/think>/gi, '').trim(); cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim(); var openChar = expectKey && expectKey.startsWith('[') ? '[' : '{'; var closeChar = openChar === '{' ? '}' : ']'; var firstIdx = cleaned.indexOf(openChar); if (firstIdx === -1) throw new Error('AI 返回中找不到 JSON 起始符'); var fullContent = cleaned.slice(firstIdx); var depth = 0, inString = false, escape = false, endIdx = -1; for (var i = firstIdx; i < cleaned.length; i++) { var ch = cleaned[i]; if (escape) { escape = false; continue; } if (ch === '\\') { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === openChar) depth++; else if (ch === closeChar) { depth--; if (depth === 0) { endIdx = i + 1; break; } } } if (endIdx === -1) { try { var repaired = repairTruncatedJson(fullContent); var result = JSON.parse(repaired); if (result && typeof result === 'object') result._truncated = true; return result; } catch (eRepair) { var m = cleaned.match(/\{[\s\S]*\}/); if (m) endIdx = m.index + m[0].length; else throw new Error('AI 返回中找不到 JSON 闭合符 (疑似响应被截断)'); } } var jsonStr = cleaned.slice(firstIdx, endIdx); jsonStr = jsonStr.replace(/[\s,]+$/, ''); jsonStr = jsonStr.replace(/^\s*\/\/.*$/gm, ''); jsonStr = jsonStr.replace(/\/\*[\s\S]*?\*\//g, ''); var quoteCount = (jsonStr.match(/"/g) || []).length; if (quoteCount % 2 !== 0) jsonStr = jsonStr.replace(/"([^"]*)$/, '$1'); jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1'); jsonStr = jsonStr.replace(/'([a-zA-Z_]\w*)'(\s*:)/g, '"$1"$2'); try { return JSON.parse(jsonStr); } catch (e1) { try { var fixed = jsonStr.replace(/[\u0000-\u001F\u007F]/g, function(m) { return (m === '\n' || m === '\r' || m === '\t') ? m : ''; }); return JSON.parse(fixed); } catch (e2) { try { var repaired2 = repairTruncatedJson(fullContent); var result2 = JSON.parse(repaired2); if (result2 && typeof result2 === 'object') result2._truncated = true; return result2; } catch (e3) { throw new Error('AI 返回 JSON 解析失败: ' + e1.message.slice(0, 80)); } } } } // ============================================================ // v1.3.6: 热门市场物品 (/market/popular API) // ============================================================ // v1.5.0: 读取热门市场缓存 (IDB 优先, GM 回退) async function readPopularMarketCache() { if (IDB.isReady()) { var data = await IDB.get('popularMarket', CONFIG.POPULAR_MARKET_CACHE_KEY); if (data) return data; } try { var raw = GM_getValue(CONFIG.POPULAR_MARKET_CACHE_KEY, ''); if (!raw) return null; var obj = JSON.parse(raw); if (obj.expires && Date.now() > obj.expires) return null; // 迁移到 IDB if (IDB.isReady()) await IDB.set('popularMarket', CONFIG.POPULAR_MARKET_CACHE_KEY, obj.data, CONFIG.POPULAR_MARKET_CACHE_TTL); return obj.data; } catch (e) { return null; } } // v1.5.0: 写入热门市场缓存 (IDB 优先, GM 回退) async function writePopularMarketCache(data) { if (IDB.isReady()) { await IDB.set('popularMarket', CONFIG.POPULAR_MARKET_CACHE_KEY, data, CONFIG.POPULAR_MARKET_CACHE_TTL); } try { GM_setValue(CONFIG.POPULAR_MARKET_CACHE_KEY, JSON.stringify({ data: data, expires: Date.now() + CONFIG.POPULAR_MARKET_CACHE_TTL })); } catch (e) {} } // 调用 /market/popular 获取单个热门物品 function fetchOnePopularItem() { // v1.3.7: 改用 gmFetchJson 替代原始 GM_xmlhttpRequest, 获得 429 重试 + 指数退避 return gmFetchJson('https://steamcommunity.com/market/popular', { timeout: 15000, retries: 2, headers: { 'Accept': 'application/json' } }); } // 轮询获取多个热门物品 (/market/popular 每次返回1个,轮询收集) async function fetchPopularMarketItems(onProgress) { var collected = []; var seenKeys = {}; // v1.5.0: 异步读取缓存 (IDB 优先) var cached = await readPopularMarketCache(); if (cached && cached.length > 0) { collected = cached; cached.forEach(function(item) { if (item.hashName) seenKeys[item.hashName] = true; }); } var pollCount = CONFIG.POPULAR_MARKET_POLL_COUNT; var consecutiveFails = 0; // v1.3.7: 连续失败计数 var MAX_CONSECUTIVE_FAILS = CONFIG.POPULAR_MARKET_MAX_CONSECUTIVE_FAILS; // v1.3.7: 连续失败上限 for (var i = 0; i < pollCount; i++) { try { var data = await fetchOnePopularItem(); var item = parsePopularItem(data); if (item && item.hashName && !seenKeys[item.hashName]) { seenKeys[item.hashName] = true; collected.unshift(item); // 新物品插入头部 } consecutiveFails = 0; // 成功则重置 if (onProgress) onProgress(i + 1, pollCount, collected.length); } catch (e) { consecutiveFails++; console.warn('[SBC] /market/popular 请求失败 (' + consecutiveFails + '/' + MAX_CONSECUTIVE_FAILS + '):', e.message); // v1.3.7: 连续失败超限则提前终止, 避免无意义请求加剧限流 if (consecutiveFails >= MAX_CONSECUTIVE_FAILS) { console.warn('[SBC] /market/popular 连续失败 ' + MAX_CONSECUTIVE_FAILS + ' 次, 提前终止轮询'); break; } } if (i < pollCount - 1) { // v1.3.7: 市场价格正在加载时增大轮询间隔, 避免叠加请求触发 429 var interval = CONFIG.POPULAR_MARKET_POLL_INTERVAL; if (typeof marketLoading !== 'undefined' && marketLoading) { interval = CONFIG.POPULAR_MARKET_POLL_INTERVAL_BUSY; // 市场加载中: 5s 间隔 } await delay(interval); } } // 截断到展示上限 if (collected.length > CONFIG.POPULAR_MARKET_DISPLAY_COUNT) { collected = collected.slice(0, CONFIG.POPULAR_MARKET_DISPLAY_COUNT); } // v1.5.0: 异步写缓存 (IDB 优先) await writePopularMarketCache(collected); return collected; } // 解析 /market/popular 响应为标准化物品对象 function parsePopularItem(data) { if (!data || !data.success) return null; var listing = data.popular_listing; if (!listing) return null; var item = listing.item || {}; var assets = data.assets || {}; var appid = String(item.appid || ''); var classid = String(item.classid || ''); var hashName = item.market_hash_name || ''; var assetInfo = (assets[appid] && assets[appid][classid]) || {}; var name = assetInfo.name || hashName; var type = assetInfo.type || ''; var iconUrl = assetInfo.icon_url || ''; var priceText = listing.price || ''; var quantity = listing.quantity || 0; var timestamp = listing.timestamp || 0; // 判断是否为卡牌类物品 var isCard = (type.indexOf('Trading Card') !== -1) || (type.indexOf('卡牌') !== -1) || (hashName.indexOf('Trading Card') !== -1) || (appid === '753'); return { hashName: hashName, name: name, type: type, appid: appid, classid: classid, iconUrl: iconUrl ? 'https://community.cloudflare.steamstatic.com/economy/image/' + iconUrl + '/48fx48f' : '', priceText: priceText, quantity: quantity, timestamp: timestamp, isCard: isCard, marketUrl: hashName ? 'https://steamcommunity.com/market/listings/' + appid + '/' + encodeURIComponent(hashName) : '' }; } // 渲染热门市场物品标签页 // v1.5.0: 缓存读取改为异步,先展示 loading 再从 IDB 加载 function renderPopularMarket(content) { if (popularMarketLoading) { content.innerHTML = '' + '' + '正在获取全站热门市场物品…' + ''; return; } // v1.5.0: 首次打开且无数据时,异步从 IDB 缓存加载 if (popularMarketItems.length === 0 && !popularMarketError) { content.innerHTML = '' + '' + '正在从缓存加载热门市场数据…' + ''; readPopularMarketCache().then(function(cached) { if (cached && cached.length > 0) { popularMarketItems = cached; if (analysisActiveTab === 'popular') renderPopularMarket(content); } else { loadPopularMarket(); } }); return; } // v1.5.0: 增强错误提示 — 加载失败时显示醒目的错误信息 if (popularMarketError) { content.innerHTML = '' + '⚠ 热门市场数据加载失败' + '' + escapeHtml(popularMarketError) + '' + '可能原因: Steam 服务器繁忙(429 限流)或网络连接问题' + '' + '重试加载'; var retryBtn = document.getElementById('sbc-popular-retry'); if (retryBtn) retryBtn.addEventListener('click', function() { popularMarketError = null; loadPopularMarket(); }); return; } var items = popularMarketItems || []; var cardCount = items.filter(function(i) { return i.isCard; }).length; var totalQty = items.reduce(function(s, i) { return s + (i.quantity || 0); }, 0); var listHtml = items.length > 0 ? items.map(function(item, idx) { var rankCls = idx === 0 ? ' style="background:var(--sbc-gold);color:#1b2838;"' : idx === 1 ? ' style="background:#b8c3cc;color:#1b2838;"' : idx === 2 ? ' style="background:#cd7f32;color:#fff;"' : ''; var cardTag = item.isCard ? '卡牌' : ''; var typeText = item.type ? escapeHtml(item.type) : ''; var qtyText = item.quantity > 0 ? '' + item.quantity + ' 件' : ''; var priceText = item.priceText ? '' + escapeHtml(item.priceText) + '' : ''; var nameHtml = item.marketUrl ? '' + escapeHtml(item.name) + '' : escapeHtml(item.name); var imgHtml = item.iconUrl ? '' : '🎮'; return '' + '' + (idx + 1) + '' + imgHtml + '' + '' + nameHtml + ' ' + cardTag + '' + '' + (typeText ? '' + typeText + '' : '') + qtyText + '' + '' + priceText + ''; }).join('') : '暂无热门市场数据,点击刷新获取'; content.innerHTML = '' + '全站热门市场物品排行 · 数据来源 /market/popular API' + '' + (items.length > 0 ? '刷新热门' : '获取热门') + '' + '' + (items.length > 0 ? '' + '热门物品' + items.length + '已收集' + '卡牌物品' + cardCount + '交易卡牌类' + '总挂单量' + totalQty.toLocaleString() + '在售数量' + '' : '') + '' + listHtml + '' + '提示: /market/popular 每次返回1个当前热门物品,多次轮询收集排行 · 缓存 ' + (CONFIG.POPULAR_MARKET_CACHE_TTL / 60000) + ' 分钟'; var refreshBtn = document.getElementById('sbc-popular-refresh'); if (refreshBtn) refreshBtn.addEventListener('click', function() { loadPopularMarket(); }); } // 加载热门市场物品 // v1.5.0: 增强错误提示 — 区分网络错误和限流错误 async function loadPopularMarket() { popularMarketLoading = true; popularMarketError = null; renderPopularMarket(document.getElementById('sbc-ap-content')); try { var items = await fetchPopularMarketItems(function(done, total, collected) { var content = document.getElementById('sbc-ap-content'); if (content && analysisActiveTab === 'popular') { content.innerHTML = '' + '' + '正在轮询热门物品… (' + done + '/' + total + ') 已收集 ' + collected + ' 个' + ''; } }); popularMarketItems = items; if (!items || items.length === 0) { popularMarketError = '未获取到任何热门市场数据(可能 Steam 服务器暂时不可用)'; } } catch (e) { var msg = e.message || '获取热门市场数据失败'; if (msg.indexOf('429') !== -1 || msg.indexOf('rate') !== -1) { popularMarketError = 'Steam 市场请求被限流(429),请稍后再试'; } else if (msg.indexOf('timeout') !== -1 || msg.indexOf('Timeout') !== -1) { popularMarketError = '请求超时,Steam 服务器响应缓慢'; } else { popularMarketError = msg; } } finally { popularMarketLoading = false; if (analysisActiveTab === 'popular') renderPopularMarket(document.getElementById('sbc-ap-content')); } } // ============================================================ // v1.3.6: AI 洞察分析 (参考 SGLV 2.9.84 triggerAiMarketInsight) // ============================================================ // 构建 AI 洞察提示词 // v1.3.7: 构建AI洞察提示词 (远程模板加载 + 占位符替换) async function buildAiInsightPrompt() { // 收集徽章/卡牌市场数据 var completed = allBadges.filter(function(b) { return b.isCompleted; }).length; var inProgress = allBadges.filter(function(b) { return !b.isCompleted && !b.isLocked; }).length; var locked = allBadges.filter(function(b) { return b.isLocked; }).length; var totalXp = allBadges.reduce(function(s, b) { return s + (b.xp || 0); }, 0); var drops = allBadges.reduce(function(s, b) { return s + (b.drops || 0); }, 0); // 市场价格数据 var pricedApps = 0, marketVal = 0, seenApps = {}; var gameMarketData = []; allBadges.forEach(function(b) { if (!b.appId || seenApps[b.appId]) return; seenApps[b.appId] = true; var mp = getMarketPriceForApp(b.appId); if (mp && mp.totalCost > 0) { pricedApps++; marketVal += mp.totalCost; gameMarketData.push({ appId: b.appId, name: b.name || ('App ' + b.appId), cardCount: mp.cardCount || 0, totalCost: mp.totalCost, avgPrice: mp.avgPrice || 0, medianPrice: mp.medianPrice || 0, level: b.level || 0, xp: b.xp || 0, drops: b.drops || 0, isCompleted: b.isCompleted }); } }); // 按总价值排序取 Top 10 gameMarketData.sort(function(a, b) { return b.totalCost - a.totalCost; }); var topGames = gameMarketData.slice(0, 10); // 价值评分 Top 5 var valuableBadges = getValuableBadges(5); var valueData = valuableBadges.map(function(vb) { var b = vb.badge || {}; return '- ' + (b.name || 'App ' + b.appId) + ': 价值分=' + (vb.score || 0).toFixed(1) + ', 等级Lv.' + (b.level || 0) + ', XP=' + (b.xp || 0); }).join('\n') || '暂无'; // v1.3.7: 获取用户当前货币符号 (如 ₹/$/¥ 等, 默认 ¥) var curSym = (primaryCurrency && primaryCurrency.symbol) ? primaryCurrency.symbol : '¥'; // 构建游戏卡牌详情文本 var gamesDataStr = topGames.map(function(g) { return '- ' + g.name + ' (AppID:' + g.appId + '): ' + g.cardCount + '张卡牌, 合成成本' + curSym + (g.totalCost / 100).toFixed(2) + ', 均价' + curSym + (g.avgPrice / 100).toFixed(2) + ', 当前等级Lv.' + g.level + ', XP=' + g.xp + (g.drops > 0 ? ', 剩余掉落' + g.drops : '') + (g.isCompleted ? ', 已满级' : ''); }).join('\n') || '暂无市场数据'; // 交易推荐 var tradeRecs = getTradeRecommendations(); var tradeStr = (tradeRecs && tradeRecs.length > 0) ? tradeRecs.slice(0, 5).map(function(r) { var b = r.badge || {}; return '- ' + (b.name || 'App ' + b.appId) + ': ' + ((r.reasons || []).join(', ') || '推荐') + (r.avgCardPrice > 0 ? ' (均价' + curSym + (r.avgCardPrice / 100).toFixed(2) + ')' : ''); }).join('\n') : '暂无推荐'; // v1.3.7: 远程加载提示词模板 (带 fallback) var template = await getBadgeInsightPromptTemplate(); // 占位符替换 var prompt = template .replace(/\{CURRENCY\}/g, curSym) .replace(/\{TOTAL_BADGES\}/g, allBadges.length) .replace(/\{COMPLETED_BADGES\}/g, completed) .replace(/\{IN_PROGRESS_BADGES\}/g, inProgress) .replace(/\{LOCKED_BADGES\}/g, locked) .replace(/\{TOTAL_XP\}/g, totalXp) .replace(/\{DROPS\}/g, drops) .replace(/\{PRICED_APPS\}/g, pricedApps) .replace(/\{MARKET_VALUE\}/g, (marketVal / 100).toFixed(2)) .replace(/\{VALUE_RANKING\}/g, valueData) .replace(/\{TOP_GAMES_COUNT\}/g, topGames.length) .replace(/\{GAMES_DATA\}/g, gamesDataStr) .replace(/\{TRADE_RECS\}/g, tradeStr); return prompt; } // 触发 AI 洞察分析 async function triggerAiBadgeInsight() { if (aiInsightLoading) return; var apiKey = getStoredAiApiKey(); if (!apiKey) { aiInsightError = '未配置 AI API Key,请在设置中配置'; renderAiInsight(document.getElementById('sbc-ap-content')); return; } aiInsightLoading = true; aiInsightError = null; aiInsightResult = null; renderAiInsight(document.getElementById('sbc-ap-content')); try { var prompt = await buildAiInsightPrompt(); var content = await callAiApi(prompt, { temperature: 0.4, timeout: CONFIG.AI_API_TIMEOUT, maxTokens: CONFIG.AI_API_MAX_TOKENS }); var result = safeParseAiJson(content, '{'); if (!result.summary || !Array.isArray(result.recommendations)) { throw new Error('AI 返回结构不完整'); } aiInsightResult = result; // v1.5.0: 缓存结果到 IDB (aiInsight 仓库) if (IDB.isReady()) { await IDB.set('aiInsight', CONFIG.AI_INSIGHT_CACHE_KEY, result, CONFIG.AI_INSIGHT_CACHE_TTL); } try { GM_setValue(CONFIG.AI_INSIGHT_CACHE_KEY, JSON.stringify({ data: result, expires: Date.now() + CONFIG.AI_INSIGHT_CACHE_TTL })); } catch (e) {} } catch (e) { aiInsightError = e.message || 'AI 洞察分析失败'; } finally { aiInsightLoading = false; if (analysisActiveTab === 'aiinsight') renderAiInsight(document.getElementById('sbc-ap-content')); } } // v1.5.0: 读取 AI 洞察缓存 (IDB 优先, GM 回退) async function readAiInsightCache() { if (IDB.isReady()) { var data = await IDB.get('aiInsight', CONFIG.AI_INSIGHT_CACHE_KEY); if (data) return data; } try { var raw = GM_getValue(CONFIG.AI_INSIGHT_CACHE_KEY, ''); if (!raw) return null; var obj = JSON.parse(raw); if (obj.expires && Date.now() > obj.expires) return null; // 迁移到 IDB if (IDB.isReady()) await IDB.set('aiInsight', CONFIG.AI_INSIGHT_CACHE_KEY, obj.data, CONFIG.AI_INSIGHT_CACHE_TTL); return obj.data; } catch (e) { return null; } } // v1.3.7: AI 洞察页面 SVG 图标 (替换 emoji, 提升视觉一致性) var AI_ICON = { robot: '', chart: '', trend: '', alert: '', target: '', buy: '', sell: '', hold: '', craft: '' }; function aiSectionIcon(iconKey, bgColor) { return '' + AI_ICON[iconKey] + ''; } // 渲染 AI 洞察分析标签页 function renderAiInsight(content) { if (!content) return; var apiKey = getStoredAiApiKey(); // 加载中 if (aiInsightLoading) { content.innerHTML = '' + '' + 'AI 正在分析徽章与卡牌市场数据…' + '分析可能需要 30-60 秒,请耐心等待' + ''; return; } // v1.5.0: 错误提示增强 — 醒目错误信息 if (aiInsightError) { content.innerHTML = '' + '⚠ AI 洞察分析失败' + '' + escapeHtml(aiInsightError) + '' + '' + '重新分析'; var retryBtn = document.getElementById('sbc-ai-retry'); if (retryBtn) retryBtn.addEventListener('click', triggerAiBadgeInsight); return; } // 未配置 API Key if (!apiKey) { content.innerHTML = '' + '未配置 AI API Key。请在设置中配置 AI 模型接口(API 地址、Key、模型名称)后使用 AI 洞察分析。' + '点击此处打开设置' + ''; var settingsLink = document.getElementById('sbc-ai-open-settings'); if (settingsLink) settingsLink.addEventListener('click', function() { openUserSettings(); }); return; } // 有结果 if (aiInsightResult) { var r = aiInsightResult; var dims = r.dimensions || {}; var dimConfig = [ { key: 'value', label: '整体价值', color: '#66c0f4' }, { key: 'potential', label: '升级潜力', color: '#a855f7' }, { key: 'liquidity', label: '流动性', color: '#4ade80' }, { key: 'risk', label: '风险程度', color: '#f59e0b' }, { key: 'efficiency', label: '性价比', color: '#ec4899' } ]; var dimHtml = dimConfig.map(function(d) { var val = Math.min(100, Math.max(0, Number(dims[d.key]) || 0)); return '' + '' + d.label + '' + '' + val + '' + '' + ''; }).join(''); var recHtml = (r.recommendations || []).map(function(rec) { var actionMap = { '买入': 'buy', '卖出': 'sell', '持有': 'hold', '合成': 'craft' }; var actionCls = actionMap[rec.action] || 'hold'; var actionIcon = AI_ICON[actionCls] || AI_ICON.hold; return '' + '' + actionIcon + '' + escapeHtml(rec.action || '持有') + '' + '' + '' + escapeHtml(rec.name || '') + '' + '' + escapeHtml(rec.reason || '') + '' + '' + ''; }).join(''); content.innerHTML = '' + '' + escapeHtml(r.summary || '') + '' + '' + '' + dimHtml + '' + '' + aiSectionIcon('chart', 'rgba(102,192,244,.15)') + '投资建议' + '' + recHtml + '' + (r.marketTrend ? '' + aiSectionIcon('trend', 'rgba(74,222,128,.15)') + '市场趋势' + escapeHtml(r.marketTrend) + '' : '') + (r.riskAssessment ? '' + aiSectionIcon('alert', 'rgba(245,158,11,.15)') + '风险评估' + escapeHtml(r.riskAssessment) + '' : '') + (r.bestStrategy ? '' + aiSectionIcon('target', 'rgba(139,92,246,.15)') + '最优策略' + escapeHtml(r.bestStrategy) + '' : '') + '重新分析'; var refreshBtn = document.getElementById('sbc-ai-refresh'); if (refreshBtn) refreshBtn.addEventListener('click', function() { aiInsightResult = null; aiInsightError = null; triggerAiBadgeInsight(); }); return; } // v1.5.0: 初始状态 - 异步从 IDB 加载缓存 content.innerHTML = '' + '' + '正在从缓存加载 AI 洞察结果…' + ''; readAiInsightCache().then(function(cached) { if (cached) { aiInsightResult = cached; if (analysisActiveTab === 'aiinsight') renderAiInsight(content); } else { // 无缓存,显示触发页 content.innerHTML = '' + '' + AI_ICON.robot + '' + 'AI 徽章市场洞察' + '' + '基于你的徽章库和卡牌市场数据,AI 将生成:' + '• 市场总览与价值评估' + '• 五维度评分(价值/潜力/流动性/风险/性价比)' + '• 个性化投资建议(买入/卖出/持有/合成)' + '• 市场趋势与风险分析' + '参考 SGLV 洞察页面提示词与分析架构' + '' + '开始 AI 分析' + ''; var triggerBtn = document.getElementById('sbc-ai-trigger'); if (triggerBtn) triggerBtn.addEventListener('click', triggerAiBadgeInsight); } }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();