From 88a386b58827ab319ec93c5a2698d2cb9f5e1237 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 26 Jun 2026 09:38:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app.js | 722 +++++++++++++++++++++++++++++ frontend/index.html | 1051 ++++++------------------------------------- frontend/styles.css | 801 +++++++++++++++++++++++++++++++++ 3 files changed, 1664 insertions(+), 910 deletions(-) create mode 100644 frontend/app.js create mode 100644 frontend/styles.css diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..9d88824 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,722 @@ +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(); diff --git a/frontend/index.html b/frontend/index.html index 82a933b..f6d8121 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,245 +4,22 @@ 斗鱼抢购系统 - + -
-