let accounts=[], products=[], productInfo=null, selProdId=null, selProdName='', selAccs=new Set(); let productSearch=''; let scheduleActive=false, countdownTimer=null, lastResults={}; let lastLogId=0; let statusMap={}; // ← 加这行 // applyTheme 应用亮色/暗色主题,并把选择保存到本地。 function applyTheme(theme) { let nextTheme = theme === 'light' ? 'light' : 'dark'; document.body.dataset.theme = nextTheme; localStorage.setItem('uiTheme', nextTheme); let btn = document.getElementById('btnTheme'); if (btn) { btn.textContent = nextTheme === 'light' ? '☀' : '☾'; btn.title = nextTheme === 'light' ? '切换到暗色' : '切换到亮色'; btn.setAttribute('aria-label', btn.title); } } // toggleTheme 在亮色和暗色之间切换。 function toggleTheme() { applyTheme(document.body.dataset.theme === 'light' ? 'dark' : 'light'); } // init 初始化页面状态:恢复本地缓存、加载账号和商品、启动时间与结果轮询。 async function init(){ let savedTheme=localStorage.getItem('uiTheme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); applyTheme(savedTheme); // 恢复缓存的选择 let savedAccs=localStorage.getItem('selAccs'); let savedProd=localStorage.getItem('selProd'); let savedProdName=localStorage.getItem('selProdName'); let savedTime=localStorage.getItem('targetTime'); let savedAdv=localStorage.getItem('advance'); let savedStatus=localStorage.getItem('statusMap'); // ← 提前读取 let savedProductSearch=localStorage.getItem('productSearch'); clearLegacyProductBindings(); if(savedAccs) selAccs=new Set(JSON.parse(savedAccs)); if(savedProd){ selProdId=savedProd; selProdName=savedProdName||''; } if(savedTime) document.getElementById('targetTime').value=savedTime; if(savedAdv) document.getElementById('advance').value=savedAdv; if(savedStatus) statusMap=JSON.parse(savedStatus); // ← 提前恢复 if(savedProductSearch) { productSearch=savedProductSearch; document.getElementById('productSearch').value=productSearch; } updateSelectedProductCard(); updateDashboardStats(); await loadAccounts(); await loadProducts(false); updateTime(); clearLog(); // ← 加这行,启动时清空日志区 //localStorage.removeItem('statusMap'); // ← 加这行 // statusMap={}; // ← 加这行 这两行删除就保存成功失败得状态 // 清空后端旧结果 await App.ClearResults(); App.GetVersion().then(v => { document.getElementById('versionTag').textContent = 'v' + v; }).catch(()=>{}); setInterval(updateTime,1000); setInterval(checkResults,2000); setInterval(fetchServerLogs,2000); fetchServerLogs(); } // setProductSearch 更新商品搜索词,并即时过滤左侧商品列表。 function setProductSearch(value) { productSearch = String(value || '').trim(); if (productSearch) localStorage.setItem('productSearch', productSearch); else localStorage.removeItem('productSearch'); renderProducts(); renderProductMeta(); } // clearProductSearch 清空商品搜索词。 function clearProductSearch() { productSearch = ''; localStorage.removeItem('productSearch'); document.getElementById('productSearch').value = ''; renderProducts(); renderProductMeta(); } // filteredProducts 根据搜索词过滤商品,支持名称、ID、积分和库存。 function filteredProducts() { let keyword = productSearch.toLowerCase(); if (!keyword) return products; return products.filter(p => [ p.commodityName, p.commodityId, p.score, p.count ].some(v => String(v ?? '').toLowerCase().includes(keyword))); } // updateTime 从后端获取斗鱼服务端时间,并刷新顶部时间展示。 function updateTime(){ App.GetTime().then(d=>{ document.getElementById('svrTime').textContent=d.serverTime||'--'; }).catch(()=>{}); } // apiJSON 统一处理 Wails 绑定调用,失败时抛出后端返回的错误文案。 async function apiJSON(fn) { let data = await fn(); if (data && data.success === false && data.error) { throw new Error(data.error); } return data; } // Wails 绑定快捷引用 const App = window.go.main.App; // reloadAccounts 通知后端重新读取 user.txt,然后刷新前端账号表格。 async function reloadAccounts(){ let d=await App.ReloadAccounts(); await loadAccounts(); toast('账号已刷新',`共 ${d.count} 个账号`,'success'); } // queryAllScore 并发查询所有账号积分,并把积分按账号名缓存到 localStorage。 async function queryAllScore() { addLog('正在并发查询积分...', 'i'); let tasks = []; for (let i = 0; i < accounts.length; i++) { tasks.push((async (idx) => { let a = accounts[idx]; try { let d = await App.QueryScore(a.name); let score = (d.score !== undefined && d.score !== null && d.score !== '') ? d.score : 'N/A'; localStorage.setItem('accScore_' + a.name, score); let el = document.getElementById('score_' + idx); if (el) el.textContent = score; } catch (e) { } })(i)); } await Promise.all(tasks); addLog('积分查询完成', 's'); toast('积分查询完成', '', 'success'); } // ==================== Toast提示 ==================== // toast 创建右侧滑入提示,按类型控制颜色,并在指定时间后自动移除。 function toast(title, body, type, duration){ type=type||'info'; duration=duration||4000; let container=document.getElementById('toastContainer'); let div=document.createElement('div'); div.className='toast toast-'+type; div.innerHTML=`
${title}
${body}
`; container.appendChild(div); // 短延迟回调用于触发 CSS 过渡,让提示从右侧滑入。 setTimeout(()=>container.classList.add('show'),10); // 外层回调控制停留时长,内层回调等待淡出动画结束后移除节点。 setTimeout(()=>{ div.style.opacity='0'; div.style.transition='opacity .3s'; setTimeout(()=>{div.remove();if(container.children.length===0)container.classList.remove('show');},300); },duration); } // ==================== 商品 ==================== // loadProducts 从后端实时刷新斗鱼商品;失败时不读取旧缓存,避免库存误判。 async function loadProducts(showToast = true){ let btn = document.getElementById('btnLoadProducts'); let meta = document.getElementById('productMeta'); btn.disabled = true; btn.textContent = '刷新中...'; meta.className = 'product-meta'; meta.textContent = '正在请求斗鱼商品接口'; addLog('正在实时刷新商品...','i'); try{ let d = await App.LoadProducts(true); products = Array.isArray(d.products) ? d.products : []; productInfo = d; if (selProdId && !products.some(p => p.commodityId === selProdId)) { clearSelectedProduct(); } renderProducts(); renderProductMeta(); renderAccounts(); addLog(`商品刷新完成,共 ${products.length} 个,库存时间 ${d.fetchedAt || '--'}`,'s'); if (showToast) toast('商品已刷新',`共 ${products.length} 个,库存时间 ${d.fetchedAt || '--'}`,'success'); }catch(e){ products = []; productInfo = null; clearSelectedProduct(); renderProducts(); renderProductMeta(e.message); addLog('商品刷新失败: ' + e.message,'e'); toast('商品刷新失败', e.message || '请检查网络连接','error'); }finally{ btn.disabled = false; btn.textContent = '刷新商品'; } } // renderProducts 根据当前商品数组绘制左侧商品列表,并标记已选商品。 function renderProducts(){ let h=''; let list = filteredProducts(); document.getElementById('btnClearProductSearch').style.display = productSearch ? 'block' : 'none'; // 遍历商品缓存,生成每个商品的点击项和库存、积分信息。 list.forEach(p=>{ h+=`
${escHtml(p.commodityName)}
${p.score} 积分库存 ${p.count}
`; }); let emptyText = productSearch ? `没有匹配“${escHtml(productSearch)}”的商品` : '暂无实时商品数据'; document.getElementById('prodList').innerHTML=h||`
${emptyText}
`; updateSelectedProductCard(); } // renderProductMeta 显示库存来源和刷新时间,避免把旧库存当实时库存。 function renderProductMeta(errorMsg) { let meta = document.getElementById('productMeta'); if (errorMsg) { meta.className = 'product-meta warn'; meta.textContent = '刷新失败,未使用旧库存'; return; } if (!productInfo || !productInfo.fetchedAt) { meta.className = 'product-meta'; meta.textContent = '库存未刷新'; return; } meta.className = 'product-meta ' + (productInfo.stale ? 'warn' : 'good'); if (productSearch) { meta.textContent = `实时库存 ${filteredProducts().length}/${productInfo.count} 个,刷新于 ${productInfo.fetchedAt}`; } else { meta.textContent = `实时库存 ${productInfo.count} 个,刷新于 ${productInfo.fetchedAt}`; } } // selProduct 记录当前选择的商品,提交时会应用到所有已勾选账号。 function selProduct(id, name) { selProdId = id; selProdName = name; localStorage.setItem('selProd', id); localStorage.setItem('selProdName', name); renderProducts(); renderAccounts(); updateSelectedProductCard(); updateDashboardStats(); } // clearSelectedProduct 清理已经不存在的商品选择。 function clearSelectedProduct() { selProdId = null; selProdName = ''; localStorage.removeItem('selProd'); localStorage.removeItem('selProdName'); updateSelectedProductCard(); updateDashboardStats(); } // ==================== 账号 ==================== // loadAccounts 从后端获取账号列表,清理无效本地缓存后重新渲染账号表格。 async function loadAccounts() { accounts = await App.GetAccounts(); selAccs = new Set([...selAccs].filter(i => i >= 0 && i < accounts.length)); localStorage.setItem('selAccs', JSON.stringify([...selAccs])); // map 回调抽取账号名,用于判断 localStorage 中的旧积分缓存是否仍然有效。 let validNames = new Set(accounts.map(a => a.name)); for (let key in localStorage) { // 只清理积分缓存,商品选择改为全局当前商品。 if (key.startsWith('accScore_')) { let name = key.substring(key.indexOf('_') + 1); if (!validNames.has(name)) { localStorage.removeItem(key); } } } renderAccounts(); } // renderAccounts 根据账号、选择状态、商品绑定、积分和抢购结果绘制账号表格。 function renderAccounts() { document.getElementById('accCnt').textContent = accounts.length; document.getElementById('selectedCnt').textContent = selAccs.size; document.getElementById('accountPanelSub').textContent = `${accounts.length} 个账号,已选 ${selAccs.size} 个`; let chkAll = document.getElementById('chkAll'); if (chkAll) chkAll.checked = accounts.length > 0 && selAccs.size === accounts.length; // 将账号数组映射为表格行 HTML,行内状态从 localStorage 和 statusMap 读取。 let h = accounts.map((a, i) => { let isSel = selAccs.has(i); // 商品列展示当前选中商品,提交时会应用到所有勾选账号。 let prodTag = selProdName ? `${escHtml(selProdName)}` : '-'; let scoreVal = localStorage.getItem('accScore_' + a.name) || '-'; // ← 改成 a.name let st = statusMap[a.name]; // ← 已经是 a.name,保持 let status = statusInfo(st, isSel); let credential = a.cookie ? '已加载' : '缺失'; let lastMessage = resultMessage(st); return `
${escHtml(a.name)}
${isSel ? '已加入任务' : '未选择'}
${credential} ${prodTag} ${scoreVal} ${status.text} ${lastMessage} `; }); document.getElementById('accTable').innerHTML = h.join(''); updateDashboardStats(); } // toggleAll 根据表头复选框批量选择或取消所有账号,并持久化选择结果。 function toggleAll(){ let ck=document.getElementById('chkAll').checked; // 批量选择时的回调把每个账号下标加入选择集合。 if(ck) accounts.forEach((_,i)=>selAccs.add(i)); else selAccs.clear(); localStorage.setItem('selAccs',JSON.stringify([...selAccs])); renderAccounts(); updateDashboardStats(); } // togAcc 切换单个账号的选择状态,并同步更新 localStorage。 function togAcc(i){ if(selAccs.has(i)) selAccs.delete(i); else selAccs.add(i); localStorage.setItem('selAccs',JSON.stringify([...selAccs])); renderAccounts(); updateDashboardStats(); } // ==================== 立即抢购(不弹窗) ==================== // startNow 构建账号商品映射后,调用后端立即启动抢购。 async function startNow(){ let ap=buildAP(); if(!ap) return; resetRunResults(); setRunState('立即抢购已提交'); addLog(`立即抢购: ${Object.keys(ap).length}个账号 → ${selProdName}`,'i'); toast('抢购中',`${Object.keys(ap).length} 个账号正在抢购 ${selProdName}`,'info',3000); try { let d=await App.StartSeckill(ap); addLog(d.msg||'已提交','i'); } catch(e) { setRunState('提交失败'); addLog('提交失败: '+e.message,'e'); toast('提交失败',e.message,'error'); } } // ==================== 定时抢购 ==================== // startSchedule 构建定时参数并提交给后端,由后端按服务端时间安排抢购。 async function startSchedule(){ let ap=buildAP(); if(!ap) return; resetRunResults(); let timeStr=document.getElementById('targetTime').value.trim(); if(!timeStr){ toast('提示','请选择目标时间','warn'); return; } if(timeStr.split(':').length===2) timeStr+=':00'; let today=new Intl.DateTimeFormat('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}) .format(new Date()).replace(/\//g,'-'); let fullTime=today+' '+timeStr; let adv=parseInt(document.getElementById('advance').value)||300; setRunState('定时配置中'); addLog(`设置定时: ${fullTime}, 提前=${adv}ms`,'i'); try { let d=await App.SetSchedule({accountProducts:ap,targetTime:fullTime,advance:adv}); scheduleActive=true; // 保存定时信息 localStorage.setItem('targetTime',timeStr); localStorage.setItem('advance',adv); // 锁定:关闭自动,固定当前值 document.getElementById('btnSchedule').style.display='none'; document.getElementById('btnCancel').style.display='inline-block'; setRunState(`定时已开启,触发 ${d.triggerTime}`); addLog(`定时已开启: 触发 ${d.triggerTime},剩余 ${d.remainSecond} 秒`,'s'); toast('定时已开启',`触发时刻: ${d.triggerTime}
剩余时间: ${formatRemain(d.remainSecond)}
商品: ${selProdName}
账号: ${Object.keys(ap).length} 个`,'success',5000); startCountdown(d.remainSecond); } catch(e) { setRunState('定时设置失败'); addLog('设置失败: '+e.message,'e'); toast('定时失败',e.message,'error'); } } // cancelSchedule 调用后端取消尚未触发的定时任务,并恢复前端按钮状态。 async function cancelSchedule(){ try{ await App.CancelSchedule(); }catch(e){} scheduleActive=false; // 清除缓存的定时 localStorage.removeItem('targetTime'); localStorage.removeItem('advance'); // 恢复自动 document.getElementById('btnSchedule').style.display='inline-block'; document.getElementById('btnCancel').style.display='none'; stopCountdown(); setRunState('定时已停止'); addLog('定时已手动停止','w'); toast('定时已停止','抢购任务已取消','warn'); } // startCountdown 启动前端倒计时徽标,用后端返回的剩余秒数做展示。 function startCountdown(sec) { stopCountdown(); let badge = document.getElementById('cdBadge'); badge.style.display = 'inline-block'; let startTime = Date.now(); let endTime = startTime + sec * 1000; // 每 500ms 刷新一次显示,确保用户能看到接近触发时的状态变化。 countdownTimer = setInterval(() => { let now = Date.now(); let remain = Math.max(0, Math.ceil((endTime - now) / 1000)); if (remain <= 0) { badge.textContent = '触发中'; clearInterval(countdownTimer); countdownTimer = null; scheduleActive = false; document.getElementById('btnSchedule').style.display = 'inline-block'; document.getElementById('btnCancel').style.display = 'none'; setRunState('定时已触发'); // 延迟回调让“触发中”状态短暂停留,避免用户看不到最终状态。 setTimeout(() => badge.style.display = 'none', 5000); } else { badge.textContent = formatRemain(remain); } }, 500); } // stopCountdown 停止倒计时定时器,并隐藏倒计时徽标。 function stopCountdown(){ clearInterval(countdownTimer); document.getElementById('cdBadge').style.display='none'; } // formatRemain 把秒数格式化为 h:mm:ss,供定时提示和倒计时徽标使用。 function formatRemain(sec){ let h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60; return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; } // setRunState 更新顶部运行状态。 function setRunState(text) { let el = document.getElementById('runStateText'); if (el) el.textContent = text || '等待操作'; } // switchLogTab 切换右侧日志/结果/统计面板。 function switchLogTab(id) { document.querySelectorAll('.log-tab').forEach(btn => btn.classList.toggle('active', btn.dataset.logTab === id)); document.querySelectorAll('.log-panel').forEach(panel => panel.classList.toggle('active', panel.id === id)); } // updateSelectedProductCard 刷新左侧当前商品卡片。 function updateSelectedProductCard() { let card = document.getElementById('selectedProductCard'); let nameEl = document.getElementById('selectedProductName'); let metaEl = document.getElementById('selectedProductMeta'); if (!nameEl || !metaEl) return; let product = products.find(p => p.commodityId === selProdId); nameEl.textContent = selProdName || '未选'; if (card) card.classList.toggle('has-product', !!selProdId); if (!product) { metaEl.innerHTML = selProdId ? '等待刷新' : '未绑定任务'; return; } metaEl.innerHTML = `${product.score} 积分库存 ${product.count}`; } // statusInfo 将后端结果转换为账号状态标签。 function statusInfo(st, selected) { if (st === true) return {text:'成功', cls:'status-success'}; if (st === false) return {text:'失败', cls:'status-fail'}; if (st && typeof st === 'object') { if (st.ok) return {text:'成功', cls:'status-success'}; let msg = String(st.message || ''); if (msg.includes('库存已耗完')) return {text:'库存耗尽', cls:'status-warn'}; if (msg.includes('兑换上限')) return {text:'已达上限', cls:'status-warn'}; if (msg.includes('未绑定')) return {text:'未绑定', cls:'status-fail'}; if (msg.includes('精英手册')) return {text:'资格不足', cls:'status-fail'}; return {text:selected ? '未命中' : '失败', cls:selected ? 'status-warn' : 'status-fail'}; } return selected ? {text:'待命', cls:'status-ready'} : {text:'未选择', cls:'status-idle'}; } // resultMessage 返回账号最后一次结果摘要。 function resultMessage(st) { let msg = ''; if (st && typeof st === 'object') msg = st.message || ''; else if (st === true) msg = '成功'; else if (st === false) msg = '失败'; if (!msg) return '-'; let short = msg.length > 46 ? msg.slice(0, 46) + '...' : msg; return `${escHtml(short)}`; } // updateDashboardStats 刷新顶部统计指标。 function updateDashboardStats() { let selectedNames = new Set([...selAccs].map(i => accounts[i]?.name).filter(Boolean)); let success = 0, fail = 0; for (let name of selectedNames) { let st = statusMap[name]; if (st === true || (st && typeof st === 'object' && st.ok)) success++; else if (st === false || (st && typeof st === 'object' && st.message)) fail++; } let pending = Math.max(0, selectedNames.size - success - fail); setText('selectedCnt', selectedNames.size); setText('successCnt', success); setText('failCnt', fail); setText('pendingCnt', pending); let stats = document.getElementById('statsBody'); if (stats) { stats.innerHTML = `
成功账号: ${success}
异常账号: ${fail}
待处理账号: ${pending}
已选账号: ${selectedNames.size} / 总账号: ${accounts.length}
`; } } function setText(id, value) { let el = document.getElementById(id); if (el) el.textContent = value; } // resetRunResults 清理上一轮前端结果,避免新任务被旧状态干扰。 function resetRunResults() { lastResults = {}; statusMap = {}; localStorage.removeItem('statusMap'); let resultEl = document.getElementById('resultBody'); if (resultEl) resultEl.innerHTML = '
暂无抢购结果
'; renderAccounts(); updateDashboardStats(); } // ==================== 结果刷新 ==================== // checkResults 轮询后端抢购结果,更新日志区、账号状态和本地状态缓存。 async function checkResults() { try { let d = await App.GetResults(); if (JSON.stringify(d) !== JSON.stringify(lastResults) && Object.keys(d).length > 0) { lastResults = d; let newStatusMap = {}; let h = '
=== 最新结果 ===
'; for (let [k, v] of Object.entries(d)) { let cls = v.includes('成功') ? 'log-s' : 'log-e'; h += `
${k}: ${v}
`; let name = k.split('_')[0]; if (!(name in newStatusMap)) newStatusMap[name] = {ok:false,message:v}; if (v.includes('成功')) newStatusMap[name] = {ok:true,message:v}; } statusMap = newStatusMap; localStorage.setItem('statusMap', JSON.stringify(statusMap)); renderAccounts(); let resultEl = document.getElementById('resultBody'); resultEl.innerHTML = h; resultEl.scrollTop = resultEl.scrollHeight; updateDashboardStats(); } } catch (e) { } } // fetchServerLogs 增量拉取后端日志,把真实接口链路日志显示到前端日志区。 async function fetchServerLogs() { try { let d = await App.GetLogs(lastLogId); if (!d.success || !Array.isArray(d.logs) || d.logs.length === 0) return; d.logs.forEach(item => { lastLogId = Math.max(lastLogId, item.id || 0); let type = 'i'; if (item.level === 'success') type = 's'; else if (item.level === 'error') type = 'e'; else if (item.level === 'warn') type = 'w'; else if (item.level === 'debug') type = 'd'; addServerLog(item.message, type, item.category); }); } catch (e) { } } // addServerLog 追加一条后端日志,和本地操作日志共用同一个日志区域。 function addServerLog(msg, type = 'i', category = '') { let el = document.getElementById('logBody'); let cat = category ? `${escHtml(category)}` : ''; el.innerHTML += `
[后端] ${cat}${escHtml(cleanServerLogMessage(msg))}
`; el.scrollTop = el.scrollHeight; } // cleanServerLogMessage 去掉后端结构化日志前缀,分类由单独标签展示。 function cleanServerLogMessage(msg) { return String(msg ?? '').replace(/\[[^\]]+\]\[(info|success|warn|error)\]\s*/i, ''); } // ==================== 设置 ==================== // openSettings 打开设置弹窗,从后端读取当前配置填充表单。 async function openSettings() { try { let cfg = await App.GetSettings(); document.getElementById('setTraceEnabled').checked = !!cfg.traceEnabled; document.getElementById('setTraceMaxBody').value = cfg.traceMaxBody || 0; document.getElementById('setLogLevel').value = cfg.logLevel || 'info'; document.getElementById('setConcurrency').value = cfg.concurrency || 80; document.getElementById('setAttempts').value = cfg.attempts || 1; } catch(e) { toast('读取设置失败', e.message, 'error'); } document.getElementById('settingsModal').classList.add('show'); } // closeSettings 关闭设置弹窗。 function closeSettings() { document.getElementById('settingsModal').classList.remove('show'); } // saveSettings 收集表单数据,提交给后端保存。 async function saveSettings() { let cfg = { traceEnabled: document.getElementById('setTraceEnabled').checked, traceMaxBody: parseInt(document.getElementById('setTraceMaxBody').value) || 0, logLevel: document.getElementById('setLogLevel').value, concurrency: parseInt(document.getElementById('setConcurrency').value) || 80, attempts: parseInt(document.getElementById('setAttempts').value) || 1, }; try { let d = await App.SaveSettings(cfg); if (d.success) { toast('设置已保存', '配置已生效', 'success'); closeSettings(); } else { toast('保存失败', d.error || '', 'error'); } } catch(e) { toast('保存失败', e.message, 'error'); } } // buildAP 根据已勾选账号和账号绑定商品,生成后端需要的账号到商品 ID 映射。 function buildAP() { if (selAccs.size === 0) { toast('提示', '请勾选要抢购的账号', 'warn'); return null; } if (!selProdId) { toast('提示', '请先选择商品', 'warn'); return null; } if (!productInfo || !products.some(p => p.commodityId === selProdId)) { toast('提示', '请先刷新商品,确认库存是实时数据', 'warn'); return null; } let ap = {}; // 遍历已选账号,把当前选中商品应用到每个账号。 selAccs.forEach(i => { let account = accounts[i]; if (account) { ap[account.name] = selProdId; } }); if (Object.keys(ap).length === 0) { toast('提示', '没有有效账号可提交', 'warn'); return null; } return ap; } // clearLegacyProductBindings 删除旧版按账号保存商品的缓存,避免残留旧商品误导界面。 function clearLegacyProductBindings() { for (let key in localStorage) { if (key.startsWith('accProd_') || key.startsWith('accProdName_')) { localStorage.removeItem(key); } } } // addLog 向底部日志区追加一条带毫秒时间戳的运行日志。 function addLog(msg, type = 'i') { let el = document.getElementById('logBody'); let now = new Date(); let timeStr = now.toLocaleTimeString() + '.' + String(now.getMilliseconds()).padStart(3, '0'); el.innerHTML += `
[${timeStr}] ${msg}
`; el.scrollTop = el.scrollHeight; } // clearLog 清空前端日志显示,并重置最近一次结果快照。 function clearLog(){ document.getElementById('logBody').innerHTML='
[系统] 就绪,等待操作...
'; document.getElementById('resultBody').innerHTML='
暂无抢购结果
'; lastResults={}; setRunState('等待操作'); updateDashboardStats(); } // escHtml 转义 HTML 特殊字符,避免接口数据破坏页面结构。 function escHtml(s){ return String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } // jsAttr 转义写入 onclick 字符串参数的数据。 function jsAttr(s){ return escHtml(String(s ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,' ')); } init();