// ==UserScript== // @name Steam 徽章卡片视图 Pro // @namespace https://github.com/steam-badges-card-view // @version 1.3.2 // @description 将Steam徽章页面改为卡片网格展示,支持社区市场价格、徽章价值分析、未解锁徽章预览、智能分析浮窗、可合卡勋章标记、全部卡牌游戏列表(含最大徽章等级)、鉴赏家推荐聚合。v1.3.2 借鉴 Trading Card Info 脚本优化:卡牌价格统计新增中位数/忽略最高价均价/预计税后总收入(利用已有_calculateSellerReceives精确手续费含可变发行商费率)、市场加载前新增社区登录状态前置检测(未登录时提前提示而非静默失败)、详情浮窗信息网格和价格表增加中位价与税后收入展示。v1.3.1: 性能优化(IntersectionObserver懒加载、DocumentFragment批量渲染、DOM引用缓存)与UI交互增强(视图切换动画、骨架屏、键盘快捷键、Toast通知、翻页平滑滚动)。v1.3.1 R2: 网络与缓存增强(concurrentPool滑动窗口+429自动降并发+指数退避、negCache负缓存30分钟TTL、gmFetchText/gmFetchJson带重试、多CDN回退、缓存版本控制)。 // @author SmallRob // @match https://steamcommunity.com/id/*/badges* // @match https://steamcommunity.com/profiles/*/badges* // @match https://store.steampowered.com/curator/* // @include https://store.steampowered.com/curator/* // @icon https://store.steampowered.com/favicon.ico // @connect steamcommunity.com // @connect steamcardexchange.net // @connect store.steampowered.com // @connect api.steampowered.com // @connect steampowered.com // @connect cdn.cloudflare.steamstatic.com // @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: 10 * 60 * 1000, MARKET_REQUEST_DELAY: 450, // v1.2.10: 600ms → 450ms(并发 3→4 后放慢些避免被限流) MARKET_REQUEST_CONCURRENCY: 4, // v1.2.10: 3 → 4 // v1.2.10: 性能优化 MARKET_RERENDER_THROTTLE_MS: 120, // 局部重渲染节流(rAF 兜底) MARKET_PAGE_START_DELAY: 250, // 页面渲染完成后等 250ms 再后台启动价格加载 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.2.16: 鉴赏家推荐聚合配置 CURATOR_CACHE_TTL: 12 * 60 * 60 * 1000, // 鉴赏家推荐缓存 12 小时 CURATOR_REQUEST_CONCURRENCY: 3, // 3 个并发 worker CURATOR_REQUEST_DELAY: 1000, // 请求间隔 1 秒 CURATOR_LIST_COUNT: 50, // 单次拉取推荐列表数量 CURATOR_BADGE_PAGE_MAX_APPS: 120, // 徽章页聚合维度最多拉取的游戏数(避免 API 过载) CURATOR_TOP_N: 5, // TOP N 展示数 // 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) 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: 2, // 市场缓存结构版本 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: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), 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 { transform: translateY(-5px); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(139, 92, 246, 0.2); border-color: var(--sbc-border-hover); } /* 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 8px 32px rgba(192, 160, 46, 0.25); } /* 自动合卡: 可合卡/合卡中/已完成 状态 */ .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; } .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; } .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 { transform: translateY(-2px); box-shadow: var(--sbc-shadow-hover); border-color: rgba(102, 192, 244, 0.3); } .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-toggle { color: var(--sbc-text-secondary); transition: transform var(--sbc-transition); } .sbc-game-group-toggle.collapsed { transform: rotate(-90deg); } /* v1.2.14-fix2: 分组视图详情按钮(打开大屏浮窗) */ .sbc-game-group-detail-btn { 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) 56px 70px minmax(140px, 200px) minmax(80px, 110px) 80px 56px; 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.2.14-fix2: 徽章行详情按钮 */ .sbc-badge-row-detail-btn { padding: 3px 8px; 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; } .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 6px; border-radius: 4px; } .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: transform 0.15s, 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 { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5); border-color: var(--sbc-accent); 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.2.14-fix2: 适配新增的详情按钮列) */ @media (max-width: 1200px) { .sbc-badge-row { grid-template-columns: 32px minmax(0, 1fr) 50px 60px minmax(110px, 1fr) 90px 70px 50px; } } @media (max-width: 800px) { .sbc-badge-row { grid-template-columns: 32px minmax(0, 1fr) 60px 80px 70px 50px; } .sbc-badge-row-cards, .sbc-badge-row-xp { 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: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); overflow: hidden; /* v1.2.14-fix6: 限制浮窗最大高度,超出部分滚动 */ max-height: calc(100vh - 80px); 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); } /* v1.2.14-fix14: 批量购买按钮(求购价按钮下方) */ .sbc-detail-batch-buy-btn { margin-top: 0; padding: 4px 12px; border-radius: 4px; font-size: 11px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #5ba83e, #3d8b2a); border: 1px solid rgba(255,255,255,0.15); cursor: pointer; transition: all var(--sbc-transition); display: inline-flex; align-items: center; gap: 4px; } .sbc-detail-batch-buy-btn:hover { background: linear-gradient(135deg, #6cba4a, #4a9d33); transform: translateY(-1px); } .sbc-detail-batch-buy-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .sbc-detail-batch-buy-wrap { display: flex; justify-content: flex-end; margin-top: 0; margin-bottom: 8px; } .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.2.14-fix2: 大屏浮窗变体(游戏详情) */ .sbc-detail-overlay--large { padding: 24px 16px; } .sbc-detail-panel--large { max-width: 1080px; max-height: calc(100vh - 48px); display: flex; flex-direction: column; } .sbc-detail-panel--large .sbc-detail-body { overflow-y: auto; flex: 1; padding: 16px 18px; } .sbc-detail-panel--large .sbc-detail-header { padding: 12px 18px; } .sbc-detail-panel--large .sbc-detail-header img { width: 52px; height: 52px; } .sbc-detail-panel--large .sbc-detail-title { font-size: 17px; } /* 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-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); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .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 { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; } .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-accent); } .sbc-detail-card-item img { width: 100%; max-width: 100px; height: auto; border-radius: 4px; margin-bottom: 6px; } .sbc-detail-card-name { font-size: 11px; color: var(--sbc-text-secondary); line-height: 1.3; word-break: break-word; } .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.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); } /* 统一游戏卡片内的卡牌网格(略小) */ .sbc-ugv-detail-section--cards .sbc-detail-cards-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 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: 84px; } /* v1.2.16: 鉴赏家推荐聚合样式 */ .sbc-curator-panel { margin: 16px 0; padding: 16px; background: var(--sbc-bg-card); border: 1px solid var(--sbc-border); border-radius: 10px; color: var(--sbc-text-primary); font-family: "Motiva Sans", Arial, sans-serif; } .sbc-curator-panel h2 { margin: 0 0 4px; font-size: 18px; color: var(--sbc-text-bright); } .sbc-curator-panel .sbc-curator-sub { font-size: 12px; color: var(--sbc-text-secondary); margin-bottom: 12px; } .sbc-curator-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-bottom: 14px; } .sbc-curator-stat { background: var(--sbc-bg-secondary); border-radius: 8px; padding: 10px; border: 1px solid var(--sbc-border); } .sbc-curator-stat-label { font-size: 11px; color: var(--sbc-text-secondary); text-transform: uppercase; letter-spacing: .5px; } .sbc-curator-stat-value { font-size: 20px; font-weight: 700; color: var(--sbc-text-bright); margin-top: 2px; } .sbc-curator-stat-desc { font-size: 11px; color: var(--sbc-text-secondary); margin-top: 2px; } .sbc-curator-dist { display: flex; gap: 6px; margin-top: 6px; } .sbc-curator-dist-seg { flex: 1; height: 10px; border-radius: 4px; } .sbc-curator-top { margin-bottom: 14px; } .sbc-curator-top-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; } .sbc-curator-top-item:hover { background: var(--sbc-bg-hover); } .sbc-curator-rank { width: 22px; height: 22px; border-radius: 50%; background: var(--sbc-bg-secondary); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: var(--sbc-accent); flex-shrink: 0; } .sbc-curator-top-item:nth-child(1) .sbc-curator-rank { background: var(--sbc-gold); color: #1b2838; } .sbc-curator-top-item:nth-child(2) .sbc-curator-rank { background: #b8c3cc; color: #1b2838; } .sbc-curator-top-item:nth-child(3) .sbc-curator-rank { background: #cd7f32; color: #fff; } .sbc-curator-top-name { flex: 1; font-size: 13px; color: var(--sbc-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sbc-curator-top-count { font-size: 12px; color: var(--sbc-accent); font-weight: 600; } .sbc-curator-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; } .sbc-curator-btn { background: var(--sbc-bg-secondary); color: var(--sbc-text-primary); border: 1px solid var(--sbc-border); border-radius: 6px; padding: 6px 12px; font-size: 12px; cursor: pointer; transition: all .15s; } .sbc-curator-btn:hover { background: var(--sbc-bg-hover); border-color: var(--sbc-accent); } .sbc-curator-btn.active { background: var(--sbc-accent); color: #1b2838; border-color: var(--sbc-accent); font-weight: 600; } .sbc-curator-list { display: flex; flex-direction: column; gap: 8px; } .sbc-curator-item { display: flex; gap: 12px; padding: 10px; background: var(--sbc-bg-secondary); border: 1px solid var(--sbc-border); border-radius: 8px; transition: border-color .15s; } .sbc-curator-item:hover { border-color: var(--sbc-border-hover); } .sbc-curator-item img.sbc-curator-cap { width: 120px; height: 45px; object-fit: cover; border-radius: 4px; flex-shrink: 0; background: #000; } .sbc-curator-item-body { flex: 1; min-width: 0; } .sbc-curator-item-name { font-size: 14px; color: var(--sbc-text-bright); font-weight: 600; } .sbc-curator-item-name a { color: var(--sbc-text-bright); text-decoration: none; } .sbc-curator-item-name a:hover { color: var(--sbc-accent); } .sbc-curator-rec-badge { display: inline-flex; align-items: center; gap: 4px; background: rgba(166, 111, 38, .25); color: var(--sbc-gold); border: 1px solid rgba(199, 160, 46, .5); border-radius: 12px; padding: 2px 8px; font-size: 11px; font-weight: 600; margin-left: 8px; vertical-align: middle; } .sbc-curator-rec-badge.not { background: rgba(231, 76, 60, .2); color: var(--sbc-danger); border-color: rgba(231, 76, 60, .5); } .sbc-curator-avatars { display: flex; align-items: center; gap: 4px; margin-top: 6px; flex-wrap: wrap; } .sbc-curator-avatar { width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--sbc-border); cursor: pointer; transition: transform .12s; } .sbc-curator-avatar:hover { transform: scale(1.15); border-color: var(--sbc-accent); } .sbc-curator-avatar-more { font-size: 11px; color: var(--sbc-text-secondary); padding: 0 4px; } .sbc-curator-status { font-size: 11px; padding: 2px 6px; border-radius: 4px; margin-left: 6px; } .sbc-curator-status--rec { background: rgba(92, 126, 16, .25); color: #a4d007; } .sbc-curator-status--not { background: rgba(231, 76, 60, .2); color: var(--sbc-danger); } .sbc-curator-progress { margin-top: 8px; font-size: 12px; color: var(--sbc-text-secondary); } .sbc-curator-empty { padding: 24px; text-align: center; color: var(--sbc-text-secondary); font-size: 13px; } /* 徽章页卡片上的鉴赏家推荐标记 */ .sbc-card-curator-badge { position: absolute; top: 6px; left: 6px; z-index: 3; display: inline-flex; align-items: center; gap: 3px; background: rgba(166, 111, 38, .85); color: #fff; border: 1px solid rgba(199, 160, 46, .8); border-radius: 10px; padding: 2px 7px; font-size: 10px; font-weight: 600; cursor: pointer; } .sbc-card-curator-badge:hover { background: rgba(199, 160, 46, .95); } /* 鉴赏家推荐浮窗(徽章页点击标记展开) */ .sbc-curator-popup { position: fixed; z-index: 10000; background: var(--sbc-bg-card); border: 1px solid var(--sbc-border-hover); border-radius: 8px; padding: 12px; box-shadow: 0 8px 24px rgba(0,0,0,.5); max-width: 320px; } .sbc-curator-popup-title { font-size: 13px; color: var(--sbc-text-bright); font-weight: 600; margin-bottom: 8px; } .sbc-curator-popup-list { display: flex; flex-direction: column; gap: 6px; } .sbc-curator-popup-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--sbc-text-primary); cursor: pointer; padding: 4px; border-radius: 4px; } .sbc-curator-popup-item:hover { background: var(--sbc-bg-hover); } .sbc-curator-popup-item img { width: 20px; height: 20px; border-radius: 50%; } /* ============================================================ 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%; } } /* --- 增强卡片 hover 微交互 --- */ .sbc-card { transition: transform 0.2s cubic-bezier(0.22, 0.61, 0.36, 1), box-shadow 0.2s ease, border-color 0.2s ease; will-change: transform; } .sbc-card:hover { transform: translateY(-4px) scale(1.015); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--sbc-accent); } .sbc-card:active { transform: translateY(-1px) scale(1.005); } .sbc-card-img-wrap img { transition: transform 0.3s ease, filter 0.3s ease; } .sbc-card:hover .sbc-card-img-wrap img { transform: scale(1.06); } .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; } `; // ============================================================ // 全局状态 // ============================================================ 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.2.16: 鉴赏家推荐聚合状态 // curatorRecs: { appId: { curators: [...], total: N, fetchedAt: ts } } let curatorRecs = {}; let curatorLoading = false; let curatorLoadProgress = { total: 0, done: 0 }; // 限流: 采用"预留位"模式,在 sleep 前更新 lastRequestTime 防止并发竞态 let curatorLastRequestTime = 0; // 共享默认 filter set 必须为 new Set 实例,防止跨组件引用共享 // 组件使用时需通过 new Set(curatorDefaultFilterSet) 复制独立副本 let curatorDefaultFilterSet = new Set(['all']); // ============================================================ // 工具函数 // ============================================================ 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/'; var SBC_COVER_PLACEHOLDER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 46 22'%3E%3Crect fill='%23141b32' width='46' height='22' rx='3'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.35em' fill='%23475569' font-size='8'%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.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); } }); } // --- 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.3.1: 性能增强工具集 // ============================================================ // --- LazyImageLoader: 基于 IntersectionObserver 的图片懒加载 --- // 用法: 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) { img.src = 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 { // Fallback: 不支持 IntersectionObserver 时直接加载 img.src = 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.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 存储),不会上传。' + '' + '' + '' + '' + '' + ''; document.body.appendChild(backdrop); var inputId = backdrop.querySelector('#sbc-input-steam-id'); var inputKey = backdrop.querySelector('#sbc-input-api-key'); 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 = ''; setStoredFallbackSteamId(''); setStoredApiKey(''); 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); 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(src); }); // v1.2.14-fix10: isCompleted 仅在 collected>=total 时为 true(恢复原逻辑) // 移除 fix9 错误逻辑(原把所有 level>0 的已合成徽章都标记为 isCompleted,导致 150 个全计入可合卡) // 可合卡判断改用 hasCraftButton(Steam 原生合卡按钮) 或 collected>=total var isCompleted = (collected >= total && total > 0) || hasCraftButton; var isLocked = !isCompleted && collected === 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 + ' 个徽章'); 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.3.1 R2: 缓存版本控制 — 版本不匹配时清除旧缓存 var CARD_DB_VER_KEY = 'sbc_card_db_cache_ver'; if (ensureCacheVersion(CARD_DB_VER_KEY, CONFIG.CARD_DB_CACHE_VERSION)) { try { GM_setValue(CONFIG.CARD_DB_CACHE_KEY, null); } catch (e) {} setCacheVersion(CARD_DB_VER_KEY, CONFIG.CARD_DB_CACHE_VERSION); console.log('[SBC-Pro] 卡牌数据库缓存版本更新,清除旧缓存'); } // 检查本地缓存 var cached = GM_getValue(CONFIG.CARD_DB_CACHE_KEY, null); if (cached && cached.timestamp && (Date.now() - cached.timestamp < CONFIG.CARD_DB_CACHE_TTL)) { console.log('[SBC-Pro] 使用卡牌数据库缓存(' + Object.keys(cached.data).length + ' 个游戏)'); return cached.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]] // current[0] = [appid, name], current[3] = [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 + ' 个游戏'); // 缓存到本地 GM_setValue(CONFIG.CARD_DB_CACHE_KEY, { timestamp: Date.now(), data: db }); return db; } catch (e) { console.error('[SBC-Pro] 卡牌数据库获取失败:', e); throw new Error('卡牌数据库获取失败: ' + e.message); } } /** * 异步加载卡牌数据库并更新 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: 在活动勋章静态数据中查找匹配项 */ function findActivityBadge(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; allBadges.forEach(function(badge) { if (!badge.appId) return; var maxLvl = getCardCountForApp(badge.appId); // v1.2.3: 卡牌数,非等级 if (maxLvl <= 0) return; var cardEl = document.querySelector('.sbc-card[data-index="' + 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 }] async function fetchOwnedGamesFromPage() { if (ownedGamesCache) return ownedGamesCache; var steamId = (typeof unsafeWindow !== 'undefined' && unsafeWindow.g_steamID) || ''; if (!steamId) { // 从当前 URL 提取 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 = await fmRequest(url); // 页面内嵌 rgGames 数组,格式: var rgGames = [{appid:123,name:"...",...}, ...]; var match = text.match(/var\s+rgGames\s*=\s*(\[.+?\]);/s); if (!match) { console.warn('[SBC-Pro] 游戏库页面未找到 rgGames 数据'); 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; } function readMarketCache(appId) { try { var raw = GM_getValue(getCacheKey(appId), null); if (!raw) return null; var data = JSON.parse(raw); // v1.3.1 R2: 缓存版本检查 if (!data.v || data.v < CONFIG.MARKET_CACHE_VERSION) return null; if (!data.updatedAt || Date.now() - data.updatedAt > CONFIG.MARKET_CACHE_TTL) return null; return data; } catch (e) { return null; } } function writeMarketCache(appId, data) { try { // v1.3.1 R2: 写入缓存版本号 data.v = CONFIG.MARKET_CACHE_VERSION; GM_setValue(getCacheKey(appId), JSON.stringify(data)); } 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 + '/64fx64f' : ''; 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); var name = (item.name || desc.market_name || desc.name || hashName).replace(/\s*\(Foil\)\s*$/i, '').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 async function _enrichBuyOrder(card) { try { var html = await _requestTextWithRetry(card.marketUrl, 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; } 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 : '未知错误', }); } } 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 || []; var withScore = appIds.map(function(appId) { var badge = null; for (var i = 0; i < allBadges.length; i++) { if (allBadges[i].appId === appId) { badge = allBadges[i]; break; } } 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; function scheduleIncrementalRender() { 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; try { if (typeof computeValueScores === 'function') computeValueScores(); // 局部更新:重新渲染 stats(估值/已加载数)+ 价值角标(已渲染的卡片元素) var statsEl = document.querySelector('.sbc-stats'); if (statsEl) { var newStats = createStats(); if (newStats) statsEl.replaceWith(newStats); } // 重新渲染每个卡片的 .sbc-card-market-badge / .sbc-card-value-badge updatePriceBadgesInPlace(); } catch (e) { console.warn('[SBC-Pro] scheduleIncrementalRender error:', e); } }; if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fire); else setTimeout(fire, 16); } /** * v1.2.10: 就地更新价格/价值角标(不重建卡片 DOM,避免丢失滚动位置 & 动画) * 角标结构: .sbc-card > .sbc-card-img-wrap > [..., .sbc-card-market-badge] * 仅在价格/价值就位时新增/更新;否则隐藏现有 badge。 */ 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; var mp = marketPrices[appId]; 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.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'; } } } catch (e) { /* 静默 */ } } /** * v1.2.10: 断点续传 —— 把"已完成/失败"清单写到 GM_setValue, * 下次刷新时跳过这些,避免每次都重头跑。 */ function readMarketResume() { try { var raw = GM_getValue(CONFIG.MARKET_RESUME_KEY, ''); if (!raw) return null; var data = JSON.parse(raw); if (!data || Date.now() - (data.savedAt || 0) > 24 * 60 * 60 * 1000) return null; // 24h 过期 return data; } catch (e) { return null; } } function writeMarketResume() { try { GM_setValue(CONFIG.MARKET_RESUME_KEY, JSON.stringify({ savedAt: Date.now(), done: Array.from(marketDone), success: marketProgress.successCount, total: marketProgress.total })); } catch (e) { /* 静默 */ } } function clearMarketResume() { 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 负缓存 */ async function _fetchMarketPricesImpl(appIds) { marketLoading = true; marketInflight.clear(); marketDone.clear(); marketConsecutiveFails = 0; marketPauseUntil = 0; // v1.2.10: 读取断点续传(本次 session 内已成功过的,跳过) var resume = readMarketResume(); if (resume && Array.isArray(resume.done)) { resume.done.forEach(function(id) { marketDone.add(String(id)); }); } // 1. 从缓存加载(GM_setValue 本地缓存) var toFetch = appIds.slice(); toFetch.forEach(function(appId) { var cached = readMarketCache(appId); if (cached) { marketPrices[appId] = cached; marketDone.add(String(appId)); } }); // 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() }; if (needFetch.length === 0) { marketLoading = false; console.log('[SBC-Pro] 市场价格:全部命中缓存(共 ' + toFetch.length + ' 款)'); return marketPrices; } console.log('[SBC-Pro] 市场价格:需请求 ' + needFetch.length + ' 款(总 ' + toFetch.length + ' 款,缓存命中 ' + (toFetch.length - needFetch.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: 成功后立即触发节流后的局部重渲染 if (succeeded) scheduleIncrementalRender(); return result; }; }); // v1.3.1 R2: 使用 concurrentPool 滑动窗口并发抓取 // 优势: 完成一个立即填入下一个(不等整批),429 自动降并发,指数退避重试 await concurrentPool(tasks, CONFIG.MARKET_REQUEST_CONCURRENCY, function(completed, total) { // 进度日志(每10个或完成时) 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'); 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; // 完成后清理断点 + 关闭进度条 clearMarketResume(); if (typeof showMarketProgress === 'function') showMarketProgress(null); return marketPrices; } 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) {} // 回退:用主货币 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.2.14-fix12: 无徽章图片时用 Steam 商店封面图兜底 var headImg = g.gameImg; var headImgHtml = ''; if (headImg) { headImgHtml = ''; } else if (g.appId) { var capsuleUrl2 = 'https://cdn.cloudflare.steamstatic.com/steam/apps/' + g.appId + '/capsule_236x174.jpg'; 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 ? '可合卡' : ''; // v1.2.16: 鉴赏家推荐角标(数据懒加载后才有,updateCuratorBadgesInPlace 会就地补齐) var curatorRec = badge.appId ? getCuratorRecsForApp(badge.appId) : null; var curatorBadgeHtml = (curatorRec && curatorRec.recommendedCount > 0) ? '★ ' + curatorRec.recommendedCount + ' 鉴赏家' : ''; card.innerHTML = '' + imgContent + (badge.level > 1 ? 'Lv.' + badge.level + '' : '') + '' + badge.xp + ' XP' + typeBadgeHtml + valueBadgeHtml + marketBadgeHtml + craftBadgeHtml + curatorBadgeHtml + '' + '' + '' + '' + 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); }); } // v1.2.16: 鉴赏家推荐角标点击(展开鉴赏家列表浮窗) var curatorBadgeEl = card.querySelector('.sbc-card-curator-badge'); if (curatorBadgeEl) { curatorBadgeEl.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); showCuratorPopup(badge.appId, curatorBadgeEl); }); } 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); 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 + ' (' + rate + '%)' + '经验值: ' + group.totalXp + '' + (group.totalDrops > 0 ? '掉落: ' + group.totalDrops + '' : '') + '' + // v1.2.14-fix2: 新增大屏详情按钮(浮窗展示全部卡牌+列表切换+求购价) '详情' + '' + '' + '' + // 徽章列表(原版风格) '徽章 点击徽章行打开 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)); }); // v1.2.6: 卡牌网格(7列紧凑) var cardsContainer = el.querySelector('.sbc-card-grid--cards'); if (cardsContainer && totalCards > 0) { allCards.forEach(function(card, i) { var isOwned = isAllSynthesized || (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.2.6: 原版风格徽章行(参考 Steam 徽章页结构) * 结构: [徽章图标] [徽章名 + 等级] [经验值] [卡牌进度条] [状态] */ function createBadgeTableRow(badge) { 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.2.14-fix2: 详情按钮事件(阻止冒泡,避免触发外层链接跳转) var detailBtn = row.querySelector('[data-badge-detail]'); if (detailBtn) { detailBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); showCardDetail(badge); }); } return row; } /** * 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.2.3: 卡牌详情弹窗 // ============================================================ function showCardDetail(badge) { 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.setAttribute('data-app-id', badge.appId || ''); panel.addEventListener('click', function(e) { e.stopPropagation(); }); // v1.2.14: 详情浮窗视图模式(封面/列表),闭包保存以便切换时重渲染 var detailViewMode = 'cover'; // v1.2.14: 求购价加载状态 var buyOrderState = { loading: false, loaded: false }; // 获取市场数据 var marketData = getMarketPriceForApp(badge.appId); var needFetch = shouldFetchDetailMarket(badge, marketData); function rerenderCards(state) { var wrap = panel.querySelector('.sbc-detail-cards-wrap'); if (!wrap) return; var currentData = getMarketPriceForApp(badge.appId) || marketData; wrap.innerHTML = buildDetailCardsHtml(badge, currentData, state || null, detailViewMode); } var subtitle = badge.badgeName && badge.badgeName !== badge.gameName ? badge.badgeName : ''; if (badge.appId) subtitle += (subtitle ? ' · ' : '') + 'AppID: ' + badge.appId; // v1.2.14-fix7: 顶部标题右侧展示地区旗帜+货币符号 var regionBadge = buildRegionBadge(); panel.innerHTML = '' + (badge.badgeImg ? '' : '') + '' + escapeHtml(badge.gameName) + regionBadge + '' + (subtitle ? '' + escapeHtml(subtitle) + '' : '') + '' + // v1.2.14-fix14: 社区快捷入口移到标题栏右上角 buildDetailActionsHtml(badge, marketData) + '✕' + '' + '' + '' + buildDetailInfoItems(badge, marketData) + '' + '' + buildDetailCardsHtml(badge, 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.2.14: 事件委托(失败重试 / 封面列表切换 / 求购价加载) panel.addEventListener('click', function(e) { var target = e.target && e.target.closest ? e.target.closest('[data-detail-retry], [data-view], [data-detail-buyorder]') : null; if (!target) return; // 失败重试 if (target.hasAttribute('data-detail-retry')) { rerenderCards({ loading: true }); loadDetailMarketData(badge, panel, overlay, function() { return detailViewMode; }); 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(badge, panel, detailViewMode, buyOrderState, rerenderCards); return; } }); document.addEventListener('keydown', function escD(e) { if (e.key === 'Escape') { overlay.classList.remove('active'); document.removeEventListener('keydown', escD); } }); // v1.2.14: 未预加载市场价格时,打开详情即按需抓取该游戏卡牌图片与价格 if (needFetch) loadDetailMarketData(badge, panel, overlay, function() { return detailViewMode; }); } // 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.2.14-fix2: 游戏详情大屏浮窗(汇总视图 + 分组视图共用) // 展示: 游戏头部 + 徽章列表 + 全部卡牌(封面/列表切换 + 求购价) // ============================================================ function showGameDetailPopup(gameData) { var existing = document.getElementById('sbc-detail-overlay'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.className = 'sbc-detail-overlay sbc-detail-overlay--large'; 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 sbc-detail-panel--large'; panel.setAttribute('data-app-id', gameData.appId || ''); panel.addEventListener('click', function(e) { e.stopPropagation(); }); // 视图模式与求购价状态(闭包) var detailViewMode = 'cover'; var buyOrderState = { loading: false, loaded: false }; // 聚合该游戏所有徽章的已拥有卡牌图 var ownedImgs = []; if (gameData.badges) { gameData.badges.forEach(function(b) { if (b.cardImgs && b.cardImgs.length) ownedImgs = ownedImgs.concat(b.cardImgs); }); } var syntheticBadge = { appId: gameData.appId, gameName: gameData.gameName, cardImgs: ownedImgs, badgeImg: (gameData.badges && gameData.badges[0]) ? gameData.badges[0].badgeImg : '', badgeName: '', isLocked: false, level: 0, xp: 0, collected: 0, total: 0, }; var marketData = getMarketPriceForApp(gameData.appId); // v1.2.14-fix4: 统一使用 shouldFetchDetailMarket(与 showCardDetail 一致) var needFetch = shouldFetchDetailMarket(syntheticBadge, marketData); function rerenderCards(state) { var wrap = panel.querySelector('.sbc-detail-cards-wrap'); if (!wrap) return; var currentData = getMarketPriceForApp(gameData.appId) || marketData; // 用游戏级 syntheticBadge 渲染(聚合所有徽章的已拥有卡牌) wrap.innerHTML = buildDetailCardsHtml(syntheticBadge, currentData, state || null, detailViewMode); } // 头部信息 var badgeCount = gameData.badges ? gameData.badges.length : 0; var subtitle = badgeCount + ' 枚徽章'; if (gameData.appId) subtitle += ' · AppID: ' + gameData.appId; if (gameData.totalXp) subtitle += ' · ' + gameData.totalXp + ' XP'; if (gameData.totalDrops) subtitle += ' · 掉落 ' + gameData.totalDrops; // 徽章列表 HTML function buildBadgesListHtml() { if (!gameData.badges || gameData.badges.length === 0) { return '暂无徽章数据'; } var sorted = gameData.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) { html += '' + '' + (b.badgeImg ? '' : '') + '' + '' + '' + escapeHtml(b.badgeName || b.gameName) + '' + '' + (b.isLocked ? '未解锁' : 'Lv.' + b.level) + ' · ' + b.xp + ' XP' + (b.isFoil ? ' · 闪卡' : '') + '' + '' + '详情' + ''; }); html += ''; return html; } panel.innerHTML = '' + (syntheticBadge.badgeImg ? '' : '') + '' + escapeHtml(gameData.gameName) + buildRegionBadge() + '' + '' + escapeHtml(subtitle) + '' + // v1.2.14-fix14: 社区快捷入口移到标题栏右上角 buildDetailActionsHtml(syntheticBadge, marketData) + '✕' + '' + '' + '' + buildBadgesListHtml() + '' + '' + '' + 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'); }); // 事件委托(失败重试 / 封面列表切换 / 求购价加载 / 徽章详情) 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 badge = (typeof allBadges !== 'undefined') ? allBadges.find(function(b) { return b.index === idx; }) : null; if (badge) { overlay.classList.remove('active'); setTimeout(function() { showCardDetail(badge); }, 150); } return; } // 失败重试 if (target.hasAttribute('data-detail-retry')) { rerenderCards({ loading: true }); loadDetailMarketData(syntheticBadge, panel, overlay, function() { return detailViewMode; }); 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); } }); // v1.2.14-fix4: 按需抓取市场数据(shouldFetchDetailMarket 已检查 appId) if (needFetch) loadDetailMarketData(syntheticBadge, panel, overlay, function() { return detailViewMode; }); } // 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: 信息网格(抓取成功后可重渲染,补齐"全套卡牌均价") function buildDetailInfoItems(badge, marketData) { 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 = '求购价'; // v1.2.14-fix14: 求购价按钮下方新增"批量购买未拥有"按钮 var batchBuyBtn = '' + '批量购买未拥有' + ''; return '' + '卡牌列表' + (cardCount > 0 ? ' (' + cardCount + ' 张)' : '') + '' + (cardCount > 0 ? toggle + buyOrderBtn : '') + '' + (cardCount > 0 ? batchBuyBtn : ''); } // 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 类用于灰色遮罩 function buildCardCoverItem(c, badge) { var isOwned = true; if (badge && badge.cardImgs && badge.cardImgs.length > 0 && c.iconUrl) { isOwned = badge.cardImgs.some(function(src) { return normalizeCardUrl(src) === normalizeCardUrl(c.iconUrl); }); } else if (badge && badge.cardImgs && badge.cardImgs.length > 0 && !c.iconUrl) { isOwned = false; } var ownedMark = isOwned ? '✓' : ''; var item = '' + (c.iconUrl ? '' : '🎴') + ownedMark + '' + escapeHtml(c.name || '已拥有卡牌') + '' + buildCardPriceBlock(c, 'cover') + ''; return c.marketUrl ? '' + item + '' : item; } // v1.2.14: 列表视图单个卡牌(参考 steam-store-card-prices.renderCardRow) // v1.2.14-fix: 未拥有卡牌添加 --unowned 类用于灰色遮罩 function buildCardListItem(c, badge) { var isOwned = true; if (badge && badge.cardImgs && badge.cardImgs.length > 0 && c.iconUrl) { isOwned = badge.cardImgs.some(function(src) { return normalizeCardUrl(src) === normalizeCardUrl(c.iconUrl); }); } else if (badge && badge.cardImgs && badge.cardImgs.length > 0 && !c.iconUrl) { isOwned = false; } 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: 详情浮窗按需抓取 —— 内存 → GM 缓存 → 实时请求,单游戏单飞 var _detailMarketInflight = {}; function ensureDetailMarketData(appId) { var existing = getMarketPriceForApp(appId); if (existing && existing.cards && existing.cards.length > 0) return Promise.resolve(existing); var cached = readMarketCache(appId); if (cached && cached.cards && cached.cards.length > 0) { marketPrices[appId] = cached; return Promise.resolve(cached); } 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 渲染 function loadDetailMarketData(badge, panel, overlay, getViewMode) { 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); 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); } } // 同步刷新背景卡片的角标/统计 scheduleIncrementalRender(); }).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 商店封面图 if (!imgContent && game.appId) { var capsuleUrl = 'https://cdn.cloudflare.steamstatic.com/steam/apps/' + game.appId + '/capsule_236x174.jpg'; imgContent = '' + // 加载失败时的兜底 SVG(默认隐藏) '' + '' + '' + '' + ''; } 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' + '✕' + '' + '' + '概览统计' + '价值排行' + '市场价格' + '交易推荐' + '鉴赏家推荐 (v1.2.16)' + '' + ''; 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 'curator': renderCuratorAnalysis(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) { 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) { 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.3.1...'); GM_addStyle(CSS); 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); console.log('[SBC-Pro] 初始化完成,共 ' + allBadges.length + ' 个徽章,' + craftableBadges.length + ' 个可合卡'); showToast('已加载 ' + allBadges.length + ' 个徽章,' + craftableBadges.length + ' 个可合卡 (Pro v1.3.1)', '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.2.16: 鉴赏家推荐聚合模块 // ------------------------------------------------------------ // 功能: // 1) 鉴赏家页面(store.steampowered.com/curator/*)注入聚合信息面板 // - 顶部统计: 推荐游戏总数 / TOP5 被最多鉴赏家推荐 / 推荐率分布 // - 增强列表: 每个游戏显示"X 个鉴赏家推荐"徽章 + 其他鉴赏家头像 // - 筛选: 按推荐数量(3+)/按推荐状态(推荐/不推荐) // - 排序: 按推荐鉴赏家数量切换 // 2) 徽章页面(steamcommunity.com/*/badges*)扩展 // - 卡片视图为有鉴赏家推荐的游戏添加推荐标记,点击展开鉴赏家列表 // - 智能分析浮窗新增"鉴赏家推荐"维度 // 数据来源: // - store.steampowered.com/curator/ajaxgetcuratedapplist?curator_id=&start=&count= // - store.steampowered.com/curator/ajaxgetcuratorsforapp?appid= // 缓存: GM_setValue, key = curator_recs_{appid}, TTL 12 小时 // 技术约定: 3 并发 worker / 1s 间隔 / 预留位限流 / 字符串 URL 拼接 // ============================================================ // ---- 页面检测 ---- function isCuratorPage() { return /^https:\/\/store\.steampowered\.com\/curator\//.test(location.href); } function isBadgesPage() { return /^https:\/\/steamcommunity\.com\/(id|profiles)\/[^\/]+\/badges/.test(location.href); } function getCuratorId() { var m = location.pathname.match(/\/curator\/(\d+)/); return m ? m[1] : ''; } // ---- 限流(预留位模式): 在 sleep 前更新 lastRequestTime 防止并发竞态 ---- async function curatorThrottle() { var now = Date.now(); var wait = curatorLastRequestTime + CONFIG.CURATOR_REQUEST_DELAY - now; if (wait > 0) { // 预留位: 立即占住下一个槽位,并发 worker 会基于已推进的时间计算各自等待 curatorLastRequestTime = curatorLastRequestTime + CONFIG.CURATOR_REQUEST_DELAY; await delay(wait); } else { curatorLastRequestTime = now; } } // ---- 缓存读写 ---- function readCuratorCache(appId) { var key = 'curator_recs_' + appId; var raw = null; try { raw = GM_getValue(key, null); } catch (e) { raw = null; } if (!raw) return null; try { var obj = typeof raw === 'string' ? JSON.parse(raw) : raw; if (obj && obj.expiresAt && Date.now() < obj.expiresAt) return obj.data; return null; } catch (e) { return null; } } function writeCuratorCache(appId, data) { var key = 'curator_recs_' + appId; var obj = { data: data, expiresAt: Date.now() + CONFIG.CURATOR_CACHE_TTL, cachedAt: Date.now() }; try { GM_setValue(key, JSON.stringify(obj)); } catch (e) { /* 静默 */ } } // ---- 数据抓取 ---- // 字符串 URL 拼接(不用 new URL 处理相对路径) function buildCuratorListUrl(curatorId, start, count) { return 'https://store.steampowered.com/curator/ajaxgetcuratedapplist?curator_id=' + curatorId + '&start=' + start + '&count=' + count; } function buildCuratorsForAppUrl(appId) { return 'https://store.steampowered.com/curator/ajaxgetcuratorsforapp?appid=' + appId; } // 获取当前鉴赏家的推荐游戏列表 async function fetchCuratorRecommendedApps(curatorId) { var allApps = []; var start = 0; var count = CONFIG.CURATOR_LIST_COUNT; var total = Infinity; // 最多翻 4 页(200 条),避免对单个鉴赏家拉取过多 var maxPages = 4; while (start < total && start < count * maxPages) { await curatorThrottle(); var text; try { text = await fmRequest(buildCuratorListUrl(curatorId, start, count)); } catch (e) { console.warn('[SBC-Pro] 鉴赏家推荐列表请求失败:', e.message); break; } var data; try { data = JSON.parse(text); } catch (e) { break; } if (!data || data.success === false || data.success === 0) break; var results = Array.isArray(data.results) ? data.results : (Array.isArray(data.apps) ? data.apps : (Array.isArray(data.items) ? data.items : [])); if (results.length === 0) break; results.forEach(function(it) { if (!it) return; var appId = it.appid || it.app_id || it.id; if (!appId) return; allApps.push({ appId: String(appId), name: it.name || it.title || ('App ' + appId), headerImage: it.header_image || it.capsule || it.logo || '', recommended: it.recommended !== false && it.recommended !== 'not_recommended', }); }); total = Number.isFinite(Number(data.total_count)) ? Number(data.total_count) : results.length; if (results.length < count) break; start += count; } return allApps; } // 归一化单个鉴赏家条目(兼容多种返回结构) function _normalizeCuratorEntry(entry) { if (!entry) return null; var cur = entry.curator || entry; var clid = cur.clid || cur.curator_id || cur.id || entry.clid || ''; var name = cur.name || cur.curator_name || entry.name || ''; var avatar = cur.avatar || cur.avatar_image || cur.avatar_url || entry.avatar || ''; var recFlag = entry.recommended; var isRec; if (typeof recFlag === 'string') isRec = /recommend/i.test(recFlag) && !/not/i.test(recFlag); else if (typeof recFlag === 'boolean') isRec = recFlag; else isRec = true; if (!clid && !name) return null; return { clid: String(clid), name: name, avatar: avatar, recommended: isRec, url: clid ? ('https://store.steampowered.com/curator/' + clid) : '', }; } // 获取推荐某游戏的鉴赏家列表(带缓存) async function fetchCuratorsForApp(appId) { // 内存缓存 if (curatorRecs[appId] && curatorRecs[appId].curators) return curatorRecs[appId]; // GM 缓存 var cached = readCuratorCache(appId); if (cached) { curatorRecs[appId] = cached; return cached; } await curatorThrottle(); var text; try { text = await fmRequest(buildCuratorsForAppUrl(appId)); } catch (e) { console.warn('[SBC-Pro] 鉴赏家列表请求失败 app=' + appId + ':', e.message); return null; } var data; try { data = JSON.parse(text); } catch (e) { return null; } if (!data || data.success === false || data.success === 0) return null; var arr = Array.isArray(data.curators) ? data.curators : (Array.isArray(data.results) ? data.results : (Array.isArray(data.items) ? data.items : [])); var curators = []; arr.forEach(function(entry) { var c = _normalizeCuratorEntry(entry); if (c) curators.push(c); }); var recCount = curators.filter(function(c) { return c.recommended; }).length; var result = { curators: curators, total: curators.length, recommendedCount: recCount, fetchedAt: Date.now() }; curatorRecs[appId] = result; writeCuratorCache(appId, result); return result; } // v1.3.1 R2: 使用 concurrentPool 替代手写 worker pool,获得 429 降并发 + 指数退避重试 async function fetchCuratorsForAppsBatch(appIds, onProgress) { var results = {}; var tasks = appIds.map(function(appId) { return async function() { try { results[appId] = await fetchCuratorsForApp(appId); } catch (e) { results[appId] = null; } return results[appId]; }; }); await concurrentPool(tasks, CONFIG.CURATOR_REQUEST_CONCURRENCY, function(completed, total) { if (typeof onProgress === 'function') { try { onProgress(completed, total); } catch (e) {} } }); return results; } // ---- 聚合统计 ---- function buildCuratorAggregation(appIds) { var stats = { totalApps: appIds.length, withRecs: 0, topApps: [], dist: { one: 0, twoFive: 0, fivePlus: 0 }, recommendedApps: 0, notRecommendedApps: 0, }; var scored = []; appIds.forEach(function(appId) { var rec = curatorRecs[appId]; if (!rec) return; var n = rec.recommendedCount != null ? rec.recommendedCount : 0; if (n > 0) { stats.withRecs++; scored.push({ appId: appId, count: n, rec: rec }); if (n === 1) stats.dist.one++; else if (n <= 5) stats.dist.twoFive++; else stats.dist.fivePlus++; // 推荐状态: 推荐数 > 不推荐数 视为"推荐" var notRec = rec.total - n; if (n >= notRec) stats.recommendedApps++; else stats.notRecommendedApps++; } }); scored.sort(function(a, b) { return b.count - a.count; }); stats.topApps = scored.slice(0, CONFIG.CURATOR_TOP_N); return stats; } // ============================================================ // 鉴赏家页面 UI // ============================================================ // 每个面板实例持有自己的 filter set 副本(从共享默认 new Set 复制), // 避免跨组件引用共享导致互相污染。set 内 token: all/recommended/not_recommended/min3 let curatorPanelState = { sortByRec: false, // 是否按推荐鉴赏家数量排序 filterSet: new Set(curatorDefaultFilterSet), // 独立副本,防止跨组件引用共享 apps: [], // 当前鉴赏家推荐的游戏列表 currentCuratorId: '', }; // 便捷读取: 当前状态筛选值 function curatorStatusFilter() { var s = curatorPanelState.filterSet; if (s.has('recommended')) return 'recommended'; if (s.has('not_recommended')) return 'not_recommended'; return 'all'; } function curatorMin3Enabled() { return curatorPanelState.filterSet.has('min3'); } function initCurator() { var curatorId = getCuratorId(); if (!curatorId) { console.log('[SBC-Pro] 未识别到鉴赏家 ID'); return; } curatorPanelState.currentCuratorId = curatorId; console.log('[SBC-Pro] 鉴赏家聚合初始化, curator_id=' + curatorId); GM_addStyle(CSS); // 复用主样式(本页可能未注入) // 注入容器 var container = document.createElement('div'); container.className = 'sbc-curator-panel'; container.id = 'sbc-curator-panel'; container.innerHTML = '鉴赏家推荐聚合 v1.2.16' + '聚合多个鉴赏家对同一游戏的推荐数据(数据来自 Steam 鉴赏家 API,缓存 12 小时)' + '正在拉取鉴赏家推荐列表…'; // 插入到页面主内容区顶部 var anchor = document.querySelector('.curator_page_ctn') || document.querySelector('#curator_page_content') || document.querySelector('.page_content_ctn') || document.querySelector('main') || document.body; anchor.parentNode.insertBefore(container, anchor); // 1. 拉取当前鉴赏家推荐列表 fetchCuratorRecommendedApps(curatorId).then(function(apps) { curatorPanelState.apps = apps || []; renderCuratorPanelBody(); if (!apps || apps.length === 0) return; // 2. 对每个游戏拉取推荐该游戏的鉴赏家列表(3 worker, 1s 间隔) var appIds = apps.map(function(a) { return a.appId; }); curatorLoading = true; curatorLoadProgress = { total: appIds.length, done: 0 }; fetchCuratorsForAppsBatch(appIds, function(done, total) { curatorLoadProgress = { total: total, done: done }; renderCuratorPanelBody(); }).then(function() { curatorLoading = false; renderCuratorPanelBody(); }).catch(function(e) { curatorLoading = false; console.warn('[SBC-Pro] 鉴赏家聚合抓取失败:', e && e.message); renderCuratorPanelBody(); }); }).catch(function(e) { console.warn('[SBC-Pro] 鉴赏家推荐列表抓取失败:', e && e.message); var body = document.getElementById('sbc-curator-body'); if (body) body.innerHTML = '拉取鉴赏家推荐列表失败:' + escapeHtml(e && e.message || '未知错误') + ''; }); } function renderCuratorPanelBody() { var body = document.getElementById('sbc-curator-body'); if (!body) return; var apps = curatorPanelState.apps || []; if (apps.length === 0 && !curatorLoading) { body.innerHTML = '该鉴赏家暂无推荐游戏,或拉取失败。'; return; } var agg = buildCuratorAggregation(apps.map(function(a) { return a.appId; })); // ---- 顶部统计 ---- var distTotal = agg.dist.one + agg.dist.twoFive + agg.dist.fivePlus; var distHtml = ''; if (distTotal > 0) { var segs = [ { n: agg.dist.one, c: '#66c0f4', l: '1 个鉴赏家' }, { n: agg.dist.twoFive, c: '#f59e0b', l: '2-5 个' }, { n: agg.dist.fivePlus, c: '#10b981', l: '5+ 个' }, ]; distHtml = '' + segs.map(function(s) { var pct = distTotal > 0 ? (s.n / distTotal * 100) : 0; return ''; }).join('') + '' + '' + segs.map(function(s) { return '● ' + s.l + ': ' + s.n + ''; }).join('') + ''; } // ---- TOP 5 ---- var topHtml = ''; if (agg.topApps.length > 0) { var appMap = {}; apps.forEach(function(a) { appMap[a.appId] = a; }); topHtml = '被最多鉴赏家推荐 TOP ' + agg.topApps.length + '' + agg.topApps.map(function(it, i) { var a = appMap[it.appId] || { name: 'App ' + it.appId, appId: it.appId }; return '' + '' + (i + 1) + '' + '' + escapeHtml(a.name) + '' + '' + it.count + ' 个鉴赏家' + ''; }).join('') + ''; } // ---- 工具栏(排序 / 筛选) ---- var progressHtml = curatorLoading ? '聚合中 ' + curatorLoadProgress.done + '/' + curatorLoadProgress.total + '…' : '已聚合 ' + agg.withRecs + '/' + apps.length + ' 款有鉴赏家推荐'; var toolbarHtml = '' + '' + (curatorPanelState.sortByRec ? '↓ 推荐数排序' : '默认排序') + '' + '' + '仅 3+ 鉴赏家' + '全部状态' + '仅推荐' + '仅不推荐' + '' + progressHtml + ''; // ---- 列表(增强) ---- var list = curatorPanelState.apps.slice(); // 排序 if (curatorPanelState.sortByRec) { list.sort(function(a, b) { var ra = curatorRecs[a.appId] ? (curatorRecs[a.appId].recommendedCount || 0) : 0; var rb = curatorRecs[b.appId] ? (curatorRecs[b.appId].recommendedCount || 0) : 0; return rb - ra; }); } // 筛选(基于独立 filter set 副本) var statusVal = curatorStatusFilter(); var min3 = curatorMin3Enabled(); var filtered = list.filter(function(a) { var rec = curatorRecs[a.appId]; var recCount = rec ? (rec.recommendedCount || 0) : 0; // 当前鉴赏家的推荐状态(a.recommended 来自列表本身) if (statusVal === 'recommended' && !a.recommended) return false; if (statusVal === 'not_recommended' && a.recommended) return false; if (min3 && recCount < 3) return false; return true; }); var listHtml = ''; if (filtered.length === 0) { listHtml = '当前筛选条件下无游戏。' + (curatorLoading ? '聚合仍在进行中,稍后再试。' : '') + ''; } else { listHtml = '' + filtered.map(function(a) { var rec = curatorRecs[a.appId]; var recCount = rec ? (rec.recommendedCount || 0) : 0; var notCount = rec ? (rec.total - recCount) : 0; var capsule = a.headerImage ? a.headerImage : ('https://cdn.cloudflare.steamstatic.com/steam/apps/' + a.appId + '/capsule_236x174.jpg'); // 推荐该游戏的其他鉴赏家头像(最多 5 个) var avatarsHtml = ''; if (rec && rec.curators && rec.curators.length > 0) { var shown = rec.curators.slice(0, 5); avatarsHtml = '' + shown.map(function(c) { var img = c.avatar ? '' : '' + escapeHtml((c.name || '?').charAt(0)) + ''; return img; }).join('') + (rec.curators.length > 5 ? '+' + (rec.curators.length - 5) + '' : '') + ''; } else if (curatorLoading) { avatarsHtml = '聚合中…'; } var recBadge = rec ? '' + recCount + ' 个鉴赏家推荐' + (notCount > 0 ? ' · ' + notCount + ' 不推荐' : '') + '' : ''; var statusBadge = a.recommended ? '本鉴赏家:推荐' : '本鉴赏家:不推荐'; return '' + '' + '' + '' + '' + escapeHtml(a.name) + '' + recBadge + statusBadge + '' + avatarsHtml + '' + ''; }).join('') + ''; } body.innerHTML = '' + '推荐游戏总数' + apps.length + '当前鉴赏家推荐' + '有鉴赏家推荐' + agg.withRecs + '被 ≥1 个鉴赏家推荐' + '推荐 / 不推荐' + agg.recommendedApps + ' / ' + agg.notRecommendedApps + '按推荐数多数判定' + '推荐率分布' + agg.dist.one + ' / ' + agg.dist.twoFive + ' / ' + agg.dist.fivePlus + '1个 / 2-5个 / 5+个' + distHtml + '' + '' + topHtml + toolbarHtml + listHtml; bindCuratorPanelEvents(); } function bindCuratorPanelEvents() { var sortBtn = document.getElementById('sbc-curator-sort'); if (sortBtn) sortBtn.addEventListener('click', function() { curatorPanelState.sortByRec = !curatorPanelState.sortByRec; renderCuratorPanelBody(); }); var minBtn = document.getElementById('sbc-curator-filter-min'); if (minBtn) minBtn.addEventListener('click', function() { // 切换 min3 token(操作的是组件自己的 filterSet 副本) if (curatorPanelState.filterSet.has('min3')) curatorPanelState.filterSet.delete('min3'); else curatorPanelState.filterSet.add('min3'); renderCuratorPanelBody(); }); document.querySelectorAll('.sbc-curator-btn[data-status]').forEach(function(btn) { btn.addEventListener('click', function() { var st = btn.getAttribute('data-status'); var s = curatorPanelState.filterSet; // 状态互斥: 清除旧状态 token,写入新状态(操作副本,不影响共享默认) s.delete('all'); s.delete('recommended'); s.delete('not_recommended'); s.add(st); renderCuratorPanelBody(); }); }); // TOP 项 / 列表项点击跳转商店(头像点击跳鉴赏家) document.querySelectorAll('.sbc-curator-top-item[data-appid], .sbc-curator-item[data-appid]').forEach(function(el) { el.addEventListener('click', function(e) { if (e.target.closest('a') || e.target.closest('.sbc-curator-avatar')) return; var appId = el.getAttribute('data-appid'); if (appId) window.open('https://store.steampowered.com/app/' + appId, '_blank'); }); }); document.querySelectorAll('.sbc-curator-avatar[data-url]').forEach(function(av) { av.addEventListener('click', function(e) { e.stopPropagation(); var url = av.getAttribute('data-url'); if (url) window.open(url, '_blank'); }); }); } // ============================================================ // 徽章页面扩展: 鉴赏家推荐标记 + 分析维度 // ============================================================ function getCuratorRecsForApp(appId) { if (!appId) return null; return curatorRecs[appId] || null; } // 徽章页: 为用户游戏懒加载鉴赏家推荐(按需触发,避免每次进页都打 API) async function loadCuratorRecsForBadges(onProgress) { if (curatorLoading) return; // 收集唯一 appId(去重,封顶) var seen = {}, appIds = []; for (var i = 0; i < allBadges.length; i++) { var b = allBadges[i]; if (b && b.appId && !seen[b.appId]) { seen[b.appId] = true; appIds.push(String(b.appId)); if (appIds.length >= CONFIG.CURATOR_BADGE_PAGE_MAX_APPS) break; } } if (appIds.length === 0) return; curatorLoading = true; curatorLoadProgress = { total: appIds.length, done: 0 }; try { await fetchCuratorsForAppsBatch(appIds, function(done, total) { curatorLoadProgress = { total: total, done: done }; if (typeof onProgress === 'function') { try { onProgress(done, total); } catch (e) {} } // 就地更新卡片上的鉴赏家标记 updateCuratorBadgesInPlace(); }); } finally { curatorLoading = false; updateCuratorBadgesInPlace(); } } // 就地为已渲染的卡片注入鉴赏家推荐标记(参考 updatePriceBadgesInPlace) function updateCuratorBadgesInPlace() { 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; var rec = getCuratorRecsForApp(appId); var imgWrap = card.querySelector('.sbc-card-img-wrap') || card; var existing = imgWrap.querySelector('.sbc-card-curator-badge'); if (rec && rec.recommendedCount > 0) { if (!existing) { existing = document.createElement('div'); existing.className = 'sbc-card-curator-badge'; imgWrap.appendChild(existing); } existing.textContent = '★ ' + rec.recommendedCount + ' 鉴赏家'; existing.title = '被 ' + rec.recommendedCount + ' 个鉴赏家推荐,点击查看详情'; existing.setAttribute('data-app-id', appId); // 绑定点击(展开浮窗) existing.onclick = function(e) { e.preventDefault(); e.stopPropagation(); showCuratorPopup(this.getAttribute('data-app-id'), this); }; } else if (existing) { existing.remove(); } } } catch (e) { /* 静默 */ } } // 徽章页: 点击标记展开鉴赏家列表浮窗 function showCuratorPopup(appId, anchorEl) { // 关闭已有 var old = document.getElementById('sbc-curator-popup'); if (old) old.remove(); var rec = getCuratorRecsForApp(appId); if (!rec || !rec.curators || rec.curators.length === 0) return; var badge = null; for (var i = 0; i < allBadges.length; i++) { if (String(allBadges[i].appId) === String(appId)) { badge = allBadges[i]; break; } } var popup = document.createElement('div'); popup.className = 'sbc-curator-popup'; popup.id = 'sbc-curator-popup'; var title = (badge && badge.gameName) ? badge.gameName : ('App ' + appId); popup.innerHTML = '鉴赏家推荐 · ' + escapeHtml(title) + ' ' + rec.recommendedCount + ' 推荐' + (rec.total - rec.recommendedCount > 0 ? ' / ' + (rec.total - rec.recommendedCount) + ' 不推荐' : '') + '' + '' + rec.curators.slice(0, 20).map(function(c) { var img = c.avatar ? '' : '' + escapeHtml((c.name || '?').charAt(0)) + ''; var tag = c.recommended ? '推荐' : '不推荐'; return '' + img + '' + escapeHtml(c.name) + '' + tag + ''; }).join('') + (rec.curators.length > 20 ? '还有 ' + (rec.curators.length - 20) + ' 个鉴赏家…' : '') + ''; document.body.appendChild(popup); // 定位到锚点附近 if (anchorEl && anchorEl.getBoundingClientRect) { var r = anchorEl.getBoundingClientRect(); var left = Math.min(r.left, window.innerWidth - 340); var top = r.bottom + 6; if (top + 200 > window.innerHeight) top = Math.max(4, r.top - 200); popup.style.left = Math.max(4, left) + 'px'; popup.style.top = top + 'px'; } else { popup.style.left = '50%'; popup.style.top = '30%'; popup.style.transform = 'translateX(-50%)'; } popup.querySelectorAll('.sbc-curator-popup-item[data-url]').forEach(function(it) { it.addEventListener('click', function() { var url = it.getAttribute('data-url'); if (url) window.open(url, '_blank'); }); }); // 点击外部关闭 setTimeout(function() { document.addEventListener('click', function closeCuratorPopup(e) { if (!popup.contains(e.target) && e.target !== anchorEl) { popup.remove(); document.removeEventListener('click', closeCuratorPopup); } }); }, 0); // ESC 关闭 var escHandler = function(e) { if (e.key === 'Escape') { popup.remove(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // 分析浮窗: 鉴赏家推荐维度渲染 function renderCuratorAnalysis(content) { var loadedCount = Object.keys(curatorRecs).length; var hasData = loadedCount > 0; if (!hasData && !curatorLoading) { content.innerHTML = '鉴赏家推荐聚合 (v1.2.16)' + '尚未加载鉴赏家推荐数据。点击下方按钮,将为你的游戏拉取各鉴赏家推荐情况(3 并发 / 1s 间隔 / 12 小时缓存)。' + '加载鉴赏家推荐' + ''; var btn = document.getElementById('sbc-ap-load-curator'); if (btn) btn.addEventListener('click', function() { btn.disabled = true; btn.textContent = '加载中…'; loadCuratorRecsForBadges(function() { if (analysisActiveTab === 'curator') renderCuratorAnalysis(content); }).then(function() { if (analysisActiveTab === 'curator') renderCuratorAnalysis(content); }).catch(function(e) { showToast('鉴赏家推荐加载失败: ' + (e && e.message || ''), 'error'); if (analysisActiveTab === 'curator') renderCuratorAnalysis(content); }); }); return; } if (curatorLoading && !hasData) { content.innerHTML = '鉴赏家推荐聚合 (v1.2.16)' + '' + '' + '正在聚合鉴赏家推荐 ' + curatorLoadProgress.done + '/' + curatorLoadProgress.total + '…' + ''; return; } // 收集用户游戏中出现鉴赏家推荐的 appId var appIds = []; var seen = {}; allBadges.forEach(function(b) { if (b && b.appId && curatorRecs[b.appId] && !seen[b.appId]) { seen[b.appId] = true; appIds.push(String(b.appId)); } }); var agg = buildCuratorAggregation(appIds); var appMap = {}; allBadges.forEach(function(b) { if (b && b.appId) appMap[String(b.appId)] = b; }); var topRows = agg.topApps.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 b = appMap[it.appId] || { gameName: 'App ' + it.appId, url: 'https://store.steampowered.com/app/' + it.appId }; var notRec = it.rec.total - it.count; return '' + '' + (i + 1) + '' + getGameCoverHtml(it.appId) + '' + escapeHtml(b.gameName) + '' + '' + it.count + ' 鉴赏家' + '' + (notRec > 0 ? notRec + ' 不推荐' : '—') + '' + '推荐'; }).join(''); var progressLine = curatorLoading ? '聚合中 ' + curatorLoadProgress.done + '/' + curatorLoadProgress.total + '…' : ''; content.innerHTML = '鉴赏家推荐聚合 (v1.2.16)' + '借鉴 sffxzzp Curator Enhancer 的聚合模式:评分聚合展示 + 推荐过滤能力。数据缓存 12 小时。' + '' + '已聚合游戏' + loadedCount + '有鉴赏家推荐数据' + '被鉴赏家推荐' + agg.withRecs + '≥1 个鉴赏家推荐' + '推荐 / 不推荐' + agg.recommendedApps + ' / ' + agg.notRecommendedApps + '按推荐数多数判定' + '' + '推荐率分布: 1 个鉴赏家 ' + agg.dist.one + ' · 2-5 个 ' + agg.dist.twoFive + ' · 5+ 个 ' + agg.dist.fivePlus + '' + progressLine + '被最多鉴赏家推荐 TOP ' + agg.topApps.length + '' + (topRows ? '' + topRows + '' : '暂无数据') + '' + (curatorLoading ? '' : '重新加载鉴赏家推荐'); var reloadBtn = document.getElementById('sbc-ap-reload-curator'); if (reloadBtn) reloadBtn.addEventListener('click', function() { // 清空内存缓存强制刷新(GM 缓存保留避免重复打 API,但内存清空可重渲染) curatorRecs = {}; renderCuratorAnalysis(content); loadCuratorRecsForBadges(function() { if (analysisActiveTab === 'curator') renderCuratorAnalysis(content); }).then(function() { if (analysisActiveTab === 'curator') renderCuratorAnalysis(content); }); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function() { if (isCuratorPage()) initCurator(); else if (isBadgesPage()) init(); }); else { if (isCuratorPage()) initCurator(); else if (isBadgesPage()) init(); } })();