// ==UserScript== // @name Steam 许可分类器 // @namespace http://tampermonkey.net/ // @version 2.4.12 // @description 对Steam账户许可进行分类:商店、零售、礼物、免费,支持绝版游戏检查 // @author SmallFork // @match https://store.steampowered.com/account/licenses* // @grant GM_info // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_getResourceText // @grant unsafeWindow // @resource delistedData https://raw.githubusercontent.com/SmallFork/json/main/steam_delisted_apps.json // @connect api.steampowered.com // @connect raw.githubusercontent.com // @license MIT // @tag Steam // @tag games // @homepageURL https://keylol.com/t1035308-1-1 // @icon data:image/svg+xml, // ==/UserScript== (function() { 'use strict'; // ==================== 配置 ==================== const CFG = { PAGE_SIZE: 50, DEBOUNCE: 150, BLOB_CLEANUP: 500, DATE_W: 160, DAY_MS: 864e5, OBS_DEBOUNCE: 150, STICKY: 0, REDEEM_MAX: 9, REDEEM_WAIT: 20, MAX_PAGES: 3000, PAGE_DELAY: 150, CACHE_TTL: 7 * 864e5, // 缓存有效期 7 天(毫秒) INCREMENTAL_MAX_PAGES: 500, // 增量加载兜底:最多抓取页数,防止缓存指纹失配导致死循环 STALE_CACHE_THRESHOLD: 5 // 连续全新页达到此数时判定缓存失效 }; // ==================== I18N ==================== const ZH = { store:'商店',retail:'零售',free:'免费',gift:'礼物',all:'全部',total:'总计', pageTitle:'{nickname} 的许可',searchPlaceholder:'搜索游戏名称...', showAll:'显示全部',pagedView:'分页显示',prev:'上一页',next:'下一页', historyLink:'消费历史记录', advanced:'高级选项',dateFrom:'从',dateTo:'到', sortBy:'排序',nameAsc:'名称 A-Z',nameDesc:'名称 Z-A',dateLabel:'日期',clearFilter:'清除筛选', donutTitle:'许可分布详情', exportCSV:'CSV',exportJSON:'JSON',exportFailed:'导出失败',exportNoData:'导出失败: 没有数据',exportSuccess:'导出成功', csvDate:'日期',csvName:'名称',csvCategory:'分类',csvDetails:'获取方式', redeemBtn:'激活',redeemTitle:'CDKey 批量激活', redeemPlaceholder:'输入CDKey,支持批量激活', activateBtn:'激活Key',pasteBtn:'粘贴',clearBtn:'清空',copySub:'复制Sub',noSubData:'没有可复制的Sub',pasteNoKey:'没提取到Key',pasteKeyCount:'共提取{count}个Key', indexLabel:'#',resultLabel:'结果',detailLabel:'详情',itemLabel:'项目', success:'成功',fail:'失败',redeeming:'激活中',waiting:'等待中', copyFailed:'复制失败Key',noFailedKeys:'没有失败的Key',copiedKeys:'复制成功',clearHistory:'清空历史', redeemDone:'激活完成:共{total}个,成功{success}个,失败{fail}个', totalLabel:'总计',successLabel:'成功',failLabel:'失败', networkError:'网络错误或超时',othersError:'其他错误', invalidKey:'无效激活码',duplicateKey:'重复激活',rateLimit:'次数上限',regionRestrict:'地区限制',alreadyOwned:'已拥有',missingDep:'缺少主游戏',walletCode:'这是充值码',successDetail:'激活成功', withoutFree:'排除免费',freeDisabled:'已排除免费,无法查看免费分类',yearTitle:'年份',noData:'暂无数据', calendarTitle:'日历热力图',calendarNoData:'没有数据',calendarLess:'少',calendarMore:'多',countUnit:'个', filterCleared:'已清除筛选',clearCache:'清除缓存并刷新',clearCacheConfirm:'确定要清除所有缓存并刷新页面吗?',confirmOk:'确定',confirmCancel:'取消',noKeyInput:'没有输入Key',inputCleared:'已清空', monthlyDistribution:'月度分布', similarTitle:'相同许可猜测',similarPrefixLen:'前缀单词数',similarCount:'数量', dataNote:'数据仅供参考,实际以系统记录为准', trendTitle:'许可增加曲线',trendCumulative:'累加模式', predictTitle:'添加许可频率分析', predictDays:'天', predictNextDate:'预计下次添加',predictLastDate:'最后添加日期', predictTotalGames:'添加许可总天数',predictTotalItems:'非免费许可总数', predictAvgPerDay:'平均每日许可数',predictOverallAvg:'平均间隔', predictYearAvg:'年均间隔',predictYearCount:'添加天数', predictYearLicenses:'许可数',predictDailyLicenses:'每日许可数',predictYearRatio:'一年占比',predictNote:'基于历史数据预测,仅供参考', delistedTitle:'绝版游戏检查',delistedTotal:'绝版总数',delistedOwned:'已拥有',delistedMissing:'未拥有', delistedOwnership:'绝版拥有率',delistedSearch:'搜索绝版游戏...',delistedFetching:'正在获取游戏库...', delistedFetchFail:'获取游戏库失败',delistedNoData:'暂无绝版游戏数据',delistedLoaded:'已加载绝版数据', typeDelisted:'已下架',typePurchaseDisabled:'已禁售',typeF2P:'F2P不可用',typeRetailOnly:'仅零售',typeTestApp:'测试应用', delistedOwners:'拥有占比',delistedDate:'下架时间',delistedAchievements:'成就数',delistedCardName:'游戏名称',delistedCover:'封面', autoLoadingLicenses:'正在加载入库记录...',autoLoadingProgress:'正在加载入库记录... ({loaded}/{total})', autoLoadingDone:'入库记录加载完成,共 {count} 条',autoLoadingError:'加载入库记录失败: {msg}', autoLoadingRedirect:'检测到非第一页,正在跳转以加载全部记录...', loadStatusInit:'正在加载入库记录,请耐心等待...', loadStatusCache:'正在恢复缓存...', loadStatusCheck:'正在检查新增 (已对比 {count} 条)...', loadStatusFetch:'正在加第{page}页... ({loaded}/{total})...', loadStatusDone:'加载完成', }; const ZH_TW = { ...ZH, free:'贈品',gift:'禮物',total:'總計',pageTitle:'{nickname} 的授權', searchPlaceholder:'搜尋遊戲名稱...',showAll:'顯示全部',pagedView:'分頁顯示', historyLink:'消費歷史記錄',advanced:'進階選項',dateFrom:'從', nameAsc:'名稱 A-Z',nameDesc:'名稱 Z-A',clearFilter:'清除篩選', donutTitle:'許可分佈詳情', exportFailed:'匯出失敗',exportNoData:'匯出失敗: 沒有資料',exportSuccess:'匯出成功', csvName:'名稱',csvCategory:'分類',csvDetails:'取得方式', redeemPlaceholder:'輸入CDKey,支持批量激活', pasteBtn:'貼上',clearBtn:'清空',copySub:'複製Sub',noSubData:'沒有可複製的Sub',pasteNoKey:'沒提取到Key',pasteKeyCount:'共提取{count}個Key', indexLabel:'#',resultLabel:'結果',detailLabel:'詳情',itemLabel:'項目', fail:'失敗', copyFailed:'複製失敗Key',noFailedKeys:'沒有失敗的Key',copiedKeys:'複製成功',clearHistory:'清空歷史', redeemDone:'激活完成:共{total}個,成功{success}個,失敗{fail}個', totalLabel:'總計',successLabel:'成功',failLabel:'失敗', networkError:'網路錯誤或超時',othersError:'其他錯誤', invalidKey:'無效激活碼',duplicateKey:'重複激活',rateLimit:'次數上限',regionRestrict:'地區限制',alreadyOwned:'已擁有',missingDep:'缺少主遊戲',walletCode:'這是充值碼',successDetail:'激活成功', withoutFree:'排除免費',freeDisabled:'已排除免費,無法查看免費分類',yearTitle:'年份',noData:'暫無數據', calendarTitle:'日曆熱力圖',calendarNoData:'沒有數據',calendarLess:'少',calendarMore:'多',countUnit:'個', filterCleared:'已清除篩選',clearCache:'清除快取並重新整理',clearCacheConfirm:'確定要清除所有快取並重新整理嗎?',confirmOk:'確定',confirmCancel:'取消',noKeyInput:'沒有輸入Key',inputCleared:'已清空', monthlyDistribution:'月度分布', similarTitle:'相同許可猜測',similarPrefixLen:'前綴單詞數',similarCount:'數量', dataNote:'數據僅供參考,實際以系統記錄為準', trendTitle:'許可增加曲線',trendCumulative:'累加模式', predictTitle:'添加許可頻率分析', predictDays:'天', predictNextDate:'預計下次添加',predictLastDate:'最後添加日期', predictTotalGames:'添加許可總天數',predictTotalItems:'非免費許可總數', predictAvgPerDay:'平均每日許可數',predictOverallAvg:'平均間隔', predictYearAvg:'年均間隔',predictYearCount:'添加天數', predictYearLicenses:'許可數',predictDailyLicenses:'每日許可數',predictYearRatio:'一年占比',predictNote:'基於歷史數據預測,僅供參考', delistedTitle:'絕版遊戲檢查',delistedTotal:'絕版總數',delistedOwned:'已擁有',delistedMissing:'未擁有', delistedOwnership:'絕版擁有率',delistedSearch:'搜尋絕版遊戲...',delistedFetching:'正在獲取遊戲庫...', delistedFetchFail:'獲取遊戲庫失敗',delistedNoData:'暫無絕版遊戲數據',delistedLoaded:'已載入絕版數據', typeDelisted:'已下架',typePurchaseDisabled:'已禁售',typeF2P:'F2P不可用',typeRetailOnly:'僅零售',typeTestApp:'測試應用', delistedOwners:'擁有佔比',delistedDate:'下架時間',delistedAchievements:'成就數',delistedCardName:'遊戲名稱',delistedCover:'封面', autoLoadingLicenses:'正在載入入庫記錄...',autoLoadingProgress:'正在載入入庫記錄... ({loaded}/{total})', autoLoadingDone:'入庫記錄載入完成,共 {count} 條',autoLoadingError:'載入入庫記錄失敗: {msg}', autoLoadingRedirect:'檢測到非第一頁,正在跳轉以載入全部記錄...', loadStatusInit:'正在載入入庫記錄,請耐心等待...', loadStatusCache:'正在恢復快取...', loadStatusCheck:'正在檢查新增 (已對比 {count} 條)...', loadStatusFetch:'正在載入第{page}頁... ({loaded}/{total})...', loadStatusDone:'載入完成', }; const EN = { ...ZH, store:'Store',retail:'Retail',free:'Free',gift:'Gift',all:'All',total:'Total', pageTitle:"{nickname}'s Licenses",searchPlaceholder:'Search game name...', showAll:'Show All',pagedView:'Paged',prev:'Prev',next:'Next', historyLink:'Purchase History', advanced:'Advanced',dateFrom:'From',dateTo:'To', sortBy:'Sort',nameAsc:'Name A-Z',nameDesc:'Name Z-A',dateLabel:'Date',clearFilter:'Clear Filters', donutTitle:'License Distribution Detail', exportFailed:'Export Failed',exportNoData:'Export failed: No data',exportSuccess:'Export successful', csvDate:'Date',csvName:'Name',csvCategory:'Category',csvDetails:'Source', redeemBtn:'Redeem',redeemTitle:'Batch CDKey Activation', redeemPlaceholder:'Enter CDKeys, supports batch activation', activateBtn:'Redeem Keys',pasteBtn:'Paste',clearBtn:'Clear',copySub:'Copy Sub',noSubData:'No Sub data to copy',pasteNoKey:'No keys extracted',pasteKeyCount:'Extracted {count} keys', indexLabel:'#',resultLabel:'Result',detailLabel:'Detail',itemLabel:'Item', success:'Success',fail:'Failed',redeeming:'Redeeming',waiting:'Waiting', copyFailed:'Copy Failed Keys',noFailedKeys:'No failed keys',copiedKeys:'Copied successfully',clearHistory:'Clear History', redeemDone:'Activation complete: {total} total, {success} succeeded, {fail} failed', totalLabel:'Total',successLabel:'Success',failLabel:'Failed', networkError:'Network error/timeout',othersError:'Other error', invalidKey:'Invalid key',duplicateKey:'Duplicate key',rateLimit:'Rate limit',regionRestrict:'Region restricted',alreadyOwned:'Already owned',missingDep:'Missing dependency',walletCode:'Wallet code',successDetail:'Activated', withoutFree:'Without Free',freeDisabled:'Free excluded, cannot view free category',yearTitle:'Year',noData:'No data', calendarTitle:'Calendar Heatmap',calendarNoData:'No data',calendarLess:'Less',calendarMore:'More',countUnit:'', filterCleared:'Filters cleared',clearCache:'Clear cache & reload',clearCacheConfirm:'Are you sure you want to clear all cache and reload?',confirmOk:'Confirm',confirmCancel:'Cancel',noKeyInput:'No keys entered',inputCleared:'Cleared', monthlyDistribution:'Monthly Distribution', similarTitle:'Similar License Guess',similarPrefixLen:'Prefix Words',similarCount:'Count', dataNote:'Data is for reference only, actual records prevail', trendTitle:'License Growth Trend',trendCumulative:'Cumulative', predictTitle:'Add License Frequency', predictDays:'days', predictNextDate:'Predicted Next',predictLastDate:'Last Added', predictTotalGames:'Total License Days',predictTotalItems:'Non-free Licenses', predictAvgPerDay:'Avg Licenses/Day',predictOverallAvg:'Avg Interval', predictYearAvg:'Year Avg',predictYearCount:'Add Days', predictYearLicenses:'Licenses',predictDailyLicenses:'Daily Licenses',predictYearRatio:'Year Ratio',predictNote:'Prediction based on historical data, for reference only', delistedTitle:'Delisted Games Check',delistedTotal:'Total Delisted',delistedOwned:'Owned',delistedMissing:'Missing', delistedOwnership:'Delisted Ownership',delistedSearch:'Search delisted games...',delistedFetching:'Fetching game library...', delistedFetchFail:'Failed to fetch game library',delistedNoData:'No delisted game data',delistedLoaded:'Delisted data loaded', typeDelisted:'Delisted',typePurchaseDisabled:'Purchase Disabled',typeF2P:'F2P Unavailable',typeRetailOnly:'Retail Only',typeTestApp:'Test App', delistedOwners:'Owner %',delistedDate:'Delisted Date',delistedAchievements:'Achievements',delistedCardName:'Name',delistedCover:'Cover', autoLoadingLicenses:'Loading licenses...',autoLoadingProgress:'Loading licenses... ({loaded}/{total})', autoLoadingDone:'Loaded {count} licenses',autoLoadingError:'Failed to load licenses: {msg}', autoLoadingRedirect:'Not on first page, redirecting to load all records...', loadStatusInit:'Loading licenses, please wait...', loadStatusCache:'Restoring from cache...', loadStatusCheck:'Checking for updates (compared {count})...', loadStatusFetch:'Loading page {page}... ({loaded}/{total})...', loadStatusDone:'Loading complete', }; const I18N = { 'zh-CN': ZH, 'zh-TW': ZH_TW, 'zh-HK': ZH_TW, 'zh-MO': ZH_TW, 'en': EN }; let locale = 'zh-CN'; function detectLang() { const l = (document.documentElement.lang || '').toLowerCase(); if (I18N[l]) return l; if (l.startsWith('zh') && (l.includes('tw') || l.includes('hant'))) return 'zh-TW'; return l.startsWith('zh') ? 'zh-CN' : 'en'; } const t = k => I18N[locale]?.[k] ?? ZH[k] ?? k; const stopEv = (e, fn) => { e.stopPropagation(); e.preventDefault(); fn?.(); }; function toast(msg, type = 'info', ms = 3000) { const el = document.createElement('div'); el.className = `slc-toast slc-toast--${type}`; el.textContent = msg; document.body.appendChild(el); requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('slc-toast--visible'))); const hide = () => { el.classList.remove('slc-toast--visible'); setTimeout(() => el.remove(), 300); }; const timer = setTimeout(hide, ms); el.addEventListener('click', () => { clearTimeout(timer); hide(); }); return { el, hide }; } function showConfirm(msg) { return new Promise(resolve => { const modal = document.createElement('div'); modal.className = 'slc-confirm-modal'; modal.innerHTML = `
${escHtml(msg)}
`; document.body.appendChild(modal); requestAnimationFrame(() => requestAnimationFrame(() => modal.classList.add('visible'))); let done = false; const close = (result) => { if (done) return; done = true; modal.classList.remove('visible'); setTimeout(() => modal.remove(), 250); resolve(result); }; modal.querySelector('.slc-confirm-ok').addEventListener('click', () => close(true)); modal.querySelector('.slc-confirm-cancel').addEventListener('click', () => close(false)); modal.addEventListener('click', e => { if (e.target === modal) close(false); }); document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { close(false); document.removeEventListener('keydown', esc); } }); }); } // ==================== SVG 公共属性 ==================== const _S = 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"'; // ==================== 分类 ==================== const MATCH_CFG = [ { id:'store', color:'#3b82f6', icon:``, kw:['Steam 商店','Steam Store'] }, { id:'retail', color:'#eab308', icon:``, kw:['零售','Retail'] }, { id:'free', color:'#10b981', icon:``, kw:['免费赠送','贈品','Complimentary'] }, { id:'gift', color:'#ec4899', icon:``, kw:['礼物','玩家通行证','禮物','招待券','Gift','Guest Pass'] } ]; const CATS = ['store','retail','gift','free'].map(id => { const c = MATCH_CFG.find(x => x.id === id); return { id, color: c.color, icon: c.icon, get label() { return t(id); } }; }); const ALL_TYPE = Object.freeze({ id:'all', get label(){ return t('all'); }, color:'#4792c4', icon:`` }); const CAT_RE = new Map(MATCH_CFG.map(c => [ new RegExp(c.kw.map(s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|'),'i'), c.id ])); const isNoFree = () => state.noFree && state.filter !== 'free'; const chartCounts = () => { if (!isNoFree()) return state.counts; const m = new Map(CATS.map(c => [c.id, 0])); let tot = 0; for (const c of CATS) { if (c.id==='free') continue; const v = state.counts.get(c.id)||0; m.set(c.id,v); tot+=v; } m.set('total',tot); return m; }; const chartCats = () => isNoFree() ? CATS.filter(c=>c.id!=='free') : CATS; const SEL = { tbl:'table.account_table', tb:'table.account_table tbody', hdr:'h2', fc:'#steam-license-filter' }; const VER = GM_info.script.version; const disposers = []; const state = { filter:'all', filterCls:'', search:'', page:1, showAll:false, totalPages:1, counts: new Map([...CATS.map(c=>[c.id,0]),['total',0]]), dateFrom:'', dateTo:'', sortBy:'date-desc', noFree:false, isProcessing: false, isLoading: false, cachedTb: null, cachedDataRows: null, timers: { search: null, obs: null }, obs: null }; const getSessionID = () => { try { return unsafeWindow.g_sessionID || window.g_sessionID || ''; } catch { return window.g_sessionID || ''; } }; // ==================== 样式 ==================== const _rgba = (h, a) => { const v = parseInt(h.slice(1), 16); return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${a})`; }; const _catCSS = CATS.flatMap(c => [ `table.account_table tbody tr[data-category="${c.id}"]{background:${_rgba(c.color, .15)}}`, `table.account_table tbody tr[data-category="${c.id}"]:hover{background:${_rgba(c.color, .25)}}`, `table.account_table tbody.filter-${c.id} tr[data-category]:not([data-category="${c.id}"]){display:none}` ]); const CSS_CRITICAL = [ ':root{--bg-page:#0B1120;--bg-card:#1E293B;--border-card:#334155;--text-primary:#F1F5F9;--text-secondary:#94A3B8;--accent-blue:#3B82F6;--accent-amber:#F59E0B;--accent-pink:#EC4899;--accent-green:#10B981;--accent-purple:#8B5CF6;--radius-md:8px;--radius-lg:12px;--shadow-sm:0 2px 8px rgba(0,0,0,.3);--transition:all .2s ease}', 'table.account_table{margin-top:0;border-collapse:separate;border-spacing:0;contain:layout paint}', 'table.account_table tr{transition:background-color .2s ease;contain:layout}', `table.account_table th{position:sticky;top:var(--filter-h,${CFG.STICKY}px);z-index:99;background:#0B1120;text-transform:none;font-size:13px;font-weight:bold;color:#F1F5F9;padding:12px 12px;border-bottom:1px solid #334155}`, `table.account_table td.license_date_col{width:${CFG.DATE_W}px!important;min-width:${CFG.DATE_W}px;white-space:nowrap;text-align:left}`, 'table.account_table th.license_date_col{text-align:left}', 'table.account_table td{padding:12px 12px;border-bottom:1px solid rgba(51,65,85,.5);color:#F1F5F9;vertical-align:middle}', 'table.account_table tr:hover td{background:rgba(59,130,246,.08)}', '#steam-license-filter{margin:0 0 4px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius-lg);backdrop-filter:blur(4px);position:sticky;top:0;z-index:100;contain:layout;display:flex;flex-direction:column;gap:12px}', `.stats-row{display:flex;gap:10px;flex-wrap:wrap}`, `.stat-card{background:rgba(15,23,42,.6);border:1px solid var(--border-card);border-radius:var(--radius-md);padding:10px 16px;min-width:100px;text-align:center;cursor:pointer;position:relative;overflow:hidden}`, `.stat-card:hover{transform:translateY(-2px);border-color:var(--card-color,var(--accent-blue));background:rgba(59,130,246,.1);box-shadow:var(--shadow-sm)}`, `.stat-card.active{border-color:var(--card-color,var(--accent-blue));background:rgba(59,130,246,.15);box-shadow:var(--shadow-sm)}`, `.stat-card.active .stat-number,.stat-card.active .stat-label{color:var(--card-color,var(--accent-blue))}`, `.stat-card.disabled{opacity:.4;cursor:not-allowed}`, `.stat-number{font-size:1.5rem;font-weight:700;line-height:1.2;color:var(--text-primary)}`, `.stat-label{font-size:.7rem;letter-spacing:.5px;color:var(--text-secondary);margin-top:2px;display:flex;align-items:center;justify-content:center;gap:3px}`, `.stat-label svg{width:12px;height:12px;flex-shrink:0}`, `.stat-bar{height:3px;background:rgba(255,255,255,.08);border-radius:2px;margin-top:6px;overflow:hidden}`, `.stat-bar-fill{height:100%;border-radius:2px;transition:width .3s ease}`, `.stat-tools{display:flex;align-items:center;gap:4px;flex-wrap:wrap}`, `.tool-btn{background:rgba(15,23,42,.6)!important;border:1px solid var(--border-card)!important;border-radius:var(--radius-md)!important;padding:0 10px!important;font-size:13px!important;height:34px!important;width:auto!important;color:var(--text-secondary)!important;cursor:pointer;transition:var(--transition);display:flex;align-items:center;justify-content:center;box-sizing:border-box}`, `.tool-btn:hover{background:rgba(59,130,246,.2)!important;color:var(--text-primary)!important;border-color:var(--accent-blue)!important;transform:translateY(-1px)}`, `.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:10px}`, `.toolbar .search-box{flex:2;min-width:180px;background:#0F172A;border:1px solid var(--border-card);border-radius:40px;padding:8px 16px;color:#F1F5F9;font-size:13px;outline:none;transition:var(--transition)}`, `.toolbar .search-box::placeholder{color:var(--text-secondary)}`, `.toolbar .search-box:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`, `.adv-group{display:flex;align-items:center;gap:6px;flex-wrap:wrap}`, `.adv-lbl{color:var(--text-secondary);font-size:12px;white-space:nowrap}`, `.adv-date{position:relative;background:#0F172A!important;border:1px solid var(--border-card);border-radius:40px;color:#F1F5F9!important;padding:8px 16px;font-size:13px;outline:none;transition:var(--transition);color-scheme:dark;height:36px;width:140px;-webkit-appearance:none;appearance:none;box-sizing:border-box;line-height:normal;text-align:center}`, `.adv-date::-webkit-calendar-picker-indicator{opacity:0;cursor:pointer;width:20px;height:20px}`, `.adv-date::after{content:'';position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;pointer-events:none;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E") no-repeat center}`, `.adv-date::-webkit-datetime-edit,.adv-date::-webkit-datetime-edit-fields-wrapper,.adv-date::-webkit-datetime-edit-text,.adv-date::-webkit-datetime-edit-month-field,.adv-date::-webkit-datetime-edit-day-field,.adv-date::-webkit-datetime-edit-year-field{background:transparent;color:#F1F5F9}`, `.adv-date:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`, `.adv-sort{background:#0F172A;border:1px solid var(--border-card);border-radius:40px;color:#F1F5F9;padding:8px 16px;font-size:13px;outline:none;transition:var(--transition);color-scheme:dark;height:36px;-webkit-appearance:none;appearance:none;box-sizing:border-box;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px;cursor:pointer}`, `.adv-sort option{background:#0B1120;color:var(--text-primary)}`, `.adv-sort:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`, `.adv-clr{background:rgba(15,23,42,.6)!important;border:1px solid var(--border-card)!important;border-radius:40px!important;padding:0 10px!important;font-size:13px!important;height:36px!important;width:auto!important;color:var(--text-secondary)!important;cursor:pointer;transition:var(--transition);display:flex;align-items:center;justify-content:center;box-sizing:border-box}`, `.adv-clr:hover{background:rgba(59,130,246,.2)!important;color:var(--text-primary)!important;border-color:var(--accent-blue)!important;transform:translateY(-1px)}`, `.pager{display:flex;align-items:center;gap:6px;margin-left:auto}`, `#steam-license-filter button.pager-btn{background:rgba(15,23,42,.6);border:1px solid var(--border-card);border-radius:var(--radius-md);padding:6px 14px;font-size:13px;height:auto;color:var(--text-primary);cursor:pointer;transition:var(--transition)}`, `#steam-license-filter button.pager-btn:hover{background:rgba(59,130,246,.2);transform:translateY(-1px)}`, `#steam-license-filter button.pager-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}`, `.page-info{display:inline-flex;align-items:center;gap:2px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-secondary)}`, `.page-input{width:30px;text-align:center;background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:4px;color:#c7d5e0;font-size:13px;padding:2px 4px;outline:none}`, `.page-input:focus{border-color:var(--accent-blue)}`, `#steam-license-filter button{border:2px solid transparent;border-radius:6px;background:transparent;color:var(--text-primary);cursor:pointer;font-weight:500;transition:var(--transition);outline:none;will-change:transform}`, `#steam-license-filter button:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}`, ..._catCSS, 'table.account_table tbody.paginated tr[data-category]:not(.page-visible){display:none}', 'table.account_table tbody tr[data-category="other"]{display:none}', 'h2 .search-box{display:none}', '#slc-no-free{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;user-select:none;font-size:12px;color:var(--text-secondary);white-space:nowrap}', '#slc-no-free input[type="checkbox"]{position:absolute;opacity:0;width:0;height:0}', '#slc-no-free .tg-track{position:relative;width:44px;height:20px;background:#1E293B;border:1px solid var(--border-card);border-radius:10px;cursor:pointer;transition:var(--transition);flex-shrink:0}', '#slc-no-free .tg-track::after{content:"";position:absolute;left:2px;top:2px;width:14px;height:14px;background:var(--text-secondary);border-radius:50%;transition:var(--transition)}', '#slc-no-free input:checked+.tg-track{background:rgba(139,92,246,.3);border-color:var(--accent-purple)}', '#slc-no-free input:checked+.tg-track::after{left:26px;background:var(--accent-purple)}', '.stats[class*="svelte-"]{display:none!important}', '#license-stats{display:none}', '#steam-license-filter button[data-action]:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.4);background:rgba(59,130,246,.2)}', '#steam-license-filter button[data-action]:disabled{opacity:.4;cursor:not-allowed}', '#steam-license-filter button[data-action].active-mode{background:var(--accent-purple);color:#fff}', 'table.account_table tbody.search-filtered tr[data-category]:not(.search-match){display:none}', `@media(max-width:899px){.stat-card{min-width:80px;padding:8px 10px}.stat-number{font-size:1.2rem}.toolbar .search-box{min-width:140px}.stat-tools{gap:3px}.tool-btn{padding:0 6px!important;height:30px!important}.adv-date{width:115px;padding:6px 10px;font-size:12px;height:32px}}`, `@media(max-width:599px){.stat-card{min-width:70px;padding:6px 8px}.stat-number{font-size:1rem}.pager-btn{padding:4px 8px!important;font-size:.75rem!important}.stat-tools{gap:2px}.tool-btn svg{width:14px;height:14px}.tool-btn{height:26px!important}#slc-no-free{font-size:11px}.adv-date{width:105px;height:30px;padding:6px 8px;font-size:12px}.adv-sort{font-size:12px;height:30px;padding:6px 10px}}`, '.slc-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:10001;opacity:0;transform:translateY(10px);transition:all .3s ease;pointer-events:auto;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.4)}', '.slc-toast--visible{opacity:1;transform:translateY(0)}', '.slc-toast--info{background:#2a475e;color:#c7d5e0;border:1px solid #4a6a7a}', '.slc-toast--success{background:rgba(16,185,129,.9);color:#fff}', '.slc-toast--error{background:rgba(239,68,68,.9);color:#fff}', '.slc-toast--warning{background:rgba(245,158,11,.9);color:#fff}', '.slc-load-bar{width:100%;height:4px;background:rgba(255,255,255,.08);overflow:hidden;border-radius:2px;margin:0 0 4px}', '.slc-load-bar.hidden{opacity:0;transition:opacity .3s ease}', '.slc-load-bar-fill{height:100%;width:0;background:linear-gradient(90deg,var(--accent-blue),var(--accent-purple));border-radius:2px;transition:width .3s ease}', '.slc-load-status{display:inline;margin-left:8px;font-size:13px;color:var(--text-secondary);font-weight:400;white-space:nowrap;animation:slc-pulse 1.5s ease-in-out infinite}', '@keyframes slc-pulse{0%,100%{opacity:.6}50%{opacity:1}}' ].join(''); const CSS_DEFERRED = [ '.slc-modal{position:fixed;inset:0;background:rgba(11,17,32,.85);backdrop-filter:blur(4px);z-index:10000;display:flex;opacity:0;visibility:hidden}', '#slc-donut-modal,#slc-year-modal,#slc-predict-modal,#slc-trend-modal,#slc-year-month-modal,#slc-similar-modal,#slc-cal-modal,#slc-redeem-modal,#slc-delisted-modal{align-items:center;justify-content:center}', '.slc-modal.visible{opacity:1;visibility:visible}', '.slc-mc{background:rgba(30,41,59,.95);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.1);border-radius:24px;padding:28px;box-shadow:0 8px 32px rgba(0,0,0,.5);width:90%;position:relative;transform:scale(.95);opacity:0;transition:transform .3s ease,opacity .3s ease}', '.slc-modal.visible .slc-mc{transform:scale(1);opacity:1}', '#slc-donut-c,#slc-predict-c,#slc-trend-c,#slc-similar-c,#slc-year-c,#slc-year-month-c{max-width:850px;contain:layout}', '#slc-redeem-c{max-width:850px;display:flex;flex-direction:column}', '#slc-cal-c{max-width:880px;height:650px;display:flex;flex-direction:column}', '#slc-cal-body{flex:1;overflow-y:auto;min-height:0}', '.slc-mx{position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;font-size:22px;cursor:pointer;padding:4px 8px;border-radius:8px;transition:all .2s}', '.slc-mx:hover{background:rgba(255,255,255,.1);color:#94a3b8;transform:scale(1.1)}', '.slc-mh{display:flex;align-items:center;gap:14px;margin-bottom:20px}', '.slc-mh-i{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0}', '.slc-mh-i svg{width:22px;height:22px;stroke:#fff;fill:none}', '.slc-mh-t{font-size:20px;font-weight:700;color:#f8fafc;flex:1}', '.slc-mf{margin-top:20px;padding-top:16px;border-top:1px solid #334155;display:flex;align-items:flex-start;gap:10px}', '.slc-mf svg{width:16px;height:16px;stroke:#64748b;fill:none;flex-shrink:0;margin-top:1px}', '.slc-mf-t{font-size:13px;color:#64748b;line-height:1.5}', /* ===== 窗口导航栏 ===== */ '.slc-nav-sidebar{position:absolute;left:-52px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:6px;z-index:1}', '.slc-nav-btn{width:42px;height:42px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:rgba(30,41,59,.9);color:#94a3b8;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;backdrop-filter:blur(8px)}', '.slc-nav-btn svg{width:20px;height:20px;stroke:currentColor;fill:none}', '.slc-nav-btn:hover{background:rgba(51,65,85,.9);color:#f8fafc;border-color:rgba(255,255,255,.15);transform:scale(1.05)}', '.slc-nav-btn.active{color:#fff}', '#slc-donut-hi,#slc-redeem-hi,#slc-trend-hi{background:#3b82f6}', '#slc-year-hi,#slc-year-month-hi{background:#8b5cf6}', '#slc-predict-hi{background:#f59e0b}', '#slc-similar-hi{background:#f97316}', '#slc-cal-hi{background:#10b981}', '#slc-delisted-hi{background:#f44336}', '.slc-cal-container{display:flex;flex-direction:column;gap:24px}', '.slc-cal-year{display:flex;flex-direction:column;gap:8px}', '.slc-cal-year-label{font-size:16px;font-weight:600;color:#f8fafc}', '.slc-cal-grid{display:flex;flex-direction:column;gap:4px}', '.slc-cal-months{display:flex;gap:4px;padding-left:32px}', '.slc-cal-months span{flex:1;text-align:center;font-size:11px;color:#64748b}', '.slc-cal-days{display:flex;gap:4px}', '.slc-cal-weekdays{display:flex;flex-direction:column;gap:4px;width:24px}', '.slc-cal-weekdays span{font-size:10px;color:#64748b;text-align:center;height:14px;line-height:14px}', '.slc-cal-cells{display:grid;grid-template-columns:repeat(53,1fr);grid-template-rows:repeat(7,1fr);grid-auto-flow:column;gap:2px;flex:1}', '.slc-cal-cell{width:14px;height:14px;border-radius:2px;transition:all .15s}', '.slc-cal-cell:hover{transform:scale(1.2);z-index:1}', '.slc-cal-empty{background:transparent}', '.slc-cal-legend{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:16px;padding:16px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}', '.slc-cal-legend-label{font-size:12px;color:#64748b}', '.slc-cal-legend-cells{display:flex;gap:3px}', '.slc-cal-legend-cells div{width:14px;height:14px;border-radius:2px}', '#slc-donut-legend .li{display:flex;align-items:center;gap:6px;padding:6px 10px;background:rgba(0,0,0,.2);border-radius:6px;transition:all .2s}', '#slc-donut-legend .li:hover{background:rgba(0,0,0,.3);transform:translateY(-2px)}', '#slc-donut-legend .lc{width:12px;height:12px;border-radius:3px;flex-shrink:0}', '#slc-donut-legend .ll{color:#c7d5e0;font-size:12px;white-space:nowrap}', '#slc-donut-legend .lv{color:#8a9ba8;font-size:11px;white-space:nowrap}', '#slc-donut-layout{display:flex;gap:16px;align-items:stretch}', '#slc-donut-left{flex:1;aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}', '#slc-donut-right{flex:1;aspect-ratio:1;display:flex;flex-direction:column;justify-content:center;padding:20px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}', '#slc-donut-right .bar-chart{display:flex;align-items:flex-end;justify-content:space-around;height:100%;gap:12px;padding-top:20px}', '#slc-donut-right .bar-col{display:flex;flex-direction:column;align-items:center;width:60px;height:100%;justify-content:flex-end}', '#slc-donut-right .bar-track-v{width:100%;height:100%;background:rgba(255,255,255,.06);border-radius:6px 6px 0 0;position:relative;display:flex;flex-direction:column;justify-content:flex-end}', '#slc-donut-right .bar-fill-v{width:100%;border-radius:6px 6px 0 0;transition:height .3s ease}', '#slc-donut-right .bar-value{font-size:12px;color:#c7d5e0;margin-bottom:4px;font-weight:600}', '#slc-donut-right .bar-label{font-size:11px;color:#94a3b8;margin-top:6px;text-align:center}', '#slc-donut-legend{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:16px;padding:16px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}', '#slc-year-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;flex-wrap:wrap;padding:16px;background:#1e293b;border:1px solid #334155;border-radius:12px}', '#slc-year-legend .yl{display:flex;align-items:center;gap:6px;font-size:13px;color:#c7d5e0}', '#slc-year-legend .yc{width:12px;height:12px;border-radius:2px;flex-shrink:0}', '#slc-redeem-c .ra{display:flex;gap:8px;margin-top:16px;align-items:center;flex-wrap:wrap;justify-content:flex-end}', '#slc-rcf{margin-left:auto}', '#slc-rstats{display:flex;gap:8px;flex:1;min-width:0}', '#slc-rstats .pc{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;min-width:90px}', '#slc-rstats .pc-i{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}', '#slc-rstats .pc-i svg{width:14px;height:14px}', '#slc-rstats .pc-i.blue{background:rgba(59,130,246,.15)}', '#slc-rstats .pc-i.green{background:rgba(34,197,94,.15)}', '#slc-rstats .pc-i.red{background:rgba(239,68,68,.15)}', '#slc-rstats .pc-t{display:flex;align-items:center;gap:6px;min-width:0;flex:1}', '#slc-rstats .pc-l{font-size:10px;color:#64748b;letter-spacing:.3px}', '#slc-rstats .pc-v{font-size:15px;font-weight:700;color:#f8fafc}', '#slc-rt,#slc-pt{width:100%;border-collapse:collapse;font-size:13px;background:#1e293b;border:1px solid #334155;overflow:hidden}', '#slc-rt{margin-top:12px;border-radius:12px;flex:1;overflow-y:auto;display:block}', '#slc-pt{border-radius:10px}', '#slc-rt th,#slc-pt th{padding:8px 10px;text-align:left;color:#8f98a0;font-size:12px;text-transform:uppercase;letter-spacing:.5px;background:rgba(0,0,0,.4);border-bottom:1px solid rgba(255,255,255,.08)}', '#slc-rt td,#slc-pt td{padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.05);color:#c7d5e0;vertical-align:middle}', '#slc-rt tr:hover td,#slc-pt tr:hover td{background:rgba(255,255,255,.03)}', '#slc-rt code{color:#66c0f4;font-size:13px;background:rgba(0,0,0,.2);padding:1px 5px;border-radius:3px}', '#slc-rt a{color:#66c0f4;text-decoration:none;transition:color .15s}', '#slc-rt a:hover{color:#fff}', '#slc-ps{display:flex;flex-direction:column;gap:8px;min-width:160px}', '#slc-ps .pc{background:#1e293b;border:1px solid #334155;border-radius:10px;padding:14px;display:flex;align-items:center;gap:12px}', '#slc-ps .pc-i{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}', '#slc-ps .pc-i svg{width:20px;height:20px;stroke:#fff;fill:none}', '#slc-ps .pc-i.blue{background:rgba(59,130,246,.15)}#slc-ps .pc-i.blue svg{stroke:#3b82f6}', '#slc-ps .pc-i.purple{background:rgba(139,92,246,.15)}#slc-ps .pc-i.purple svg{stroke:#8b5cf6}', '#slc-ps .pc-i.cyan{background:rgba(6,182,212,.15)}#slc-ps .pc-i.cyan svg{stroke:#06b6d4}', '#slc-ps .pc-i.green{background:rgba(34,197,94,.15)}#slc-ps .pc-i.green svg{stroke:#22c55e}', '#slc-ps .pc-i.orange{background:rgba(245,158,11,.15)}#slc-ps .pc-i.orange svg{stroke:#f59e0b}', '#slc-ps .pc-i.rose{background:rgba(236,72,153,.15)}#slc-ps .pc-i.rose svg{stroke:#ec4899}', '#slc-ps .pc-t{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}', '#slc-ps .pc-l{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:.3px}', '#slc-ps .pc-v{font-size:18px;font-weight:700;color:#f8fafc}', '#slc-ps .pc-v.ac{color:#ec4899}', '#slc-pb{display:flex;gap:20px;align-items:flex-start}', '#slc-pr{flex:1;min-width:0}', '#slc-pt .pb{height:5px;background:#334155;border-radius:2px;margin-top:3px;overflow:hidden}', '#slc-pt .pb-f{height:100%;background:#f59e0b;border-radius:2px;transition:width .3s ease}', '#slc-redeem-c textarea,#slc-redeem-c input[type="text"]{background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:6px;padding:8px 14px;color:#c7d5e0;font-size:13px;outline:none;transition:all .2s ease;box-sizing:border-box}', '#slc-redeem-c textarea:focus,#slc-redeem-c input[type="text"]:focus{border-color:#66c0f4;background:rgba(0,0,0,.4);box-shadow:0 0 0 2px rgba(102,192,244,.2)}', '#slc-redeem-c button.rb{border:none;border-radius:2px;padding:8px 20px;font-size:13px;font-weight:400;cursor:pointer;transition:all .15s ease;outline:none;text-transform:uppercase;letter-spacing:.5px}', '#slc-redeem-c button.rb:hover{filter:brightness(1.2)}', '#slc-redeem-c button.rb:active{filter:brightness(.9)}', '#slc-redeem-c button.rb:disabled{opacity:.4;cursor:not-allowed;filter:none}', '#slc-redeem-c button.rb1{background:linear-gradient(to right,#47bfff 5%,#1a44c2 60%);color:#fff}', '#slc-redeem-c button.rb1:hover:not(:disabled){background:linear-gradient(to right,#5ccfff 5%,#2555d5 60%)}', '#slc-redeem-c button.rb2{background:rgba(0,0,0,.4);color:#8f98a0;border:1px solid rgba(255,255,255,.08)}', '#slc-redeem-c button.rb2:hover:not(:disabled){background:rgba(0,0,0,.6);color:#c6d4df;border-color:rgba(255,255,255,.15)}', '.slc-edd{position:relative;display:inline-flex;z-index:999999}', '.slc-edd .em{display:none;position:absolute;top:100%;right:0;margin-top:4px;background:#0F172A;border:1px solid var(--border-card);border-radius:8px;overflow:hidden;z-index:99999;box-shadow:0 4px 12px rgba(0,0,0,.4);min-width:100px}', '.slc-edd.open .em{display:block}', '.slc-edd .em button{display:block;width:100%;padding:8px 16px;background:none;border:none;color:#F1F5F9;font-size:13px;cursor:pointer;text-align:left;white-space:nowrap;transition:background .15s}', '.slc-edd .em button:hover{background:rgba(59,130,246,.2);color:var(--accent-blue)}', /* ===== 绝版游戏检查窗口 ===== */ '#slc-delisted-c{max-width:900px;width:90vw;height:650px;display:flex;flex-direction:column}', '#slc-delisted-body{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}', '.slc-delisted-header{flex-shrink:0;padding-bottom:8px}', '.slc-delisted-scroll{flex:1;overflow-y:auto;min-height:0;padding-right:4px;overscroll-behavior:auto}', '.slc-delisted-kpi-row{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}', '.slc-delisted-kpi-card{flex:1;min-width:100px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:10px 14px;text-align:center;transition:all .2s}', '.slc-delisted-kpi-card:hover{background:rgba(255,255,255,.06)}', '.slc-delisted-kpi-card.active{background:rgba(255,255,255,.1);box-shadow:0 0 0 2px rgba(255,255,255,.15) inset}', '.slc-delisted-kpi-value{font-size:1.3rem;font-weight:700;color:#f8fafc}', '.slc-delisted-kpi-label{font-size:11px;color:#64748b;margin-top:2px}', '.slc-delisted-progress{height:5px;background:#334155;border-radius:2px;overflow:hidden;margin-bottom:12px}', '.slc-delisted-progress-fill{height:100%;background:linear-gradient(90deg,#22c55e,#66c0f4);border-radius:2px;transition:width .3s ease}', '.slc-delisted-type-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}', '.slc-delisted-type-btn{font-size:11px;padding:4px 10px;border-radius:6px;cursor:pointer;transition:all .2s;font-weight:500;white-space:nowrap}', '.slc-delisted-type-btn:hover{transform:translateY(-1px);filter:brightness(1.2)}', '.slc-delisted-type-btn.active{font-weight:700;box-shadow:0 2px 6px rgba(0,0,0,.3)}', '.slc-delisted-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}', '.slc-delisted-toolbar input[type=text]{flex:1;min-width:160px;background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:20px;padding:6px 14px;color:#c7d5e0;font-size:12px;outline:none;transition:all .2s}', '.slc-delisted-toolbar input[type=text]:focus{border-color:#66c0f4;box-shadow:0 0 0 2px rgba(102,192,244,.2)}', '.slc-delisted-toolbar button,.slc-delisted-pagination button{background:rgba(15,23,42,.6)!important;border:1px solid #334155!important;border-radius:6px!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}', '.slc-delisted-toolbar button:hover,.slc-delisted-pagination button:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}', '.slc-delisted-toolbar button.active{background:rgba(102,192,244,.2)!important;border-color:rgba(102,192,244,.4)!important;color:#66c0f4!important}', '.slc-delisted-toolbar .slc-dlst-sort-group{display:flex;gap:0;border:1px solid #334155;border-radius:6px;overflow:hidden}', '.slc-delisted-toolbar .slc-dlst-sort-btn{background:rgba(15,23,42,.6)!important;border:none!important;border-right:1px solid #334155!important;border-radius:0!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}', '.slc-delisted-toolbar .slc-dlst-sort-btn:last-child{border-right:none!important}', '.slc-delisted-toolbar .slc-dlst-sort-btn:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}', '.slc-delisted-toolbar .slc-dlst-sort-btn.active{background:rgba(102,192,244,.2)!important;color:#66c0f4!important}', '.slc-delisted-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px}', '.slc-delisted-card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:10px 14px;transition:background .2s,border-color .2s;cursor:pointer;position:relative}', '.slc-delisted-card:hover{background:rgba(255,255,255,.08);border-color:rgba(244,67,54,.3)}', '.slc-delisted-card-name{font-size:13px;font-weight:600;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}', '.slc-delisted-card-meta{font-size:11px;color:#64748b;display:flex;align-items:center;gap:6px;flex-wrap:wrap}', '.slc-delisted-badge{display:inline-block;font-size:10px;font-weight:600;padding:1px 6px;border-radius:3px}', '.slc-delisted-badge.type-delisted{background:rgba(244,67,54,.2);color:#f44336}', '.slc-delisted-badge.type-purchase_disabled{background:rgba(255,152,0,.2);color:#ff9800}', '.slc-delisted-badge.type-f2p_unavailable{background:rgba(156,39,176,.2);color:#ce93d8}', '.slc-delisted-badge.type-retail_only{background:rgba(33,150,243,.2);color:#64b5f6}', '.slc-delisted-badge.type-test_app{background:rgba(38,198,218,.2);color:#26c6da}', '.slc-delisted-card .owned-badge{position:absolute;bottom:6px;right:6px;font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600}', '.slc-delisted-card .owned-badge.owned{background:rgba(34,197,94,.2);color:#22c55e}', '.slc-delisted-card .owned-badge.missing{background:rgba(244,67,54,.15);color:#f44336}', '.slc-delisted-card-achievements,.slc-delisted-card-owners{font-size:10px}', '.slc-delisted-card-achievements{color:#66c0f4}', '.slc-delisted-card-owners{color:#94a3b8}', '.slc-delisted-list{display:flex;flex-direction:column;gap:4px}', '.slc-delisted-list-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.05);border-radius:6px;cursor:pointer;transition:background .2s,border-color .2s}', '.slc-delisted-list-item:hover{background:rgba(255,255,255,.06);border-color:rgba(244,67,54,.2)}', '.slc-delisted-list-item .name{flex:1;font-size:12px;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.slc-delisted-list-item .meta{display:flex;align-items:center;gap:6px;flex-shrink:0}', '.slc-delisted-list-item.has-cover{gap:12px}', '.slc-delisted-cover{width:120px;height:45px;border-radius:4px;object-fit:cover;flex-shrink:0;background:rgba(255,255,255,.05)}', '.slc-delisted-cover-btn{background:rgba(15,23,42,.6)!important;border:1px solid #334155!important;border-radius:6px!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}', '.slc-delisted-cover-btn:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}', '.slc-delisted-cover-btn.active{background:rgba(102,192,244,.2)!important;border-color:rgba(102,192,244,.4)!important;color:#66c0f4!important}', '.slc-delisted-pagination{display:flex;align-items:center;gap:6px;justify-content:center}', '.slc-delisted-pagination button:disabled{opacity:.4;cursor:not-allowed}', '.slc-delisted-pagination .page-info{font-size:12px;color:#64748b}', '.slc-delisted-loading{text-align:center;padding:40px;color:#64748b;font-size:14px}', '.slc-delisted-loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid rgba(102,192,244,.2);border-top-color:#66c0f4;border-radius:50%;animation:slc-spin .8s linear infinite;margin-bottom:10px}', '@keyframes slc-spin{to{transform:rotate(360deg)}}', /* ===== 确认弹窗 ===== */ '.slc-confirm-modal{position:fixed;inset:0;background:rgba(11,17,32,.85);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:10001;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity .25s ease}', '.slc-confirm-modal.visible{opacity:1;visibility:visible}', '.slc-confirm-card{background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius-lg);padding:28px 32px 24px;width:90%;max-width:380px;box-shadow:0 8px 32px rgba(0,0,0,.5);transform:scale(.92);opacity:0;transition:transform .25s ease,opacity .25s ease;text-align:center}', '.slc-confirm-modal.visible .slc-confirm-card{transform:scale(1);opacity:1}', '.slc-confirm-icon{width:52px;height:52px;margin:0 auto 16px;border-radius:50%;background:rgba(239,68,68,.15);display:flex;align-items:center;justify-content:center}', '.slc-confirm-icon svg{width:26px;height:26px;stroke:#ef4444;fill:none}', '.slc-confirm-msg{font-size:14px;color:var(--text-primary);line-height:1.6;margin-bottom:24px}', '.slc-confirm-btns{display:flex;gap:10px;justify-content:center}', '.slc-confirm-cancel{flex:1;padding:9px 0;border-radius:var(--radius-md);font-size:13px;font-weight:500;cursor:pointer;transition:var(--transition);background:rgba(255,255,255,.06);border:1px solid var(--border-card);color:var(--text-secondary)}', '.slc-confirm-cancel:hover{background:rgba(255,255,255,.12);color:var(--text-primary)}', '.slc-confirm-ok{flex:1;padding:9px 0;border-radius:var(--radius-md);font-size:13px;font-weight:600;cursor:pointer;transition:var(--transition);background:rgba(239,68,68,.9);border:none;color:#fff}', '.slc-confirm-ok:hover{background:rgba(220,38,38,.9);transform:translateY(-1px)}' ].join(''); function injectCSS(id, text) { if (document.getElementById(id)) return; const s = h('style', { id, text }); document.head.appendChild(s); disposers.push(() => s.remove()); } // ==================== 工具函数 ==================== const escHtml = s => String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/\n/g,' '); const noDataHtml = () => `
${escHtml(t('noData'))}
`; const fmtD = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; function getTb() { if (state.cachedTb && !state.cachedTb.isConnected) { state.cachedTb = null; startObs(); } return state.cachedTb || (state.cachedTb = document.querySelector(SEL.tb)); } function waitEl(sel, ms = 10000) { return new Promise((res, rej) => { const el = document.querySelector(sel); if (el) return res(el); let done = false; const fin = e => { if (done) return; done = true; obs.disconnect(); clearTimeout(t); res(e); }; const root = document.querySelector('#page_body') || document.documentElement; const obs = new MutationObserver(() => { const e = document.querySelector(sel); if (e) fin(e); }); obs.observe(root, { childList: true, subtree: true }); const t = setTimeout(() => { if (!done) { done = true; obs.disconnect(); rej(new Error(`waitEl timeout: ${sel}`)); } }, ms); }); } function classify(text) { const s = text.trim(); for (const [re, id] of CAT_RE) { if (re.test(s)) return id; } return 'other'; } const isHdr = row => { if (row.querySelector('th')) return true; const d = row.querySelector('td.license_date_col')?.textContent?.trim() || ''; return d.length > 0 && !/\d/.test(d); }; function dataRows() { if (state.cachedDataRows) return state.cachedDataRows; const tb = getTb(); const rows = tb ? Array.from(tb.rows).filter(r => !isHdr(r)) : []; state.cachedDataRows = rows; return rows; } const invalidateDataRows = () => { state.cachedDataRows = null; }; const MON = { jan:0,feb:1,mar:2,apr:3,may:4,jun:5,jul:6,aug:7,sep:8,oct:9,nov:10,dec:11 }; function parseDate(str) { const s = str.trim(); const m1 = s.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/); if (m1) return new Date(+m1[1],+m1[2]-1,+m1[3]).getTime(); const m2 = s.match(/([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})/); if (m2) { const mo = MON[m2[1].toLowerCase().slice(0,3)]; if (mo !== undefined) return new Date(+m2[3],mo,+m2[2]).getTime(); } const m3 = s.match(/(\d{1,2})\s+([A-Za-z]+),?\s+(\d{4})/); if (m3) { const mo = MON[m3[2].toLowerCase().slice(0,3)]; if (mo !== undefined) return new Date(+m3[3],mo,+m3[1]).getTime(); } const d = new Date(s); return isNaN(d) ? null : d.getTime(); } // ==================== DOM Helper ==================== const BOOL_A = new Set(['disabled','checked','selected','readonly','required','hidden','multiple','autofocus','open']); function h(tag, p = {}, ch = []) { const el = document.createElement(tag); for (const [k, v] of Object.entries(p)) { if (v == null) continue; if (k === 'style' && typeof v === 'object') { for (const [sk, sv] of Object.entries(v)) { sk.startsWith('--') ? el.style.setProperty(sk, sv) : el.style[sk] = sv; } } else if (k === 'dataset') Object.assign(el.dataset, v); else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v); else if (k === 'text') el.textContent = v; else if (k === 'html') el.innerHTML = v; else if (BOOL_A.has(k)) { v ? el.setAttribute(k, '') : el.removeAttribute(k); } else el.setAttribute(k, v); } for (const c of ch) el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); return el; } // ==================== 昵称 ==================== const getNick = () => document.querySelector('#account_pulldown')?.textContent.trim() || document.querySelector('#global_header .user_persona')?.textContent.trim() || ''; function updateTitle() { const nick = getNick(); if (!nick) return; const hdr = document.querySelector(SEL.hdr); if (!hdr) return; const orig = hdr.dataset.origTitle || hdr.textContent.trim(); const title = t('pageTitle').replace('{nickname}', nick); let span = hdr.querySelector('.page-title-text'); if (!span) { span = h('span', { class:'page-title-text', text:title }); hdr.textContent = ''; hdr.appendChild(span); hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer'; span.style.cssText = 'max-width:500px;overflow-wrap:break-word'; } if (!hdr.dataset.titleInit) { hdr.dataset.titleInit = '1'; const handler = e => { if (e.target.closest('.search-box')) return; const show = hdr.dataset.showOrig === '1'; const newText = show ? hdr.dataset.newTitle : hdr.dataset.origTitle; // 只更新文本节点,保留子元素(如 .slc-load-status) const firstNode = span.firstChild; if (firstNode && firstNode.nodeType === Node.TEXT_NODE) { firstNode.nodeValue = newText; } else { span.insertBefore(document.createTextNode(newText), span.firstChild); } hdr.dataset.showOrig = show ? '0' : '1'; }; hdr.addEventListener('click', handler); disposers.push(() => hdr.removeEventListener('click', handler)); } else { span.textContent = title; } hdr.dataset.origTitle = orig; hdr.dataset.newTitle = title; document.title = title; } function addBreadcrumb() { const cur = document.querySelector('.blockbg .breadcrumb_current_page'); if (!cur || cur.dataset.done) return; cur.dataset.done = '1'; const p = cur.parentElement; p.appendChild(h('span', { class:'breadcrumb_separator', text:'>' })); p.appendChild(h('a', { href:'https://store.steampowered.com/account/history', target:'_blank', text:t('historyLink'), 'data-panel':'{"noFocusRing":true}' })); } // ==================== UI Refs ==================== const ui = { btns: new Map(), statTotal: null, statCounts: new Map(), statBars: new Map(), statCards: new Map(), pageTotal: null, prevBtn: null, nextBtn: null, showAllBtn: null, pageInput: null, searchInput: null, donutRefs: null, yearRefs: null, yearMonthRefs: null, predictRefs: null, trendRefs: null, similarRefs: null, resizeObs: null, trendVisible: null, trendCumulative: true, similarPrefixWords: undefined }; // ==================== Core Logic ==================== function markRows() { const tb = getTb(); if (!tb) return; try { for (const row of tb.rows) { if (isHdr(row)) { delete row.dataset.category; delete row.dataset.dateTimestamp; delete row.dataset.searchText; continue; } // 删除 "未找到许可" 的无效行 if (row.textContent.includes('未找到许可')) { row.remove(); continue; } if (row.dataset.category) continue; const dc = row.querySelector('td.license_date_col'); const nc = row.querySelector('td:not(.license_date_col):not(.license_acquisition_col):not(.steamdb_license_id_col)'); const ac = row.querySelector('td.license_acquisition_col'); if (!dc || !nc || !ac) continue; const raw = dc.textContent.trim(); row.dataset.dateTimestamp = parseDate(raw) ?? 0; const norm = raw.replace(/\s+/g,' ').trim().replace(/ *([年月日]) */g,'$1'); if (dc.textContent !== norm) dc.textContent = norm; row.dataset.category = classify(ac.textContent); // 排除 "移除" 按钮文字后提取游戏名(修复 #7:避免 cloneNode 开销) const removeLink = nc.querySelector('.free_license_remove_link'); const removeText = removeLink?.textContent?.trim() || ''; const fullText = nc.textContent?.trim() || ''; row.dataset.gameName = removeText ? fullText.replace(removeText, '').trim() : fullText; row.dataset.searchText = row.dataset.gameName.toLowerCase(); } } catch (err) { console.error('[SLC] markRows error:', err); } } function recalc() { try { for (const k of state.counts.keys()) state.counts.set(k, 0); for (const row of dataRows()) { const cat = row.dataset.category; if (cat && state.counts.has(cat)) { state.counts.set(cat, state.counts.get(cat) + 1); state.counts.set('total', state.counts.get('total') + 1); } } } catch (err) { console.error('[SLC] recalc error:', err); } } function refreshNums(vis, catCnts) { const filtering = !!state.search || state.dateFrom || state.dateTo || state.noFree; const total = filtering ? [...catCnts.values()].reduce((s,v)=>s+v,0) : state.counts.get('total'); ui.statTotal.textContent = total; for (const c of CATS) { const val = filtering ? (catCnts.get(c.id)||0) : state.counts.get(c.id); const el = ui.statCounts.get(c.id); if (el) el.textContent = val; const bar = ui.statBars.get(c.id); if (bar) bar.style.width = total ? (val/total*100)+'%' : '0%'; } const totalBar = ui.statBars.get('all'); if (totalBar) totalBar.style.width = '100%'; } function lockWidths() { const d = String(state.counts.get('total')).length; if (ui.statTotal) ui.statTotal.style.minWidth = `${d}ch`; for (const [, el] of ui.statCounts) el.style.minWidth = `${Math.max(1,d-1)}ch`; } // 加载期间的轻量统计刷新:仅更新分类和数字,不做分页渲染 function refreshLoadingStats() { if (!ui.statTotal) return; // 面板尚未创建 invalidateDataRows(); markRows(); recalc(); lockWidths(); // 从 state.counts 构建显示用计数,处理排除免费(搜索/日期筛选在最终applyPage时生效) const catCnts = new Map(CATS.map(c => [c.id, state.noFree && c.id === 'free' ? 0 : state.counts.get(c.id)])); refreshNums([], catCnts); } function applyFilter(ft) { const tb = getTb(); if (!tb) return; state.filter = ft; if (state.filterCls) tb.classList.remove(state.filterCls); state.filterCls = ft !== 'all' ? `filter-${ft}` : ''; if (state.filterCls) tb.classList.add(state.filterCls); ui.btns.forEach((b, id) => b.classList.toggle('active', id === ft)); if (ui.statCards) ui.statCards.forEach((el, id) => { const card = el?.closest('.stat-card'); if (card) card.classList.toggle('active', id === ft); }); state.page = 1; applyPage(); } function sortRows(rows) { const [f, dir] = state.sortBy.split('-'); return rows.sort((a, b) => { let c = 0; if (f === 'date') c = (+a.dataset.dateTimestamp||0) - (+b.dataset.dateTimestamp||0); else if (f === 'name') c = (a.dataset.gameName||'').localeCompare(b.dataset.gameName||''); return dir === 'asc' ? c : -c; }); } function getVisible() { const all = dataRows(); const fromT = state.dateFrom ? new Date(state.dateFrom).getTime() : 0; const toT = state.dateTo ? new Date(state.dateTo).getTime() + CFG.DAY_MS : Infinity; const sq = state.search; const fcat = state.filter; const vis = [], catCnts = new Map(CATS.map(c=>[c.id,0])); for (const row of all) { const cat = row.dataset.category; if (!cat || cat === 'other') continue; const rd = +row.dataset.dateTimestamp||0; if (rd && (rd < fromT || rd > toT)) { row.classList.remove('search-match'); continue; } const match = !sq || (row.dataset.searchText||'').includes(sq); row.classList.toggle('search-match', match); if (!match) continue; if (state.noFree && cat === 'free') { row.classList.remove('search-match'); continue; } catCnts.set(cat, (catCnts.get(cat)||0)+1); if (fcat !== 'all' && cat !== fcat) { row.classList.remove('search-match'); continue; } vis.push(row); } if (state.sortBy !== 'date-desc') sortRows(vis); return { vis, catCnts }; } function applyPage() { try { const tb = getTb(); if (!tb) return; const { vis, catCnts } = getVisible(); const hasF = !!state.search || state.dateFrom || state.dateTo || state.noFree; tb.classList.toggle('search-filtered', hasF); refreshNums(vis, catCnts); if (state.showAll) { tb.classList.remove('paginated'); state.totalPages = 1; for (const r of tb.rows) r.classList.remove('page-visible'); } else { tb.classList.add('paginated'); const newTotalPages = Math.max(1, Math.ceil(vis.length / CFG.PAGE_SIZE)); state.totalPages = newTotalPages; // 修复分页同步:当 totalPages 减少时校正页码 if (state.page > state.totalPages) { state.page = state.totalPages; } state.page = Math.max(1, state.page); const start = (state.page-1)*CFG.PAGE_SIZE, end = start+CFG.PAGE_SIZE; const set = new Set(vis.slice(start, end)); for (const r of tb.rows) { if (!r.dataset.category) continue; r.classList.toggle('page-visible', set.has(r)); } } updatePager(); } catch (err) { console.error('[SLC] applyPage error:', err); } } // ==================== 分页 ==================== function createPager() { const prev = h('button',{class:'pager-btn','data-action':'prev',text:t('prev')}); const inp = h('input',{type:'text',class:'page-input'}); const tot = h('span',{style:{color:'#c7d5e0',minWidth:'30px',display:'inline-block',textAlign:'center'}}); const sep = h('span',{style:{color:'#8a9ba8'},text:' / '}); const info = h('span',{class:'page-info'},[inp,sep,tot]); const next = h('button',{class:'pager-btn','data-action':'next',text:t('next')}); const show = h('button',{class:'pager-btn','data-action':'showAll',text:t('showAll')}); inp.addEventListener('blur',()=>handlePageInp(inp)); inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();inp.blur();}}); ui.prevBtn=prev; ui.nextBtn=next; ui.showAllBtn=show; ui.pageTotal=tot; ui.pageInput=inp; return {prev,info,next,show}; } function handlePageInp(inp) { const v = parseInt(inp.value,10); if (!isNaN(v) && v>=1 && v<=state.totalPages && v!==state.page) { state.page=v; applyPage(); } else inp.value=state.page; } function updatePager() { if (!ui.prevBtn) return; const ss = document.querySelector('#slc-sort'); if (state.showAll) { ui.showAllBtn.textContent=t('pagedView'); ui.showAllBtn.classList.add('active-mode'); ui.prevBtn.disabled=ui.nextBtn.disabled=true; ui.pageInput.value=1; ui.pageInput.disabled=true; if(ss){ss.disabled=true;ss.value='date-desc';state.sortBy='date-desc';} } else { ui.showAllBtn.textContent=t('showAll'); ui.showAllBtn.classList.remove('active-mode'); ui.prevBtn.disabled=state.page<=1; ui.nextBtn.disabled=state.page>=state.totalPages; ui.pageInput.value=state.page; ui.pageInput.disabled=false; if(ss)ss.disabled=false; } ui.pageTotal.textContent=state.totalPages; } // ==================== Modal Factory ==================== const ICONS = { donut: ``, year: ``, predict: ``, trend: ``, similar: ``, key: ``, info: ``, calendarHeatmap: ``, export: ``, shield: ``, clock: `` }; ICONS['year-month'] = ICONS.year; function createChartModal(prefix, title, iconBg, noteKey = 'dataNote') { const modal = document.createElement('div'); modal.id = `slc-${prefix}-modal`; modal.className = 'slc-modal'; const contentId = `slc-${prefix}-c`; const nav = MODAL_NAV.find(n => n.prefix === prefix); const navHtml = nav ? buildNavSidebar(prefix) : ''; const modalIcon = nav?.icon || ICONS[prefix] || ICONS.info; modal.innerHTML = `
${navHtml}
${modalIcon}
${escHtml(title)}
${ICONS.info}${escHtml(t(noteKey))}
`; document.body.appendChild(modal); const hide = () => modal.classList.remove('visible'); modal.addEventListener('click', e => { if (e.target === modal) hide(); }); modal.querySelector('.slc-mx').addEventListener('click', hide); // 防止滚轮事件穿透到页面 const mc = modal.querySelector('.slc-mc'); mc.addEventListener('wheel', e => { e.stopPropagation(); }, { passive: true }); // 绑定导航按钮点击事件 modal.querySelectorAll('.slc-nav-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); switchToModal(btn.dataset.navKey); }); }); disposers.push(() => { modal.remove(); }); return { modal, hide, bodyEl: modal.querySelector(`#slc-${prefix}-body`) }; } const _modalKeys = ['donutRefs','yearRefs','yearMonthRefs','predictRefs','trendRefs','similarRefs','calRefs','delistedRefs']; // 窗口导航配置 const MODAL_NAV = [ { key: 'donutRefs', prefix: 'donut', icon: ICONS.donut, titleKey: 'donutTitle', showFn: showDonut, color: '#3b82f6' }, { key: 'yearRefs', prefix: 'year', icon: ICONS.year, titleKey: 'yearTitle', showFn: showYear, color: '#8b5cf6' }, { key: 'predictRefs', prefix: 'predict', icon: ICONS.predict, titleKey: 'predictTitle', showFn: showPredict, color: '#f59e0b' }, { key: 'trendRefs', prefix: 'trend', icon: ICONS.trend, titleKey: 'trendTitle', showFn: showTrend, color: '#3b82f6' }, { key: 'similarRefs', prefix: 'similar', icon: ICONS.similar, titleKey: 'similarTitle', showFn: showSimilar, color: '#f97316' }, { key: 'calRefs', prefix: 'cal', icon: ICONS.calendarHeatmap, titleKey: 'calendarTitle', showFn: showCalendarHeatmap, color: '#10b981' }, { key: 'delistedRefs', prefix: 'delisted', icon: ICONS.shield, titleKey: 'delistedTitle', showFn: showDelisted, color: '#f44336' } ]; // 切换到指定窗口(无动画) function switchToModal(targetKey) { const targetNav = MODAL_NAV.find(n => n.key === targetKey); if (!targetNav) return; const ref = ui[targetNav.key]; hideAllModals(); if (ref) { ref.modal.classList.add('visible'); } else { targetNav.showFn(); // 首次创建:showFn 会立即添加 visible,需要移除后在下一帧重新添加以触发动画 const newRef = ui[targetNav.key]; if (newRef) { newRef.modal.classList.remove('visible'); requestAnimationFrame(() => newRef.modal.classList.add('visible')); } } } // 生成导航栏 HTML function buildNavSidebar(currentPrefix) { const buttons = MODAL_NAV.map(nav => { const isActive = nav.prefix === currentPrefix; const style = isActive ? `background:${nav.color};border-color:transparent;box-shadow:0 2px 8px ${nav.color}40` : ''; return ``; }).join(''); return `
${buttons}
`; } function hideAllModals() { _modalKeys.forEach(k => ui[k]?.modal.classList.remove('visible')); rs.modal?.classList.remove('visible'); } function buildGrid(max, pL, pR, pT, cH) { let grid = ''; for (let i = 0; i <= 5; i++) { const v = Math.round(max / 5 * i), y = pT + cH - (cH * i / 5); grid += `${v}`; } return grid; } function buildStackedSvg({ labels, data, cats, max, svgW, svgH, pL, pR, pT, pB, bW, clickable }) { const cW = svgW - pL - pR, cH = svgH - pT - pB; const gap = cW / labels.length; const grid = buildGrid(max, pL, svgW - pR, pT, cH); let bars = ''; labels.forEach((label, i) => { const item = data[i], cx = pL + gap * i + gap / 2; let sh = 0; const tot = cats.reduce((s, c) => s + (item.get(c.id) || 0), 0); let barStack = ''; for (const c of cats) { const n = item.get(c.id) || 0, ht = max ? (n / max) * cH : 0, y = pT + cH - sh - ht; barStack += `${label} ${escHtml(c.label)}: ${n} (${tot ? (n / tot * 100).toFixed(1) : 0}%)`; sh += ht; } if (clickable) { bars += `${barStack}${label}`; } else { bars += `${barStack}${label}`; } }); const legend = cats.map(c => `${escHtml(c.label)}`).join(''); return `
${grid}${bars}
${legend}
`; } // ==================== Donut Chart ==================== function showDonut() { if (!ui.donutRefs) ui.donutRefs = createChartModal('donut', t('donutTitle'), '#3b82f6'); const { modal, bodyEl } = ui.donutRefs; const cnts=chartCounts(), total=cnts.get('total')||0; const hash=CATS.map(c=>cnts.get(c.id)).join(','); if(hash===modal.dataset.hash){modal.classList.add('visible');return;} modal.dataset.hash=hash; const cats=chartCats(), data=cats.map(c=>({...c,count:cnts.get(c.id)||0})).filter(d=>d.count>0); const sz=240,sw=40,r=(sz-sw)/2,C=2*Math.PI*r; let off=0; const segs=data.map(d=>{ const dash=(d.count/total)*C; const s=``; off+=dash; return s; }).join(''); const maxCount=Math.max(...data.map(d=>d.count),1); const barCols=data.map(d=>{ const pct=(d.count/maxCount*100).toFixed(1); return `
${d.count}
${escHtml(d.label)}
`; }).join(''); bodyEl.innerHTML=`
${segs}${total}${escHtml(t('total'))}
${barCols}
${data.map(d=>`
${escHtml(d.label)}
${d.count} (${(d.count/total*100).toFixed(1)}%)
`).join('')}
`; modal.classList.add('visible'); } // ==================== Year Chart ==================== // 构建年度/月度图表数据 function buildYearChartData(timeUnit, filterYear = null) { const cats = isNoFree() ? CATS.filter(c => c.id !== 'free') : CATS; const timeMap = new Map(); for (const row of dataRows()) { const cat = row.dataset.category; if (!cat || cat === 'other' || (isNoFree() && cat === 'free')) continue; const ts = +row.dataset.dateTimestamp || 0; if (!ts) continue; const d = new Date(ts); if (filterYear && d.getFullYear() !== filterYear) continue; const timeKey = timeUnit === 'year' ? d.getFullYear() : d.getMonth() + 1; if (!timeMap.has(timeKey)) timeMap.set(timeKey, new Map(cats.map(c => [c.id, 0]))); const td = timeMap.get(timeKey); if (td.has(cat)) td.set(cat, td.get(cat) + 1); } return { cats, timeMap }; } function showYear() { if (!ui.yearRefs) ui.yearRefs = createChartModal('year', t('yearTitle'), '#8b5cf6'); const { modal, bodyEl } = ui.yearRefs; const { cats, timeMap } = buildYearChartData('year'); if (!timeMap.size) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; } const times = [...timeMap.keys()].sort((a, b) => a - b); const maxVal = Math.max(...times.map(t => [...timeMap.get(t).values()].reduce((s, v) => s + v, 0))); const svgW = 700, svgH = 380, pL = 50, pR = 20, pT = 20, pB = 40; const bW = Math.min(40, ((svgW - pL - pR) / times.length) * .6); bodyEl.innerHTML = buildStackedSvg({ labels: times, data: times.map(t => timeMap.get(t)), cats, max: maxVal, svgW, svgH, pL, pR, pT, pB, bW, clickable: true }); bodyEl.querySelectorAll('.year-bar-group').forEach(g => { g.addEventListener('click', () => showYearMonthDetail(+g.dataset.year, cats)); }); modal.classList.add('visible'); } function showYearMonthDetail(year, cats = null) { if (!ui.yearMonthRefs) ui.yearMonthRefs = createChartModal('year-month', '', '#8b5cf6'); const { modal, bodyEl } = ui.yearMonthRefs; // 每次打开都更新标题(title 只在 createChartModal 首次调用时写入) const titleEl = modal.querySelector('.slc-mh-t'); if (titleEl) titleEl.textContent = `${year} ${t('monthlyDistribution')}`; const { cats: detailCats, timeMap } = buildYearChartData('month', year); const finalCats = cats || detailCats; // 确保所有月份都有数据 const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const monthData = months.map(m => { if (!timeMap.has(m)) { return new Map(finalCats.map(c => [c.id, 0])); } return timeMap.get(m); }); const maxM = Math.max(...monthData.map(m => [...m.values()].reduce((s, v) => s + v, 0))); const svgW = 700, svgH = 380, pL = 50, pR = 20, pT = 20, pB = 50; const bW = Math.min(30, ((svgW - pL - pR) / 12) * .6); bodyEl.innerHTML = buildStackedSvg({ labels: months.map(m => m + '月'), data: monthData, cats: finalCats, max: maxM, svgW, svgH, pL, pR, pT, pB, bW, clickable: false }); modal.classList.add('visible'); } // ==================== 预测 ==================== function buildPredictSum({ rows, udates, apd, avg, lastD, predD }) { return `
${ICONS.similar}
${escHtml(t('predictTotalItems'))}${rows}
${ICONS.calendarHeatmap}
${escHtml(t('predictTotalGames'))}${udates}
${ICONS.predict}
${escHtml(t('predictAvgPerDay'))}${apd}
${ICONS.clock}
${escHtml(t('predictOverallAvg'))}${avg.toFixed(1)} ${escHtml(t('predictDays'))}
${ICONS.donut}
${escHtml(t('predictLastDate'))}${fmtD(lastD)}
${ICONS.trend}
${escHtml(t('predictNextDate'))}${fmtD(predD)}
`; } function buildPredictTable(ym, ycMap, ylMap) { const yrs = [...ym.keys()].sort((a, b) => b - a); const tRows = yrs.map(y => { const info = ym.get(y), a = info.ints.length > 0 ? info.ints.reduce((s, v) => s + v, 0) / info.ints.length : 0; const cnt = ycMap.get(y) || 0, lic = ylMap.get(y) || 0, daily = cnt > 0 ? (lic / cnt).toFixed(2) : '0.00', pct = (cnt / 365 * 100).toFixed(1); return `${y}${lic}${cnt}${daily}${a > 0 ? a.toFixed(1) + ' ' + escHtml(t('predictDays')) : '-'}
`; }).join(''); return `
${tRows}
${escHtml(t('yearTitle'))}${escHtml(t('predictYearLicenses'))}${escHtml(t('predictYearCount'))}${escHtml(t('predictDailyLicenses'))}${escHtml(t('predictYearAvg'))}${escHtml(t('predictYearRatio'))}
`; } function showPredict() { if (!ui.predictRefs) ui.predictRefs = createChartModal('predict', t('predictTitle'), '#f59e0b', 'predictNote'); const { modal, bodyEl } = ui.predictRefs; const rows = dataRows().filter(r => r.dataset.category && r.dataset.category !== 'free' && +r.dataset.dateTimestamp > 0); if (rows.length < 2) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; } const dateSet = new Set(), udates = []; for (const r of rows) { const ts = +r.dataset.dateTimestamp; if (ts > 0 && !dateSet.has(ts)) { dateSet.add(ts); udates.push(ts); } } udates.sort((a, b) => a - b); if (udates.length < 2) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; } const intervals = []; for (let i = 1; i < udates.length; i++) intervals.push(Math.round((udates[i] - udates[i - 1]) / CFG.DAY_MS)); const avg = intervals.reduce((s, v) => s + v, 0) / intervals.length; const ym = new Map(); for (let i = 1; i < udates.length; i++) { const y = new Date(udates[i]).getFullYear(); if (!ym.has(y)) ym.set(y, { ints: [], cnt: 0 }); ym.get(y).ints.push(Math.round((udates[i] - udates[i - 1]) / CFG.DAY_MS)); ym.get(y).cnt++; } const ycMap = new Map(), ylMap = new Map(); for (const ts of udates) { const y = new Date(ts).getFullYear(); ycMap.set(y, (ycMap.get(y) || 0) + 1); } for (const r of rows) { const ts = +r.dataset.dateTimestamp; if (ts > 0) { const y = new Date(ts).getFullYear(); ylMap.set(y, (ylMap.get(y) || 0) + 1); } } const lastD = new Date(udates[udates.length - 1]); const predD = new Date(lastD.getTime() + avg * CFG.DAY_MS); const apd = udates.length > 0 ? (rows.length / udates.length).toFixed(2) : '0'; bodyEl.innerHTML = `
${buildPredictSum({ rows: rows.length, udates: udates.length, apd, avg, lastD, predD })}${buildPredictTable(ym, ycMap, ylMap)}
`; modal.classList.add('visible'); } function showTrend() { if (!ui.trendRefs) ui.trendRefs = createChartModal('trend', t('trendTitle'), '#3b82f6', 'dataNote'); const { modal, bodyEl } = ui.trendRefs; // 分类颜色(与主分类统一) const catColors = { all: '#64748b', ...Object.fromEntries(CATS.map(c => [c.id, c.color])) }; const catOrder = CATS.map(c => c.id); // 初始化可见性(默认不显示总和,只显示4个分类) if (!ui.trendVisible) ui.trendVisible = new Set(catOrder); if (ui.trendCumulative === undefined) ui.trendCumulative = true; const allRows = dataRows().filter(r => r.dataset.category && r.dataset.category !== 'other' && +r.dataset.dateTimestamp > 0); // 创建图表容器 const chartContainerId = 'slc-trend-chart-container'; bodyEl.innerHTML = `
${noDataHtml()}
`; const chartContainer = bodyEl.querySelector(`#${chartContainerId}`); // 清理上一次的事件处理器(修复 #2) if (bodyEl._trendCleanup) { bodyEl._trendCleanup(); bodyEl._trendCleanup = null; } let trendFilterHandler = null; let trendCumHandler = null; function renderTrend() { const visibleCats = catOrder.filter(c => ui.trendVisible.has(c)); const showAll = ui.trendVisible.has('all'); if (!visibleCats.length && !showAll) { chartContainer.innerHTML = noDataHtml(); return; } const rows = allRows.filter(r => ui.trendVisible.has(r.dataset.category) || showAll); if (!rows.length) { chartContainer.innerHTML = noDataHtml(); return; } // 按月统计每个分类和总和 const emptyMonth = () => ({ all: 0, ...Object.fromEntries(catOrder.map(c => [c, 0])) }); const monthly = new Map(); for (const row of rows) { const ts = +row.dataset.dateTimestamp; const d = new Date(ts); const key = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; if (!monthly.has(key)) monthly.set(key, emptyMonth()); const cat = row.dataset.category; if (monthly.get(key)[cat] !== undefined) { monthly.get(key)[cat]++; monthly.get(key).all++; } } const sorted = [...monthly.entries()].sort((a, b) => a[0].localeCompare(b[0])); // 累加模式下计算累计值 let data = sorted.map(x => ({ ...x[1] })); if (ui.trendCumulative) { const cum = emptyMonth(); data = data.map(d => { for (const c of ['all', ...catOrder]) cum[c] += d[c]; return { ...cum }; }); } const max = Math.max(...data.map(x => { let m = 0; if (showAll) m = Math.max(m, x.all); catOrder.forEach(c => { if (ui.trendVisible.has(c)) m = Math.max(m, x[c]); }); return m; })) || 1; const svgW = 700, svgH = 320, pL = 50, pR = 20, pT = 20, pB = 60; const cW = svgW - pL - pR, cH = svgH - pT - pB; const step = cW / (sorted.length - 1 || 1); // 生成每条可见曲线 const lineKeys = [...(showAll ? ['all'] : []), ...visibleCats]; const lines = lineKeys.map(cat => { const points = data.map((item, i) => { const x = pL + i * step; const y = pT + cH - (item[cat] / max * cH); return `${x},${y}`; }).join(' '); return ``; }).join(''); // 网格 const grid = buildGrid(max, pL, svgW - pR, pT, cH); // 年份标签 let labels = ''; let lastYear = ''; sorted.forEach((item, i) => { const x = pL + i * step; const year = item[0].split('-')[0]; if (year !== lastYear) { labels += `${year}`; lastYear = year; } }); const svg = `${grid}${lines}${labels}`; chartContainer.innerHTML = `
${svg}
`; } // 筛选按钮 const filterBtns = document.createElement('div'); filterBtns.setAttribute('data-trend-filter-btns', 'true'); filterBtns.style.cssText = 'display:flex;gap:12px;justify-content:center;margin-bottom:16px;padding:12px;background:rgba(0,0,0,.2);border-radius:8px'; // 累加模式切换开关(内联到筛选按钮行) const cumToggle = document.createElement('div'); cumToggle.style.cssText = 'display:flex;align-items:center;gap:8px;margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.15)'; cumToggle.innerHTML = `${t('trendCumulative')}`; const cumInput = cumToggle.querySelector('#slc-trend-cum'); const cumTrack = cumToggle.querySelector('.tg-track'); const cumThumb = cumTrack.querySelector('span'); cumInput.checked = ui.trendCumulative; function updateCumStyle() { const on = ui.trendCumulative; cumTrack.style.background = on ? 'rgba(59,130,246,.3)' : '#2a475e'; cumTrack.style.borderColor = on ? '#3b82f6' : '#4a6a7a'; cumThumb.style.left = on ? '16px' : '2px'; cumThumb.style.background = on ? '#3b82f6' : '#8a9ba8'; } updateCumStyle(); trendCumHandler = () => { ui.trendCumulative = cumInput.checked; updateCumStyle(); renderTrend(); }; cumInput.addEventListener('change', trendCumHandler); // 注册清理函数,下次打开时移除旧监听器(修复 #2) bodyEl._trendCleanup = () => { cumInput.removeEventListener('change', trendCumHandler); filterBtns.removeEventListener('click', trendFilterHandler); }; function updateFilterBtns() { const allVisible = ui.trendVisible.has('all'); filterBtns.innerHTML = `` + catOrder.map(cat => { const visible = ui.trendVisible.has(cat); return ``; }).join(''); filterBtns.appendChild(cumToggle); } updateFilterBtns(); bodyEl.insertBefore(filterBtns, chartContainer); renderTrend(); // 绑定筛选事件(使用引用以便清理) trendFilterHandler = e => { const btn = e.target.closest('button'); if (!btn) return; const cat = btn.dataset.cat; if (ui.trendVisible.has(cat)) { ui.trendVisible.delete(cat); } else { ui.trendVisible.add(cat); } renderTrend(); updateFilterBtns(); }; filterBtns.addEventListener('click', trendFilterHandler); modal.classList.add('visible'); } // ==================== Similar License Guess ==================== function showSimilar() { if (!ui.similarRefs) ui.similarRefs = createChartModal('similar', t('similarTitle'), '#f97316', 'dataNote'); const { modal, bodyEl } = ui.similarRefs; // 获取所有非免费游戏名称 const catMap = new Map(CATS.map(c => [c.id, c])); const rows = dataRows().filter(r => r.dataset.gameName && r.dataset.category && r.dataset.category !== 'other' && !(isNoFree() && r.dataset.category === 'free')); const names = rows.map(r => { const cat = catMap.get(r.dataset.category); return { name: r.dataset.gameName.trim(), cat: r.dataset.category, catLabel: cat?.label || r.dataset.category, catColor: cat?.color || '#64748b' }; }); if (names.length < 2) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; } // 前缀单词数选择 if (ui.similarPrefixWords === undefined) ui.similarPrefixWords = 5; let prefixWords = ui.similarPrefixWords; const prefixBtns = Array.from({length:10},(_,i)=>i+1).map(n=>``).join(''); const prefixBar = `
${t('similarPrefixLen')}:${prefixBtns}
`; function getWordPrefix(name, count) { // 按空格/特殊字符分词,取前 count 个单词 const words = name.split(/[\s\-:_,]+/).filter(w => w.length > 0); return { key: words.slice(0, count).join(' ').toLowerCase(), display: words.slice(0, count).join(' ') }; } function groupByPrefix(wordCount) { const groups = new Map(); for (const item of names) { const { key, display } = getWordPrefix(item.name, wordCount); const words = item.name.split(/[\s\-:_,]+/).filter(w => w.length > 0); if (words.length < wordCount) continue; if (!groups.has(key)) groups.set(key, { display, items: [] }); groups.get(key).items.push(item); } return [...groups.entries()].filter(g => g[1].items.length >= 2).sort((a, b) => b[1].items.length - a[1].items.length); } function renderSimilar(count) { const groups = groupByPrefix(count); if (!groups.length) { return noDataHtml(); } const list = groups.map((g, i) => { const [key, { display, items }] = g; const itemList = items.map(it => `
${escHtml(it.name)}${escHtml(it.catLabel)}
`).join(''); return `
${escHtml(display)}${t('similarCount')}: ${items.length}
${itemList}
`; }).join(''); return `
${list}
`; } bodyEl.innerHTML = prefixBar + `
${renderSimilar(prefixWords)}
`; // 折叠/展开事件:bodyEl 在多次打开间复用,只绑定一次 if (!bodyEl._similarFoldBound) { bodyEl._similarFoldBound = true; bodyEl.addEventListener('click', e => { const hd = e.target.closest('.slc-similar-hd'); if (!hd) return; const card = hd.closest('.slc-similar-group'); const bd = card.querySelector('.slc-similar-bd'); const arrow = hd.querySelector('.slc-similar-arrow'); const expanded = bd.style.maxHeight !== '0px'; if (expanded) { bd.style.maxHeight = '0px'; bd.style.padding = '0 16px'; arrow.style.transform = 'rotate(-90deg)'; } else { bd.style.maxHeight = bd.scrollHeight + 32 + 'px'; bd.style.padding = '12px 16px'; arrow.style.transform = 'rotate(0deg)'; } }); } // 前缀单词数选择事件:old firstElementChild 随 innerHTML 替换被销毁,无需手动解绑 bodyEl.firstElementChild.addEventListener('click', e => { const btn = e.target.closest('.slc-similar-pbtn'); if (!btn) return; prefixWords = ui.similarPrefixWords = +btn.dataset.n; bodyEl.querySelectorAll('.slc-similar-pbtn').forEach(b => { const active = +b.dataset.n === prefixWords; b.style.background = active ? 'rgba(249,115,22,.2)' : 'transparent'; b.style.borderColor = active ? '#f97316' : 'rgba(255,255,255,.2)'; b.style.color = active ? '#f97316' : '#8a9ba8'; }); bodyEl.querySelector('#slc-similar-result').innerHTML = renderSimilar(prefixWords); }); modal.classList.add('visible'); } // ==================== CDKey Redeem ==================== const FAIL_MAP = {14:'invalidKey',15:'duplicateKey',53:'rateLimit',13:'regionRestrict',9:'alreadyOwned',24:'missingDep',50:'walletCode'}; const SVG_OK=''; const SVG_FAIL=''; const rs = { keyN:0, recvN:0, failSet:new Set(), modal:null, batchT:null, rowMap:new Map(), refs:{}, activeRequests: 0 }; const extractKeys = text => [...new Set((text||'').trim().toUpperCase().match(/([0-9A-Z]{5}-){2,4}[0-9A-Z]{5}/g)||[])]; function showRedeem(){if(!rs.modal)createRedeem();rs.modal.classList.add('visible');} function redeemDone(){ const{refs:{act:$$a,inp:$$i,stTotal:$$t,stSuccess:$$s,stFail:$$f}}=rs; const fc=rs.failSet.size,sc=rs.recvN-fc; if($$t)$$t.textContent=rs.recvN; if($$s)$$s.textContent=sc; if($$f)$$f.textContent=fc; if(rs.recvN>=rs.keyN){ if($$a)$$a.disabled=false;if($$i)$$i.disabled=false; toast(t('redeemDone').replace('{total}',rs.keyN).replace('{success}',sc).replace('{fail}',fc),fc>0?'error':'success',5000); } } const REDEEM_SVG = { key: '', total: '', ok: '', fail: '' }; function createRedeem(){ const modal=document.createElement('div'); modal.id='slc-redeem-modal';modal.className='slc-modal'; modal.innerHTML=`
${REDEEM_SVG.key}
${t('redeemTitle')}
${ICONS.info}${t('dataNote')}
`; document.body.appendChild(modal); const $=s=>modal.querySelector(s); const $inp=$('#slc-rinp'),$tbody=$('#slc-rtb'),$table=$('#slc-rt'),$act=$('#slc-ra'); const $stTotal=$('#slc-rs-total'),$stSuccess=$('#slc-rs-success'),$stFail=$('#slc-rs-fail'); rs.refs={inp:$inp,tbody:$tbody,table:$table,act:$act,stTotal:$stTotal,stSuccess:$stSuccess,stFail:$stFail}; const hideModal=()=>rs.modal.classList.remove('visible'); modal.addEventListener('click',e=>{if(e.target===modal)hideModal();}); $('.slc-mx').addEventListener('click',hideModal); $('#slc-rpaste').addEventListener('click',e=>{stopEv(e,()=>navigator.clipboard.readText().then(text=>{const keys=extractKeys(text);$inp.value=keys.reduce((s,k,i)=>s+(i%4===0&&i?'\n':' ')+k,'').trim();toast(keys.length?t('pasteKeyCount').replace('{count}',keys.length):t('pasteNoKey'),keys.length?'success':'error');}).catch(()=>{}));}); $('#slc-rclear').addEventListener('click',e=>{stopEv(e,()=>{$inp.value='';toast(t('inputCleared'),'info');});}); $('#slc-rch').addEventListener('click',e=>{stopEv(e,()=>{$tbody.innerHTML='';$table.style.display='none';$act.disabled=false;$inp.disabled=false;rs.failSet.clear();rs.rowMap.clear();$('#slc-redeem-c').style.removeProperty('max-height');const stats=$('#slc-rstats');if(stats)stats.style.display='none';['slc-rch','slc-rcf','slc-rcs'].forEach(id=>{const b=$('#'+id);if(b)b.style.display='none';});toast(t('clearBtn'),'info');});}); $('#slc-rcf').addEventListener('click',e=>{stopEv(e,()=>{if(rs.failSet.size){GM_setClipboard([...rs.failSet].join(','));toast(`${t('copiedKeys')} (${rs.failSet.size})`,'success');}else{toast(t('noFailedKeys'),'error');}});}); $('#slc-rcs').addEventListener('click',e=>{stopEv(e,()=>{const lines=[...modal.querySelectorAll('#slc-rtb .rs')].map(row=>{const subId=row.querySelector('.rsub code')?.textContent.trim()||'';const gn=row.querySelector('.rgame')?.textContent.trim()||'';return(subId&&subId!=='——')?`${subId}\t${gn}`:'';}).filter(Boolean);if(lines.length){GM_setClipboard(lines.join('\n'));toast(`${t('copiedKeys')} (${lines.length})`,'success');}else{toast(t('noSubData'),'error');}});}); $('#slc-ra').addEventListener('click',e=>{stopEv(e,()=>{const keys=extractKeys($inp.value);if(keys.length)doRedeem(keys);else toast(t('noKeyInput'),'error');});}); rs.modal=modal; disposers.push(()=>{clearInterval(rs.batchT);modal?.remove();rs.modal=null;}); } function doRedeem(keys){ const{inp,tbody,table,act}=rs.refs; const $=s=>rs.modal.querySelector(s); table.style.display='';act.disabled=true;inp.disabled=true; $('#slc-redeem-c').style.maxHeight='650px'; ['slc-rch','slc-rcf','slc-rcs'].forEach(id=>{const b=$('#'+id);if(b)b.style.display='';}); const stats=$('#slc-rstats');if(stats)stats.style.display=''; rs.keyN=keys.length;rs.recvN=0;rs.failSet.clear();rs.rowMap.clear();rs.activeRequests=0; keys.forEach((key,i)=>{ const row=document.createElement('tr');row.dataset.key=key; row.innerHTML=`${i+1}${escHtml(key)}${escHtml(t('redeeming'))}...`; tbody.prepend(row);rs.rowMap.set(key,row); if(iCFG.REDEEM_MAX)startBatch(); } function redeemOne(key){ const fail=(detail,rk='networkError')=>updateRow(key,'fail',detail,rk,0,''); const sessionID = getSessionID(); if (!sessionID) { fail('No session ID', 'othersError'); return; } rs.activeRequests++; GM_xmlhttpRequest({ url:'https://store.steampowered.com/account/ajaxregisterkey/', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',Origin:'https://store.steampowered.com',Referer:'https://store.steampowered.com/account/registerkey'}, data:`product_key=${encodeURIComponent(key)}&sessionid=${encodeURIComponent(sessionID)}`,method:'POST',responseType:'json',timeout:15000, onload(resp){ rs.activeRequests--; if(resp.status!==200||!resp.response){fail(t('networkError'));return;} const d=resp.response,it=d.purchase_receipt_info?.line_items?.[0]; if(d.success===1)updateRow(key,'success',t('successDetail'),'',Number(it?.packageid)||0,it?.line_item_description||''); else{const rk=FAIL_MAP[d.purchase_result_details]||'othersError';updateRow(key,'fail',t(rk),rk,Number(it?.packageid)||0,it?.line_item_description||'');} },ontimeout:()=>{rs.activeRequests--;fail(t('networkError'));}, onerror:()=>{rs.activeRequests--;fail(t('networkError'));} }); } function updateRow(key,status,detail,rk,subId,subName){ const ok=status==='success',isFail=status==='fail'; if(!ok)rs.failSet.add(key); rs.recvN++; const row=rs.rowMap.get(key);if(!row)return; row.className=ok?'rs':(isFail?'rf':''); const cells=row.querySelectorAll('td'); cells[2].innerHTML=ok?SVG_OK+t('success'):(isFail?SVG_FAIL+t('fail'):detail); cells[2].removeAttribute('data-status');cells[2].removeAttribute('style'); cells[3].textContent=detail; cells[4].innerHTML=subId?`${escHtml(String(subId))}`:'\u2014\u2014'; cells[5].textContent=subName||'\u2014\u2014'; redeemDone(); } function startBatch(){ clearInterval(rs.batchT); rs.batchT=setInterval(()=>{ const waiting=rs.modal.querySelectorAll('#slc-rtb tr [data-waiting="true"]'); if(!waiting.length){ // 检查是否还有活跃请求 if (rs.activeRequests === 0) { clearInterval(rs.batchT); rs.batchT = null; } return; } for(const cell of waiting){ if(rs.activeRequests>=CFG.REDEEM_MAX)return; cell.textContent=`${t('redeeming')}...`; cell.removeAttribute('data-waiting'); redeemOne(cell.closest('tr').dataset.key); } },CFG.REDEEM_WAIT*1000); } // ==================== 日历热力图 ==================== const CAL_MONTHS = { zh:['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'], en:['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] }; const CAL_WEEKS = { zh:['日','一','二','三','四','五','六'], en:['Su','Mo','Tu','We','Th','Fr','Sa'] }; function showCalendarHeatmap(){ const rows=dataRows().filter(r=>{ const cat=r.dataset.category; if(!cat||cat==='other')return false; if(state.noFree&&cat==='free')return false; return +r.dataset.dateTimestamp>0; }); if(!rows.length){toast(t('calendarNoData'),'error');return;} const dateMap=new Map(); for(const r of rows){ const ts=+r.dataset.dateTimestamp; const d=new Date(ts); const key=fmtD(d); dateMap.set(key,(dateMap.get(key)||0)+1); } const dates=[...dateMap.entries()].sort((a,b)=>a[0].localeCompare(b[0])); const years=[...new Set(dates.map(([d])=>d.slice(0,4)))].sort((a,b)=>b.localeCompare(a)); const maxCount=Math.max(...dateMap.values()); const getColor=v=>{ if(v===0)return'#2a475e'; const p=v/maxCount; if(p<0.25)return'#0e4429'; if(p<0.5)return'#006d32'; if(p<0.75)return'#26a641'; return'#39d353'; }; const isZh = locale.startsWith('zh'); const monthLabels = isZh ? CAL_MONTHS.zh : CAL_MONTHS.en; const weekDayLabels = isZh ? CAL_WEEKS.zh : CAL_WEEKS.en; const buildYear=y=>{ const startDate=new Date(`${y}-01-01`); const endDate=new Date(`${y}-12-31`); const startDay=startDate.getDay(); const totalDays=Math.round((endDate-startDate)/CFG.DAY_MS)+1; let html=`
${y}
`; html+=`
${monthLabels.map(m=>`${m}`).join('')}
`; html+=`
`; html+=`
${weekDayLabels.map(d=>`${d}`).join('')}
`; html+=`
`; for(let i=0;i
`; for(let i=0;i
`; } html+=`
`; return html; }; if (!ui.calRefs) ui.calRefs = createChartModal('cal', t('calendarTitle'), '#10b981'); const { modal, bodyEl } = ui.calRefs; // hash 缓存:数据未变时跳过重建 const hash = rows.length + '_' + (rows[0]?.dataset.dateTimestamp || ''); if (hash === modal.dataset.hash) { modal.classList.add('visible'); return; } modal.dataset.hash = hash; bodyEl.innerHTML = `
${years.map(buildYear).join('')}
`; // 图例放在 bodyEl 外面,不跟随滚动 const legendCls = 'slc-cal-legend'; let legend = bodyEl.nextElementSibling; if (!legend || !legend.classList.contains(legendCls)) { bodyEl.insertAdjacentHTML('afterend', `
${t('calendarLess')}
${t('calendarMore')}
`); } modal.classList.add('visible'); } // ==================== 导出 ==================== const _RE_REMOVE = /\s*(移除|Remove|删除|Delete|卸载|Uninstall)\s*/gi; const _CAT_LABEL_MAP = new Map(CATS.map(c => [c.id, c.label])); function getExportData(){ const re=_RE_REMOVE; return dataRows().filter(r=>r.dataset.category&&r.dataset.category!=='other'&&!(state.noFree&&r.dataset.category==='free')).map(r=>({ date:r.querySelector('td.license_date_col')?.textContent.trim()||'', name:(r.dataset.gameName||'').replace(re,'').trim(), category:_CAT_LABEL_MAP.get(r.dataset.category)||r.dataset.category, details:r.querySelector('td.license_acquisition_col')?.textContent.trim()||'' })); } function dlBlob(blob,name){ const url=URL.createObjectURL(blob),a=h('a',{href:url,download:name,style:{display:'none'}}); document.body.appendChild(a);a.click(); setTimeout(()=>{a.remove();URL.revokeObjectURL(url);},CFG.BLOB_CLEANUP); } function exportData(fmt){ try{ const data=getExportData(); if(!data.length){toast(t('exportNoData'),'error');return;} const now=new Date(),ts=fmtD(now)+'_'+[now.getHours(),now.getMinutes(),now.getSeconds()].map(v=>String(v).padStart(2,'0')).join(''); if(fmt==='csv'){ const hdr=[t('csvDate'),t('csvName'),t('csvCategory'),t('csvDetails')]; const esc=s=>`"${String(s).replace(/"/g,'""')}"`; const csv=[hdr.join(','),...data.map(r=>[r.date,r.name,r.category,r.details].map(esc).join(','))].join('\n'); dlBlob(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}),`steam-licenses-${ts}.csv`); }else{ dlBlob(new Blob([JSON.stringify(data,null,2)],{type:'application/json;charset=utf-8;'}),`steam-licenses-${ts}.json`); } toast(t('exportSuccess'),'success'); }catch(err){console.error(`Export ${fmt} failed:`,err);toast(t('exportFailed')+': '+err.message,'error');} } // ==================== Main UI ==================== function addFilterBtns(){ const table=document.querySelector(SEL.tbl); if(!table||document.querySelector(SEL.fc))return; const ctn=h('div',{id:'steam-license-filter'}); // ===== 统计卡片区 ===== const statsRow=h('div',{class:'stats-row'}); const allCats=[{id:'all',color:ALL_TYPE.color,label:ALL_TYPE.label,icon:ALL_TYPE.icon},...CATS.map(c=>({id:c.id,color:c.color,label:c.label,icon:c.icon}))]; const statCards=new Map(); const statBars=new Map(); for(const c of allCats){ const card=h('div',{class:'stat-card'+(c.id==='all'?' active':''),'data-filter':c.id,style:{'--card-color':c.color}}); const num=h('div',{class:'stat-number',text:'0'}); const lbl=h('div',{class:'stat-label',html:c.icon+' '+c.label}); const bar=h('div',{class:'stat-bar'}); const barFill=h('div',{class:'stat-bar-fill',style:{width:'0%',background:c.color}}); bar.appendChild(barFill); card.append(lbl,num,bar); statsRow.appendChild(card); statCards.set(c.id,num); statBars.set(c.id,barFill); if(c.id==='all') ui.statTotal=num; else ui.statCounts.set(c.id,num); // 免费卡片右侧:排除免费开关 + 功能按钮 if(c.id==='free'){ // 排除免费开关(独立容器) const nfWrap=h('div',{class:'stat-tools',style:{gap:'6px',padding:'0 12px'}}); const nfLabel=h('label',{id:'slc-no-free',html:`${t('withoutFree')}`}); nfWrap.appendChild(nfLabel); statsRow.appendChild(nfWrap); // 功能按钮 - 两排布局 const tools=h('div',{class:'stat-tools',style:{flexDirection:'column',alignItems:'flex-start',gap:'4px'}}); const toolRow1=h('div',{style:{display:'flex',alignItems:'center',gap:'4px'}}); const toolRow2=h('div',{style:{display:'flex',alignItems:'center',gap:'4px'}}); const toolBtns=[ {id:'slc-donut-btn',title:`${t('donutTitle')} (D)`,icon:ICONS.donut}, {id:'slc-year-btn',title:`${t('yearTitle')} (Y)`,icon:ICONS.year}, {id:'slc-pred-btn',title:`${t('predictTitle')} (F)`,icon:ICONS.predict}, {id:'slc-trend-btn',title:`${t('trendTitle')} (T)`,icon:ICONS.trend}, {id:'slc-cal-btn',title:`${t('calendarTitle')} (C)`,icon:ICONS.calendarHeatmap}, ]; const svg16 = s => s.replace('')}); cacheClrBtn.addEventListener('click',async()=>{ if(!await showConfirm(t('clearCacheConfirm')))return; try{ localStorage.removeItem('slc_license_cache'); localStorage.removeItem('slc_license_cache_ts'); localStorage.removeItem('slc_license_total'); localStorage.removeItem('slc_license_cache_ver'); await clearRowDB(); }catch(e){console.warn('[SLC] clear cache failed:',e);} location.reload(); }); toolRow2.appendChild(cacheClrBtn); tools.append(toolRow1, toolRow2); statsRow.appendChild(tools); } } ui.statBars=statBars; ui.statCards=statCards; ctn.appendChild(statsRow); // 绑定排除免费和功能按钮事件 const nfCb=ctn.querySelector('#slc-nf'); if(nfCb)nfCb.addEventListener('change',e=>{ state.noFree=e.target.checked;state.page=1;e.target.blur(); // 切换免费卡片禁用状态 const freeCard=statCards.get('free')?.closest('.stat-card'); if(freeCard){ freeCard.classList.toggle('disabled',state.noFree); if(state.noFree&&state.filter==='free'){state.filter='all';applyFilter('all');} } applyPage(); }); const expDdEl=ctn.querySelector('#slc-edd'); const toolActs={'slc-donut-btn':showDonut,'slc-year-btn':showYear,'slc-pred-btn':showPredict,'slc-trend-btn':showTrend,'slc-similar-btn':showSimilar,'slc-cal-btn':showCalendarHeatmap,'slc-redeem-btn':showRedeem,'slc-delisted-btn':showDelisted,'slc-ecsv':()=>{expDdEl.classList.remove('open');exportData('csv');},'slc-ejson':()=>{expDdEl.classList.remove('open');exportData('json');}}; for(const[id,fn]of Object.entries(toolActs)){const el=ctn.querySelector(`#${id}`);if(el)el.addEventListener('click',e=>stopEv(e,fn));} const ebtnEl=ctn.querySelector('#slc-ebtn'); if(ebtnEl)ebtnEl.addEventListener('click',e=>stopEv(e,()=>expDdEl.classList.toggle('open'))); const closeExpDd=()=>expDdEl?.classList.remove('open'); document.addEventListener('click',closeExpDd); disposers.push(()=>document.removeEventListener('click',closeExpDd)); // ===== 工具栏 ===== const toolbar=h('div',{class:'toolbar'}); // 搜索框 ui.searchInput=h('input',{type:'text',class:'search-box',placeholder:t('searchPlaceholder')}); ui.searchInput.addEventListener('input',e=>{state.search=e.target.value.trim().toLowerCase();clearTimeout(state.timers.search);state.timers.search=setTimeout(()=>{state.page=1;applyPage();},dataRows().length>1000?300:CFG.DEBOUNCE);}); ui.searchInput.addEventListener('keydown',e=>{if(e.key==='Escape'){ui.searchInput.value='';state.search='';applyPage();}}); ui.searchInput.addEventListener('click',e=>e.stopPropagation()); toolbar.appendChild(ui.searchInput); // 日期与排序控件 const advGroup=h('div',{class:'adv-group'}); const dfInput=h('input',{type:'date',class:'adv-date',id:'slc-df',value:state.dateFrom,title:t('dateFrom')}); const dtInput=h('input',{type:'date',class:'adv-date',id:'slc-dt',value:state.dateTo,title:t('dateTo')}); const sortSelect=h('select',{class:'adv-sort',id:'slc-sort',title:t('sortBy'),html:``}); const clrBtn=h('button',{class:'adv-clr',id:'slc-clr',title:t('clearFilter'),html:''}); advGroup.append(h('span',{class:'adv-lbl',text:t('dateFrom')}),dfInput,h('span',{class:'adv-lbl',text:t('dateTo')}),dtInput,sortSelect,clrBtn); toolbar.appendChild(advGroup); const onDC=k=>e=>{state[k]=e.target.value;state.page=1;applyPage();}; dfInput.addEventListener('change',onDC('dateFrom')); dtInput.addEventListener('change',onDC('dateTo')); sortSelect.addEventListener('change',e=>{state.sortBy=e.target.value;applyPage();}); clrBtn.addEventListener('click',()=>{ Object.assign(state,{dateFrom:'',dateTo:'',sortBy:'date-desc',search:'',page:1,noFree:false}); dfInput.value='';dtInput.value='';sortSelect.value='date-desc'; const nfCb=document.querySelector('#slc-nf');if(nfCb)nfCb.checked=false; const freeCard=statCards.get('free')?.closest('.stat-card');if(freeCard)freeCard.classList.remove('disabled'); if(ui.searchInput)ui.searchInput.value='';applyPage();toast(t('filterCleared'),'info'); }); // 分页控件 const pg=h('div',{class:'pager'}); const{prev,info,next,show}=createPager(); pg.append(prev,info,next,show); toolbar.appendChild(pg); ctn.appendChild(toolbar); // 事件委托 ctn.addEventListener('click',e=>{ // 统计卡片点击筛选 const sc=e.target.closest('.stat-card[data-filter]'); if(sc){ if(sc.classList.contains('disabled')){toast(t('freeDisabled'),'warning');return;} const fid=sc.dataset.filter; ui.btns.forEach(b=>b.classList.remove('active')); const fb=ui.btns.get(fid); if(fb)fb.classList.add('active'); statCards.forEach((_,id)=>{const card=statCards.get(id)?.closest('.stat-card');if(card)card.classList.toggle('active',id===fid);}); applyFilter(fid);return; } const ab=e.target.closest('button[data-action]'); if(!ab||ab.disabled)return; const act=ab.dataset.action; if(act==='prev'){state.page=Math.max(1,state.page-1);applyPage();} else if(act==='next'){state.page=Math.min(state.totalPages,state.page+1);applyPage();} else if(act==='showAll'){state.showAll=!state.showAll;if(state.showAll)state.page=1;applyPage();} }); table.parentNode.insertBefore(ctn,table); // 隐藏原h2中的搜索框(如有) const hdr=document.querySelector(SEL.hdr); if(hdr){ const oldSearch=hdr.querySelector('.search-box'); if(oldSearch)oldSearch.style.display='none'; } // 监听尺寸变化以调整吸顶偏移 const updH=()=>document.documentElement.style.setProperty('--filter-h',ctn.offsetHeight+'px'); updH();ui.resizeObs=new ResizeObserver(updH);ui.resizeObs.observe(ctn); disposers.push(()=>{ctn.remove();document.documentElement.style.removeProperty('--filter-h');if(ui.searchInput?.parentNode)ui.searchInput.remove();ui.resizeObs?.disconnect();ui.resizeObs=null;}); invalidateDataRows(); requestAnimationFrame(() => { state.isProcessing = true; try { markRows(); recalc(); lockWidths(); applyPage(); } finally { state.isProcessing = false; } startObs(); }); } // ==================== 本地缓存存储 ==================== const CACHE_KEY = 'slc_license_cache'; // 存储指纹数组 JSON const CACHE_TS_KEY = 'slc_license_cache_ts'; // 存储时间戳 const CACHE_TOTAL_KEY = 'slc_license_total'; // 缓存时的总记录数 const CACHE_VER_KEY = 'slc_license_cache_ver'; // 缓存版本号 const CACHE_VERSION = 2; // 缓存数据结构版本,升级时递增以自动失效旧缓存 function loadLicenseCache() { try { const fingerprints = JSON.parse(localStorage.getItem(CACHE_KEY) || '[]'); const ts = parseInt(localStorage.getItem(CACHE_TS_KEY) || '0'); const total = parseInt(localStorage.getItem(CACHE_TOTAL_KEY) || '0'); const ver = parseInt(localStorage.getItem(CACHE_VER_KEY) || '0'); // 缓存过期、格式非法、或版本不匹配时,视为无缓存 if (Date.now() - ts > CFG.CACHE_TTL || !Array.isArray(fingerprints) || ver !== CACHE_VERSION) { if (ver && ver !== CACHE_VERSION) console.warn(`[SLC] cache version mismatch (${ver} != ${CACHE_VERSION}), ignoring stale cache`); return { fps: new Set(), total: 0, ts: 0 }; } return { fps: new Set(fingerprints), total, ts }; } catch (e) { return { fps: new Set(), total: 0, ts: 0 }; } } function saveLicenseCache(fpList, totalCount) { try { localStorage.setItem(CACHE_KEY, JSON.stringify(fpList)); localStorage.setItem(CACHE_TS_KEY, String(Date.now())); localStorage.setItem(CACHE_TOTAL_KEY, String(totalCount)); localStorage.setItem(CACHE_VER_KEY, String(CACHE_VERSION)); console.log(`[SLC] saved ${fpList.length} fingerprints (${(JSON.stringify(fpList).length / 1024).toFixed(0)}KB) to cache`); } catch (e) { console.warn('[SLC] save cache failed:', e); } } function extractRowFingerprint(row) { // 从行中提取唯一指纹:日期+名称+类型+包ID const cells = row.querySelectorAll('td'); if (cells.length < 3) return null; const date = cells[0]?.textContent?.trim() || ''; const product = cells[1]?.textContent?.trim() || ''; const type = cells[2]?.textContent?.trim() || ''; return `${date}||${product}||${type}`; } // ==================== IndexedDB 缓存(存 HTML 行,容量大) ==================== const DB_NAME = 'slc_cache_v1'; const DB_STORE = 'rows'; let _db = null; async function openRowDB() { if (_db) return _db; return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onerror = () => reject(req.error); req.onsuccess = () => { _db = req.result; resolve(_db); }; req.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(DB_STORE)) { db.createObjectStore(DB_STORE, { keyPath: 'fp' }); } }; }); } async function saveRowsToDB(rows) { try { const db = await openRowDB(); const tx = db.transaction(DB_STORE, 'readwrite'); const store = tx.objectStore(DB_STORE); for (const row of rows) { if (row.fp) store.put({ fp: row.fp, html: row.html }); } await new Promise((resolve, reject) => { tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); }); console.log(`[SLC] saved ${rows.length} rows to IndexedDB`); } catch (e) { console.warn('[SLC] IndexedDB save failed:', e); } } async function loadRowsFromDB() { try { const db = await openRowDB(); const tx = db.transaction(DB_STORE, 'readonly'); const store = tx.objectStore(DB_STORE); return new Promise((resolve, reject) => { const req = store.getAll(); req.onsuccess = () => { const arr = req.result || []; // 返回 Map,方便按指定顺序查找 resolve(new Map(arr.map(r => [r.fp, r.html]))); }; req.onerror = () => reject(req.error); }); } catch (e) { console.warn('[SLC] IndexedDB load failed:', e); return new Map(); } } async function clearRowDB() { try { const db = await openRowDB(); const tx = db.transaction(DB_STORE, 'readwrite'); const store = tx.objectStore(DB_STORE); store.clear(); await new Promise((resolve, reject) => { tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); }); } catch (e) { console.warn('[SLC] IndexedDB clear failed:', e); } } function createLoadBar() { const hdr = document.querySelector(SEL.hdr); if (!hdr) return; // 进度条 const bar = document.createElement('div'); bar.className = 'slc-load-bar'; bar.id = 'slc-load-bar'; bar.innerHTML = '
'; hdr.after(bar); // 标题文字后方状态提示 const status = h('span', { class:'slc-load-status', id:'slc-load-status', text:t('loadStatusInit') }); const titleSpan = hdr.querySelector('.page-title-text'); if (titleSpan) titleSpan.appendChild(status); else hdr.appendChild(status); } function updateLoadBar(percent, text) { const fill = document.getElementById('slc-load-bar-fill'); if (fill) fill.style.width = Math.min(100, percent) + '%'; const status = document.getElementById('slc-load-status'); if (status && text != null) status.textContent = text; } function removeLoadBar() { const bar = document.getElementById('slc-load-bar'); if (bar) { bar.classList.add('hidden'); setTimeout(() => bar.remove(), 300); } const status = document.getElementById('slc-load-status'); if (status) status.remove(); } // ==================== 自动加载全部入库记录 ==================== async function autoLoadAllPages() { const nextLink = document.querySelector('a.license_paginator_next'); if (!nextLink) { const oldLoadMore = document.querySelector('#load_more_buttons, .load_more_buttons'); if (oldLoadMore) { await autoLoadOldStyle(); } return; } const url = new URL(location.href); if (url.searchParams.has('continuationToken') || url.searchParams.has('offset')) { toast(t('autoLoadingRedirect'), 'info', 3000); url.searchParams.delete('continuationToken'); url.searchParams.delete('offset'); location.replace(url.toString()); return; } const tb = getTb(); if (!tb) return; const paginatorCtn = document.querySelector('.license_paginator_ctn'); let totalCount = 0; if (paginatorCtn) { const spanText = paginatorCtn.querySelector('span')?.textContent || ''; const m = spanText.match(/共\s*(\d+)\s*个/); if (m) totalCount = parseInt(m[1]); } const hasSteamDBCol = !!document.querySelector('th.steamdb_license_id_col'); // 加载本地缓存(只存指纹,不存HTML,避免超限) const cache = loadLicenseCache(); const hasCache = cache.fps.size > 0; console.log(`[SLC] cache: ${cache.fps.size} fingerprints, total: ${cache.total}, hasCache: ${hasCache}`); // 显示加载进度条(header 下方) createLoadBar(); state.isLoading = true; // 加载期间抑制MutationObserver的applyPage,改用轻量统计刷新 // 有缓存时:从 IndexedDB 恢复HTML到DOM,再增量抓取检查新增 const allFingerprints = new Set(); // 本次加载收集的所有指纹(用于快速查重) const fpOrderList = []; // 有序指纹数组(新在前、旧在后,用于保存和恢复) const dbNewRows = []; // 新增行,保存到 IndexedDB if (hasCache) { // 1. 从 IndexedDB 恢复全部缓存行到DOM(按指纹原始顺序) updateLoadBar(5, t('loadStatusCache')); const cachedMap = await loadRowsFromDB(); // CACHE_KEY 中的指纹数组保持了页面加载顺序(新在前、旧在后) const fpOrder = JSON.parse(localStorage.getItem(CACHE_KEY) || '[]'); // 先收集有效的指纹和HTML,再批量解析+一次性插入DOM,避免逐行append触发N次重排 const validFps = []; const htmlParts = []; for (const fp of fpOrder) { const html = cachedMap.get(fp); if (!html) continue; htmlParts.push(html); validFps.push(fp); } let restored = 0; let firstCachedRow = null; if (htmlParts.length > 0) { // 一次性解析全部行HTML,比逐行 innerHTML 快数倍 const tmp = document.createElement('tbody'); tmp.innerHTML = htmlParts.join(''); const rowsArr = Array.from(tmp.children); const frag = document.createDocumentFragment(); for (let i = 0; i < validFps.length && i < rowsArr.length; i++) { const row = rowsArr[i]; if (!row) continue; frag.appendChild(row); restored++; allFingerprints.add(validFps[i]); fpOrderList.push(validFps[i]); if (!firstCachedRow) firstCachedRow = row; } tb.appendChild(frag); // 单次插入,仅触发一次重排 } console.log(`[SLC] restored ${restored} rows from IndexedDB`); refreshLoadingStats(); // 缓存恢复后立即更新统计数字 // 2. 从第一页开始抓取,对比指纹检查新增 let nextUrl2 = nextLink.href; let pageCount2 = 0; let fetched2 = 0; let newCount = 0; let consecutiveHits = 0; let staleCachePages = 0; // 连续"整页全为新行"的页数,用于检测缓存失效 let cacheStale = false; // 缓存是否被判定为失效 while (nextUrl2 && pageCount2 < CFG.INCREMENTAL_MAX_PAGES) { try { const res = await fetch(nextUrl2); if (!res.ok) break; const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const fetchedTb = doc.querySelector('table.account_table tbody'); if (!fetchedTb) break; const rows = Array.from(fetchedTb.rows).filter(r => !isHdr(r)); let pageNewCount = 0; for (const row of rows) { if (hasSteamDBCol && !row.querySelector('td.steamdb_license_id_col')) { const methodCell = row.querySelector('td.license_acquisition_col'); if (methodCell) { const steamdbCell = document.createElement('td'); steamdbCell.className = 'steamdb_license_id_col'; row.insertBefore(steamdbCell, methodCell); } } const fp = extractRowFingerprint(row); if (fp) allFingerprints.add(fp); if (fp && !cache.fps.has(fp)) { // 新增行:插入到缓存行之前(新许可在前,旧许可在后) tb.insertBefore(row, firstCachedRow); dbNewRows.push({ fp, html: row.outerHTML }); fpOrderList.unshift(fp); // 新许可插入到有序数组最前面 newCount++; pageNewCount++; consecutiveHits = 0; } else { consecutiveHits++; } } fetched2 += rows.length; pageCount2++; // 连续一整页全部命中缓存且无新增,说明已到缓存末尾 if (pageNewCount === 0 && rows.length > 0 && consecutiveHits >= rows.length) { console.log(`[SLC] incremental: page ${pageCount2} all cache hits, stopping`); break; } // 兜底:连续多页全部为新行,说明缓存已失效(指纹全不匹配),提前终止避免卡死 if (rows.length > 0 && pageNewCount === rows.length) { staleCachePages++; if (staleCachePages >= CFG.STALE_CACHE_THRESHOLD) { console.warn(`[SLC] incremental: ${staleCachePages} consecutive all-new pages, cache likely stale, stopping`); cacheStale = true; break; } } else { staleCachePages = 0; } const pct = totalCount ? Math.min((restored + fetched2) / totalCount * 100, 100) : Math.min(pageCount2 * 5, 95); updateLoadBar(pct, t('loadStatusCheck').replace('{count}', fetched2) + ` [${pageCount2}]`); refreshLoadingStats(); // 每页加载后实时更新统计数字 const fetchedNext = doc.querySelector('a.license_paginator_next'); nextUrl2 = fetchedNext?.href || null; await new Promise(r => setTimeout(r, CFG.PAGE_DELAY)); } catch (err) { console.warn('[SLC] incremental load error:', err); break; } } // 保存新增的指纹和HTML if (cacheStale) { // 缓存已失效,清除旧缓存,下次加载将走全量路径 console.warn('[SLC] cache stale, clearing old cache'); try { localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_TS_KEY); localStorage.removeItem(CACHE_TOTAL_KEY); localStorage.removeItem(CACHE_VER_KEY); await clearRowDB(); } catch(e) { console.warn('[SLC] clear stale cache failed:', e); } toast(t('loadStatusDone') + ' (cache cleared)', 'info', 3000); } else { if (dbNewRows.length > 0) { await saveRowsToDB(dbNewRows); } if (allFingerprints.size > 0) { saveLicenseCache(fpOrderList, restored + fetched2); } } updateLoadBar(100, t('loadStatusDone')); } if (!hasCache) { // 无缓存,正常加载全部分页,同时保存HTML到IndexedDB let loaded = dataRows().length; let nextUrl = nextLink.href; let pageCount = 0; const maxPages = CFG.MAX_PAGES; const dbRows = []; // 收集所有行用于IndexedDB缓存 const totalPges = totalCount ? Math.ceil(totalCount / 100) : 0; // 根据许可总数估算总页数 while (nextUrl && pageCount < maxPages) { try { const res = await fetch(nextUrl); if (!res.ok) { console.warn(`[SLC] fetch ${nextUrl} => ${res.status}`); break; } const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const fetchedTb = doc.querySelector('table.account_table tbody'); if (!fetchedTb) break; const rows = Array.from(fetchedTb.rows).filter(r => !isHdr(r)); const frag = document.createDocumentFragment(); // 批量插入,减少重排 for (const row of rows) { if (hasSteamDBCol && !row.querySelector('td.steamdb_license_id_col')) { const methodCell = row.querySelector('td.license_acquisition_col'); if (methodCell) { const steamdbCell = document.createElement('td'); steamdbCell.className = 'steamdb_license_id_col'; row.insertBefore(steamdbCell, methodCell); } } frag.appendChild(row); const fp = extractRowFingerprint(row); if (fp) { allFingerprints.add(fp); fpOrderList.push(fp); // 页面顺序即新→旧 dbRows.push({ fp, html: row.outerHTML }); } } tb.appendChild(frag); // 单次插入,仅触发一次重排 loaded += rows.length; pageCount++; const pct = totalPges ? Math.min(pageCount / totalPges * 100, 100) : Math.min(pageCount * 5, 95); updateLoadBar(pct, t('loadStatusFetch').replace('{loaded}', loaded).replace('{total}', totalCount || loaded).replace('{page}', pageCount)); refreshLoadingStats(); // 每页加载后实时更新统计数字 const fetchedNext = doc.querySelector('a.license_paginator_next'); nextUrl = fetchedNext?.href || null; await new Promise(r => setTimeout(r, CFG.PAGE_DELAY)); } catch (err) { console.error('[SLC] autoLoadAllPages error:', err); toast(t('autoLoadingError').replace('{msg}', err.message), 'error', 5000); break; } } // 保存指纹到localStorage,HTML到IndexedDB if (allFingerprints.size > 0) { saveLicenseCache(fpOrderList, loaded); } if (dbRows.length > 0) { await saveRowsToDB(dbRows); } } state.isLoading = false; // 加载完成,恢复MutationObserver的正常响应 const paginator = document.querySelector('.license_paginator_ctn'); if (paginator) paginator.style.display = 'none'; // 隐藏进度条 setTimeout(() => { updateLoadBar(100, t('loadStatusDone')); setTimeout(() => removeLoadBar(), 300); }, 300); const finalCount = dataRows().length; toast(t('autoLoadingDone').replace('{count}', finalCount), 'success', 3000); console.log(`[SLC] autoLoadAllPages: loaded ${finalCount} licenses (cache fps: ${cache.fps.size}).`); } // 旧版页面自动加载(load_more_buttons 机制) async function autoLoadOldStyle() { const tb = getTb(); if (!tb) return; let lastCount = 0; let stableCount = 0; const progressToast = toast(t('autoLoadingLicenses'), 'info', 999999); while (stableCount < 3) { const currentCount = tb.rows.length; if (currentCount === lastCount) { stableCount++; } else { stableCount = 0; } lastCount = currentCount; const btn = document.querySelector('#load_more_buttons') || document.querySelector('.load_more_buttons') || [...document.querySelectorAll('a, button')].find(el => /load\s*more|加载更多|顯示更多|显示更多/i.test(el.textContent)); if (btn) { btn.click(); if (progressToast?.el) progressToast.el.textContent = `${t('autoLoadingLicenses')} (${currentCount})`; await new Promise(r => setTimeout(r, 500)); } else { window.scrollTo(0, document.body.scrollHeight); await new Promise(r => setTimeout(r, 300)); const newBtn = document.querySelector('#load_more_buttons') || document.querySelector('.load_more_buttons'); if (!newBtn) break; } } if (progressToast?.hide) progressToast.hide(); const finalCount = dataRows().length; toast(t('autoLoadingDone').replace('{count}', finalCount), 'success', 3000); console.log(`[SLC] autoLoadOldStyle: loaded ${finalCount} licenses.`); } // ==================== 初始化 ==================== // 防重入标记 let _initialized = false; async function init(){ if (_initialized) return; _initialized = true; locale=detectLang(); console.log(`Steam License Classifier v${VER} | lang: ${locale}`); injectCSS('steam-license-styles', CSS_CRITICAL); updateTitle();addBreadcrumb(); setTimeout(() => injectCSS('steam-license-styles-deferred', CSS_DEFERRED), 0); try{ await waitEl(`${SEL.tb} tr td`); // 先显示主面板(含筛选按钮、搜索框等),用户立即可见 addFilterBtns(); // 再异步加载剩余页面,加载期间统计数字实时更新 await autoLoadAllPages(); // 加载完成后做最终分页渲染 invalidateDataRows(); state.isProcessing = true; try { markRows(); recalc(); lockWidths(); applyPage(); } finally { state.isProcessing = false; } } catch(err){console.warn('Steam License Classifier init failed:',err.message);} finally{state.isLoading=false;} // 确保无论如何都重置加载状态 } if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',function onReady(){document.removeEventListener('DOMContentLoaded',onReady);init();});} else init(); // BFCache 支持:页面恢复时重新初始化 window.addEventListener('pageshow',e=>{if(e.persisted){_initialized=false;init();}}); window.addEventListener('pagehide',e=>{ if(!e.persisted){disposers.forEach(d=>d());disposers.length=0;state.obs?.disconnect();clearTimeout(state.timers.search);clearTimeout(state.timers.obs);} }); // ==================== 绝版游戏检查 ==================== // 懒加载:仅在用户点击按钮时才解析,避免阻塞启动 let _delistedDB = null, _delistedApps = null; function getDelistedApps() { if (_delistedApps) return _delistedApps; if (!_delistedDB) { try { _delistedDB = JSON.parse(GM_getResourceText('delistedData') || '{}'); } catch(e) { console.warn('[SLC] DELISTED_DB 加载失败:', e); _delistedDB = {}; } } let rawApps = []; if (Array.isArray(_delistedDB.data)) rawApps = _delistedDB.data; else if (_delistedDB.delisted && Array.isArray(_delistedDB.delisted.apps)) rawApps = _delistedDB.delisted.apps; _delistedApps = rawApps.map(app => ({ appid: app.appId || app.appid, name: app.name, type: app.delistType || app.type || 'Delisted', changed: app.delistedAt || app.changed, owners: app.ownerPercentage != null ? app.ownerPercentage + '%' : (app.owners || '-'), achievements: (app.achievementCount != null && app.achievementCount >= 0) ? app.achievementCount : (app.achievements > 0 ? app.achievements : null) })); return _delistedApps; } // 统一类型映射(消除 getDelistedTypeKey/getDelistedTypeLabel 中的重复映射) const _DELISTED_TYPE_MAP = { 'Delisted': { key: 'delisted', labelKey: 'typeDelisted' }, 'Purchase disabled': { key: 'purchase_disabled', labelKey: 'typePurchaseDisabled' }, 'F2P (unavailable)': { key: 'f2p_unavailable', labelKey: 'typeF2P' }, 'Retail only': { key: 'retail_only', labelKey: 'typeRetailOnly' }, 'Test app': { key: 'test_app', labelKey: 'typeTestApp' } }; // 反向映射:key → 原始名 const _KEY_TO_RAW = {}; for (const [raw, v] of Object.entries(_DELISTED_TYPE_MAP)) _KEY_TO_RAW[v.key] = raw; function getDelistedTypeKey(type) { if (!type) return 'delisted'; if (type.includes('_')) return type; return _DELISTED_TYPE_MAP[type]?.key || 'delisted'; } function getDelistedTypeLabel(type) { if (!type) return t('typeDelisted'); if (type.includes('_')) return t(_DELISTED_TYPE_MAP[_KEY_TO_RAW[type]]?.labelKey || 'typeDelisted'); return t(_DELISTED_TYPE_MAP[type]?.labelKey || 'typeDelisted'); } // 拥有状态辅助函数(消除 6 处重复表达式) const isOwned = (ids, app) => ids.has(Number(app.appid)) || ids.has(String(app.appid)); // 类型颜色常量(模块级,避免每次渲染重建) const _TYPE_COLORS = { delisted:'#f44336', purchase_disabled:'#ff9800', f2p_unavailable:'#ce93d8', retail_only:'#64b5f6', test_app:'#26c6da' }; const _dlst = { ownedIds: null, state: { search:'', typeFilter:'all', ownershipFilter:'all', page:1, pageSize:60, viewMode:'card', sortBy:'name', sortDir:'asc', showCover:false } }; // 共享 DOM 解析缓存(消除 detectSteamId/getAccessTokenSync 中的重复 getElementById) let _cachedAppConfig = null; function getAppConfig() { if (_cachedAppConfig) return _cachedAppConfig; try { const el = document.getElementById('application_config'); if (el) { _cachedAppConfig = { userinfo: JSON.parse(el.getAttribute('data-userinfo') || '{}'), storeConfig: JSON.parse(el.getAttribute('data-store_user_config') || '{}') }; return _cachedAppConfig; } } catch {} return null; } function detectSteamId() { try { if (unsafeWindow.g_steamID) return unsafeWindow.g_steamID; } catch {} const cfg = getAppConfig(); if (cfg?.userinfo?.steamid) return String(cfg.userinfo.steamid); const m = document.documentElement.innerHTML.match(/g_steamID\s*=\s*"(\d{17})"/); return m ? m[1] : ''; } let _cachedWebApiToken = null; function getAccessTokenSync() { const cfg = getAppConfig(); if (cfg?.storeConfig?.webapi_token) return cfg.storeConfig.webapi_token; const m = document.documentElement.innerHTML.match(/"webapi_token"\s*:\s*"([^"]+)"/); return m ? m[1] : null; } function fetchWebApiTokenFromStore() { return new Promise((resolve) => { if (_cachedWebApiToken) { resolve(_cachedWebApiToken); return; } GM_xmlhttpRequest({ method: 'GET', url: 'https://store.steampowered.com/', timeout: 10000, onload(resp) { try { const html = resp.responseText; const m1 = html.match(/id="application_config"[^>]*data-store_user_config="([^"]*)"/); if (m1 && m1[1]) { const decoded = m1[1].replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); const config = JSON.parse(decoded); if (config.webapi_token) { _cachedWebApiToken = config.webapi_token; resolve(config.webapi_token); return; } } const m2 = html.match(/"webapi_token"\s*:\s*"([^"]+)"/); if (m2 && m2[1]) { _cachedWebApiToken = m2[1]; resolve(m2[1]); return; } } catch (e) { /* ignore */ } resolve(null); }, onerror() { resolve(null); }, ontimeout() { resolve(null); } }); }); } async function getAccessToken() { if (_cachedWebApiToken) return _cachedWebApiToken; const syncToken = getAccessTokenSync(); if (syncToken) { _cachedWebApiToken = syncToken; return syncToken; } return await fetchWebApiTokenFromStore(); } // 封装 GM_xmlhttpRequest 为 Promise function gmFetch(url, timeout = 15000) { return new Promise((res, rej) => { GM_xmlhttpRequest({ method: 'GET', url, timeout, onload(r) { try { const text = r.responseText || ''; if (!text || text.trim().charAt(0) === '<') { rej(new Error('API 返回非 JSON')); return; } res(JSON.parse(text)); } catch(e) { rej(e); } }, onerror() { rej(new Error('网络错误')); }, ontimeout() { rej(new Error('超时')); } }); }); } async function fetchOwnedAppIds() { const steamId = detectSteamId(); if (!steamId) throw new Error('无法检测 SteamID'); const token = await getAccessToken(); if (!token) throw new Error('未获取到 access_token,请确保已登录 Steam 商店'); // 通过 GetOwnedGames 获取个人游戏库 try { const data = await gmFetch(`https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?access_token=${token}&steamid=${steamId}&include_appinfo=0&include_played_free_games=1&format=json`); const apps = data?.response?.games || []; if (apps.length > 0) return new Set(apps.map(a => a.appid)); } catch(e) { console.warn('[SLC] GetOwnedGames 获取失败:', e); } throw new Error('无法获取游戏库,请确保已登录 Steam 且个人资料公开'); } function getFilteredDelistedApps(allApps, ds, ownedIds) { let apps = allApps; if (ds.typeFilter !== 'all') apps = apps.filter(a => getDelistedTypeKey(a.type) === ds.typeFilter); if (ds.ownershipFilter === 'owned') apps = apps.filter(a => isOwned(ownedIds, a)); else if (ds.ownershipFilter === 'missing') apps = apps.filter(a => !isOwned(ownedIds, a)); if (ds.search) { const q = ds.search.toLowerCase(); apps = apps.filter(a => (a.name||'').toLowerCase().includes(q) || String(a.appid).includes(q)); } // 排序 const dir = ds.sortDir === 'asc' ? 1 : -1; apps = [...apps].sort((a, b) => { if (ds.sortBy === 'name') { return dir * (a.name || '').localeCompare(b.name || '', 'zh-CN'); } else if (ds.sortBy === 'date') { const da = a.changed || '', db = b.changed || ''; return dir * da.localeCompare(db); } else if (ds.sortBy === 'rarity') { const pa = parseFloat(a.owners) || 0, pb = parseFloat(b.owners) || 0; return dir * (pa - pb); } return 0; }); return apps; } function renderDelistedContent(bodyEl, allApps, ownedIds) { const ds = _dlst.state; const apps = getFilteredDelistedApps(allApps, ds, ownedIds); const totalDelisted = allApps.length; // Type distribution + 拥有统计(单次遍历) const typeCounts = {}, typeOwned = {}; allApps.forEach(app => { const key = getDelistedTypeKey(app.type); typeCounts[key] = (typeCounts[key] || 0) + 1; if (isOwned(ownedIds, app)) typeOwned[key] = (typeOwned[key] || 0) + 1; }); const ownedDelisted = Object.values(typeOwned).reduce((s, v) => s + v, 0); const missingDelisted = totalDelisted - ownedDelisted; const delistedPct = totalDelisted > 0 ? (ownedDelisted / totalDelisted * 100).toFixed(3) : '0.000'; let html = ''; // 固定头部区域:KPI + 进度条 + 类型筛选 + 搜索 html += '
'; // KPI cards (owned/missing are clickable filters) const ofAll = ds.ownershipFilter === 'all'; const ofOwned = ds.ownershipFilter === 'owned'; const ofMissing = ds.ownershipFilter === 'missing'; html += `
${totalDelisted.toLocaleString()}
${t('delistedTotal')}
${ownedDelisted.toLocaleString()}
${t('delistedOwned')}
${missingDelisted.toLocaleString()}
${t('delistedMissing')}
${delistedPct}%
${t('delistedOwnership')}
`; // Progress bar html += `
`; // Type distribution tags (merged with filter buttons) + pagination on the right const totalPages = Math.max(1, Math.ceil(apps.length / ds.pageSize)); if (ds.page > totalPages) ds.page = totalPages; const typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]); const typeButtons = [ { key:'all', label: t('all'), color:'#66c0f4', count: totalDelisted, owned: ownedDelisted }, ...typeEntries.map(([key, count]) => ({ key, label: getDelistedTypeLabel(key), color: _TYPE_COLORS[key] || '#8f98a0', count, owned: typeOwned[key] || 0 })) ]; html += `
${typeButtons.map(tb => `` ).join('')}
${ds.page}/${totalPages} (${apps.length})
`; // Toolbar: search + view mode const sortIcon = ds.sortDir === 'asc' ? '↑' : '↓'; html += `
`; html += '
'; // end header // 可滚动内容区域 html += '
' + buildDelistedScroll(apps, ds, ownedIds) + '
'; bodyEl.innerHTML = html; // Event bindings const searchEl = bodyEl.querySelector('#slc-dlst-search'); if (searchEl) { let searchTimer; searchEl.addEventListener('input', e => { ds.search = e.target.value; ds.page = 1; clearTimeout(searchTimer); searchTimer = setTimeout(() => updateDelistedScroll(bodyEl, allApps, ownedIds), 200); }); } // 事件委托:统一处理所有点击事件 bodyEl.addEventListener('click', e => { const target = e.target.closest('[data-ownership], .slc-delisted-type-btn, #slc-dlst-card, #slc-dlst-list, #slc-dlst-cover, .slc-dlst-sort-btn'); if (!target || !bodyEl.contains(target)) return; // KPI 卡片:拥有状态筛选 if (target.dataset.ownership) { ds.ownershipFilter = target.dataset.ownership; ds.page = 1; updateDelistedScroll(bodyEl, allApps, ownedIds); return; } // 类型筛选按钮 if (target.classList.contains('slc-delisted-type-btn')) { ds.typeFilter = target.dataset.type; ds.page = 1; bodyEl.querySelectorAll('.slc-delisted-type-btn').forEach(b => { const c = b.dataset.color; const isActive = b.dataset.type === ds.typeFilter; b.classList.toggle('active', isActive); b.style.background = isActive ? c+'33' : 'rgba(255,255,255,.05)'; b.style.color = isActive ? c : '#94a3b8'; b.style.border = `1px solid ${isActive ? c+'66' : 'rgba(255,255,255,.1)'}`; b.style.fontWeight = isActive ? '700' : '500'; b.style.boxShadow = isActive ? '0 2px 6px rgba(0,0,0,.3)' : 'none'; }); updateDelistedScroll(bodyEl, allApps, ownedIds); return; } // 视图切换 if (target.id === 'slc-dlst-card') { ds.viewMode = 'card'; updateDelistedScroll(bodyEl, allApps, ownedIds); return; } if (target.id === 'slc-dlst-list') { ds.viewMode = 'list'; updateDelistedScroll(bodyEl, allApps, ownedIds); return; } // 封面按钮 if (target.id === 'slc-dlst-cover') { ds.showCover = !ds.showCover; updateDelistedScroll(bodyEl, allApps, ownedIds); return; } // 排序按钮 if (target.classList.contains('slc-dlst-sort-btn')) { const sortBy = target.dataset.sort; if (ds.sortBy === sortBy) { ds.sortDir = ds.sortDir === 'asc' ? 'desc' : 'asc'; } else { ds.sortBy = sortBy; ds.sortDir = 'asc'; } ds.page = 1; updateDelistedScroll(bodyEl, allApps, ownedIds); } }); // KPI 卡片鼠标样式 bodyEl.querySelectorAll('.slc-delisted-kpi-card[data-ownership]').forEach(card => { card.style.cursor = 'pointer'; }); } // 构建游戏元数据 HTML(卡片/列表共用) function buildDelistedMeta(app, typeKey) { return `${getDelistedTypeLabel(app.type)} ${app.owners || '-'} ${app.changed ? `${app.changed}` : ''} ${app.achievements ? `🏆 ${app.achievements}` : ''}`; } function buildDelistedScroll(apps, ds, ownedIds) { const totalPages = Math.max(1, Math.ceil(apps.length / ds.pageSize)); if (ds.page > totalPages) ds.page = totalPages; const start = (ds.page - 1) * ds.pageSize; const pageApps = apps.slice(start, start + ds.pageSize); if (ds.viewMode === 'card') { return `
${pageApps.map(app => { const owned = isOwned(ownedIds, app); const typeKey = getDelistedTypeKey(app.type); return `
${owned ? '✓ ' + t('delistedOwned') : t('delistedMissing')}
${app.name || 'AppID: '+app.appid}
${buildDelistedMeta(app, typeKey)}
`; }).join('')}
`; } return `
${pageApps.map(app => { const owned = isOwned(ownedIds, app); const typeKey = getDelistedTypeKey(app.type); const coverUrl = `https://cdn.akamai.steamstatic.com/steam/apps/${app.appid}/header_292x136.jpg`; return `
${ds.showCover ? `` : ''}
${app.name || 'AppID: '+app.appid}
${buildDelistedMeta(app, typeKey)} ${owned ? '✓ ' + t('delistedOwned') : t('delistedMissing')}
`; }).join('')}
`; } // 仅更新滚动区域(不重建头部,避免搜索框闪烁) function updateDelistedScroll(bodyEl, allApps, ownedIds) { const ds = _dlst.state; const apps = getFilteredDelistedApps(allApps, ds, ownedIds); const totalPages = Math.max(1, Math.ceil(apps.length / ds.pageSize)); if (ds.page > totalPages) ds.page = totalPages; const scrollEl = bodyEl.querySelector('.slc-delisted-scroll'); if (scrollEl) scrollEl.innerHTML = buildDelistedScroll(apps, ds, ownedIds); // 更新分页按钮状态 const prevBtn = bodyEl.querySelector('#slc-dlst-prev'); const nextBtn = bodyEl.querySelector('#slc-dlst-next'); const pageInfo = bodyEl.querySelector('.slc-delisted-pagination .page-info'); if (prevBtn) prevBtn.disabled = ds.page <= 1; if (nextBtn) nextBtn.disabled = ds.page >= totalPages; if (pageInfo) pageInfo.textContent = `${ds.page}/${totalPages} (${apps.length})`; // 绑定分页按钮事件 if (prevBtn) prevBtn.onclick = () => { ds.page--; updateDelistedScroll(bodyEl, allApps, ownedIds); }; if (nextBtn) nextBtn.onclick = () => { ds.page++; updateDelistedScroll(bodyEl, allApps, ownedIds); }; // 更新视图模式按钮高亮 const cardBtn = bodyEl.querySelector('#slc-dlst-card'); const listBtn = bodyEl.querySelector('#slc-dlst-list'); const coverBtn = bodyEl.querySelector('#slc-dlst-cover'); if (cardBtn) cardBtn.classList.toggle('active', ds.viewMode === 'card'); if (listBtn) listBtn.classList.toggle('active', ds.viewMode === 'list'); if (coverBtn) coverBtn.classList.toggle('active', ds.showCover); // 更新排序按钮高亮和箭头 const sortIcon = ds.sortDir === 'asc' ? '↑' : '↓'; const sortLabels = { name: t('delistedCardName') || '名称', date: t('delistedDate') || '日期', rarity: t('delistedOwners') || '稀有' }; bodyEl.querySelectorAll('.slc-dlst-sort-btn').forEach(btn => { const sortBy = btn.dataset.sort; const isActive = sortBy === ds.sortBy; btn.classList.toggle('active', isActive); btn.textContent = sortLabels[sortBy] + (isActive ? ' ' + sortIcon : ''); }); // 更新 KPI 卡片高亮 bodyEl.querySelectorAll('.slc-delisted-kpi-card[data-ownership]').forEach(card => { card.classList.toggle('active', card.dataset.ownership === ds.ownershipFilter); }); } async function showDelisted() { const allApps = getDelistedApps(); if (!allApps.length) { toast(t('delistedNoData'), 'error'); return; } let refs = ui.delistedRefs; if (!refs) { refs = ui.delistedRefs = createChartModal('delisted', t('delistedTitle'), '#f44336', 'dataNote'); } const { modal, bodyEl } = refs; hideAllModals(); if (_dlst.ownedIds) { renderDelistedContent(bodyEl, allApps, _dlst.ownedIds); modal.classList.add('visible'); return; } // Loading state bodyEl.innerHTML = `
${t('delistedFetching')}
`; modal.classList.add('visible'); try { _dlst.ownedIds = await fetchOwnedAppIds(); toast(t('delistedLoaded') + ` (${_dlst.ownedIds.size})`, 'success'); renderDelistedContent(bodyEl, allApps, _dlst.ownedIds); } catch(e) { console.error('[SLC] 获取游戏库失败:', e); bodyEl.innerHTML = `
${t('delistedFetchFail')}: ${e.message}
`; } } // ==================== Keyboard Shortcuts ==================== document.addEventListener('keydown',e=>{ if(e.target.matches('input,select,textarea')||e.ctrlKey||e.altKey||e.metaKey){ if(e.key==='Escape'&&e.target.matches('input,select,textarea'))e.target.blur(); return; } if(e.key==='Escape'){hideAllModals();return;} switch(e.key){ case'/':e.preventDefault();ui.searchInput?.focus();break; case'ArrowLeft':if(state.page>1){state.page--;applyPage();}break; case'ArrowRight':if(state.page{ const hasNew=muts.some(m=>Array.from(m.addedNodes).some(n=>n.nodeType===1&&n.nodeName==='TR')); if(hasNew){clearTimeout(state.timers.obs);state.timers.obs=setTimeout(()=>{ if(state.isProcessing||state.isLoading)return; // 加载期间由refreshLoadingStats接管 invalidateDataRows(); markRows();recalc();lockWidths();applyPage(); },CFG.OBS_DEBOUNCE);} }); state.obs.observe(tb,{childList:true}); disposers.push(()=>{state.obs?.disconnect();state.obs=null;clearTimeout(state.timers.obs);state.timers.obs=null;}); } })();