diff --git a/frontend_backup/app.js b/frontend_backup/app.js deleted file mode 100644 index 9d88824..0000000 --- a/frontend_backup/app.js +++ /dev/null @@ -1,722 +0,0 @@ -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_backup/index.html b/frontend_backup/index.html deleted file mode 100644 index f6d8121..0000000 --- a/frontend_backup/index.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - 斗鱼抢购系统 - - - - -
- - - -
- - -
-
-
-
-
抢购控制台
-
等待操作
-
- -
-
- - -
-
- -
-
- 服务器 - -- -
-
- 账号 - 0/0 -
-
- 成功 - 0 -
-
- 异常 - 0 -
-
- 待处理 - 0 -
-
- -
-
- - - - - -
-
-
- - -
-
- - - ms -
-
-
- -
-
-
-
- 账号作战表 - 0 个账号 -
- -
-
- - - - - - - - - - - - - -
账号凭据商品积分状态最后结果
-
-
- - -
-
-
- - - - diff --git a/frontend_backup/styles.css b/frontend_backup/styles.css deleted file mode 100644 index de2a70b..0000000 --- a/frontend_backup/styles.css +++ /dev/null @@ -1,801 +0,0 @@ -*{margin:0;padding:0;box-sizing:border-box} -:root{ - color-scheme:dark; - --bg:#0f1419; - --surface:#161b22; - --surface-2:#1c232c; - --surface-3:#232b36; - --field:#0d1117; - --text:#c9d1d9; - --text-strong:#f0f6fc; - --muted:#8b949e; - --muted-2:#6e7681; - --line:#21262d; - --line-strong:#30363d; - --accent:#388bfd; - --accent-soft:rgba(56,139,253,.12); - --accent-border:rgba(56,139,253,.35); - --danger:#f85149; - --danger-soft:rgba(248,81,73,.12); - --success:#3fb950; - --success-soft:rgba(63,185,80,.12); - --warning:#d29922; - --warning-soft:rgba(210,153,34,.12); - --orange:#f78166; - --orange-soft:rgba(247,129,102,.12); - --shadow:0 1px 0 rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.18); - --radius:8px; - --radius-sm:6px; - --focus:0 0 0 3px rgba(56,139,253,.3); -} -body[data-theme="light"]{ - color-scheme:light; - --bg:#f6f8fa; - --surface:#ffffff; - --surface-2:#f6f8fa; - --surface-3:#eaeef2; - --field:#ffffff; - --text:#1f2328; - --text-strong:#0d1117; - --muted:#656d76; - --muted-2:#8c959f; - --line:#d0d7de; - --line-strong:#afb8c1; - --accent:#0969da; - --accent-soft:rgba(9,105,218,.08); - --accent-border:rgba(9,105,218,.3); - --danger:#cf222e; - --danger-soft:rgba(207,34,46,.08); - --success:#1a7f37; - --success-soft:rgba(26,127,55,.08); - --warning:#9a6700; - --warning-soft:rgba(154,103,0,.1); - --orange:#bc4c00; - --orange-soft:rgba(188,76,0,.08); - --shadow:0 1px 0 rgba(31,35,40,.04),0 8px 24px rgba(140,149,159,.15); - --focus:0 0 0 3px rgba(9,105,218,.3); -} - -html,body{height:100vh;overflow:hidden} -body{ - font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; - background:var(--bg); - color:var(--text); - font-size:13px; - line-height:1.5; - -webkit-font-smoothing:antialiased; - -moz-osx-font-smoothing:grayscale; -} -button,input,select{font:inherit;color:inherit} -::selection{background:var(--accent-soft);color:var(--text-strong)} -::-webkit-scrollbar{width:8px;height:8px} -::-webkit-scrollbar-track{background:transparent} -::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:999px} -::-webkit-scrollbar-thumb:hover{background:var(--muted-2)} - -/* ============ 主布局 ============ */ -.app-shell{ - display:grid; - grid-template-columns:300px minmax(0,1fr); - height:100vh; - min-width:1080px; - overflow:hidden; -} -.left{ - background:var(--surface); - border-right:1px solid var(--line); - display:flex; - flex-direction:column; - min-width:0; - min-height:0; - overflow:hidden; -} -.right{ - display:flex; - flex-direction:column; - min-width:0; - min-height:0; - overflow:hidden; -} - -/* ============ 品牌区 ============ */ -.brand{ - display:flex; - align-items:center; - justify-content:space-between; - gap:12px; - padding:16px 18px; - border-bottom:1px solid var(--line); -} -.brand h1{ - font-size:15px; - font-weight:600; - color:var(--text-strong); - letter-spacing:-.01em; -} -.brand-sub{ - font-size:11px; - color:var(--muted); - margin-top:3px; - letter-spacing:.02em; -} - -/* ============ 商品侧栏 ============ */ -.left-header{ - padding:14px 16px; - border-bottom:1px solid var(--line); - display:flex; - flex-direction:column; - gap:10px; -} -.product-actions{display:flex;gap:6px;align-items:center} -.product-actions .btn-primary{flex:1} -.product-search{ - display:flex; - align-items:center; - gap:8px; - background:var(--field); - border:1px solid var(--line); - border-radius:var(--radius-sm); - padding:0 10px; - transition:.15s; -} -.product-search:focus-within{border-color:var(--accent);box-shadow:var(--focus)} -.product-search input{ - flex:1; - min-width:0; - height:32px; - border:0; - outline:0; - background:transparent; - font-size:13px; -} -.product-search input::placeholder{color:var(--muted-2)} -.search-icon{color:var(--muted-2);font-size:13px} -.product-search .search-clear{ - width:20px;height:20px; - border:0;border-radius:4px; - background:transparent;color:var(--muted); - cursor:pointer;font-size:14px; - display:none; - align-items:center;justify-content:center; -} -.product-search .search-clear:hover{background:var(--surface-3);color:var(--text-strong)} - -.selected-product-card{ - padding:12px; - border:1px solid var(--line); - border-radius:var(--radius); - background:var(--surface-2); -} -.selected-product-card.has-product{ - border-color:var(--accent-border); - background:var(--accent-soft); -} -.selected-product-label{ - font-size:10px; - color:var(--muted); - font-weight:600; - text-transform:uppercase; - letter-spacing:.08em; - margin-bottom:6px; -} -.selected-product-name{ - font-size:13px; - color:var(--text-strong); - font-weight:600; - line-height:1.4; - word-break:break-all; -} -.selected-product-meta{ - display:flex; - gap:6px; - flex-wrap:wrap; - margin-top:8px; -} - -.product-meta{ - font-size:11px; - color:var(--muted); - padding:0 2px; -} -.product-meta.good{color:var(--success)} -.product-meta.warn{color:var(--warning)} - -.left-list{ - flex:1; - overflow-y:auto; - padding:6px 8px 12px; -} -.prod-item{ - display:grid; - grid-template-columns:40px minmax(0,1fr); - gap:10px; - padding:8px 10px; - margin:2px 0; - border-radius:var(--radius-sm); - cursor:pointer; - border:1px solid transparent; - transition:.12s; - position:relative; -} -.prod-item:hover{background:var(--surface-2)} -.prod-item.active{ - background:var(--accent-soft); - border-color:var(--accent-border); -} -.prod-item img{ - width:40px;height:40px; - border-radius:var(--radius-sm); - object-fit:cover; - background:var(--field); - border:1px solid var(--line); -} -.pinfo{min-width:0;display:flex;flex-direction:column;justify-content:center;gap:4px} -.pname{ - font-size:12.5px; - font-weight:500; - color:var(--text-strong); - white-space:nowrap; - overflow:hidden; - text-overflow:ellipsis; - line-height:1.3; -} -.pmeta{ - font-size:11px; - color:var(--muted); - display:flex; - gap:6px; - flex-wrap:wrap; -} -.chip{ - display:inline-flex; - align-items:center; - gap:3px; - padding:1px 7px; - border-radius:999px; - background:var(--surface-3); - font-size:11px; - font-weight:500; - white-space:nowrap; - color:var(--muted); -} -.chip-score{color:var(--warning);background:var(--warning-soft)} -.chip-stock{color:var(--success);background:var(--success-soft)} -.chip-stock.low{color:var(--danger);background:var(--danger-soft)} -.empty-state{ - text-align:center; - color:var(--muted); - padding:48px 16px; - font-size:12px; - line-height:1.7; -} - -/* ============ 顶部控制台 ============ */ -.console-head{ - padding:14px 20px; - border-bottom:1px solid var(--line); - background:var(--surface); - display:flex; - align-items:center; - justify-content:space-between; - gap:16px; -} -.headline{display:flex;align-items:center;gap:14px;min-width:0} -.headline-title{ - font-size:15px; - font-weight:600; - color:var(--text-strong); - letter-spacing:-.01em; -} -.headline-meta{ - font-size:12px; - color:var(--muted); - margin-top:2px; -} -.top-actions{display:flex;align-items:center;gap:6px} -.countdown-badge{ - display:inline-flex; - align-items:center; - padding:4px 10px; - border-radius:999px; - background:var(--danger-soft); - color:var(--danger); - border:1px solid var(--danger); - font-weight:600; - font-size:12px; - font-variant-numeric:tabular-nums; -} - -/* ============ 状态指标条 ============ */ -.status-grid{ - display:flex; - gap:24px; - padding:12px 20px; - background:var(--surface); - border-bottom:1px solid var(--line); - align-items:center; -} -.metric{ - display:flex; - align-items:baseline; - gap:6px; - min-width:0; -} -.metric-label{ - font-size:11px; - color:var(--muted); - text-transform:uppercase; - letter-spacing:.06em; - font-weight:500; -} -.metric-value{ - color:var(--text-strong); - font-weight:600; - font-size:14px; - font-variant-numeric:tabular-nums; -} -.metric-value.server{color:var(--accent)} -.metric-value.success-num{color:var(--success)} -.metric-value.fail-num{color:var(--danger)} - -/* ============ 操作条 ============ */ -.control-strip{ - padding:12px 20px; - background:var(--surface); - border-bottom:1px solid var(--line); - display:flex; - align-items:center; - justify-content:space-between; - gap:16px; - flex-wrap:wrap; -} -.action-group{display:flex;gap:6px;align-items:center;flex-wrap:wrap} -.schedule-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap} -.schedule-item{ - display:flex; - align-items:center; - gap:6px; -} -.schedule-bar label{ - color:var(--muted); - font-size:12px; - white-space:nowrap; -} -.schedule-bar input{ - height:30px; - padding:0 10px; - background:var(--field); - color:var(--text-strong); - border:1px solid var(--line); - border-radius:var(--radius-sm); - font-size:12.5px; - font-variant-numeric:tabular-nums; - transition:.15s; -} -.schedule-bar input:focus{outline:0;border-color:var(--accent);box-shadow:var(--focus)} -.schedule-bar input#targetTime{width:108px;text-align:center} -.schedule-bar input#advance{width:72px;text-align:center} - -/* ============ 按钮 ============ */ -.btn{ - height:30px; - padding:0 12px; - border:1px solid var(--line); - border-radius:var(--radius-sm); - background:var(--surface-2); - color:var(--text); - cursor:pointer; - font-size:12.5px; - font-weight:500; - white-space:nowrap; - transition:.12s; - display:inline-flex; - align-items:center; - justify-content:center; - gap:6px; -} -.btn:hover{background:var(--surface-3);border-color:var(--line-strong)} -.btn:focus-visible{outline:0;box-shadow:var(--focus)} -.btn:disabled{opacity:.55;cursor:not-allowed} -.btn-primary{ - background:var(--accent); - border-color:var(--accent); - color:#fff; -} -.btn-primary:hover{filter:brightness(1.08);background:var(--accent)} -.btn-danger{ - background:var(--danger); - border-color:var(--danger); - color:#fff; -} -.btn-danger:hover{filter:brightness(1.08);background:var(--danger)} -.btn-success{ - background:var(--success); - border-color:var(--success); - color:#fff; -} -.btn-success:hover{filter:brightness(1.08);background:var(--success)} -.btn-cancel{ - background:transparent; - color:var(--danger); - border-color:var(--danger); -} -.btn-cancel:hover{background:var(--danger-soft)} -.btn-ghost{background:transparent;border-color:transparent;color:var(--muted)} -.btn-ghost:hover{color:var(--text-strong);background:var(--surface-2);border-color:var(--line)} -.icon-btn{width:30px;padding:0;font-size:14px} -.btn-compact{height:26px;font-size:11.5px;padding:0 10px} - -.stat-pill{ - display:inline-flex; - align-items:center; - gap:6px; - height:24px; - padding:0 10px; - border:1px solid var(--line); - border-radius:999px; - background:var(--surface-2); - color:var(--muted); - font-size:11.5px; - font-variant-numeric:tabular-nums; -} - -/* ============ 工作区 ============ */ -.workspace{ - display:grid; - grid-template-columns:minmax(0,1fr) 380px; - gap:14px; - padding:14px 20px 20px; - align-items:stretch; - flex:1; - min-height:0; - overflow:hidden; -} -.panel{ - background:var(--surface); - border:1px solid var(--line); - border-radius:var(--radius); - min-width:0; - min-height:0; - display:flex; - flex-direction:column; - overflow:hidden; - height:100%; -} -.panel-head{ - padding:10px 14px; - border-bottom:1px solid var(--line); - display:flex; - align-items:center; - justify-content:space-between; - gap:10px; - background:var(--surface); -} -.panel-title{ - font-size:13px; - font-weight:600; - color:var(--text-strong); -} -.panel-sub{ - font-size:11.5px; - color:var(--muted); - margin-left:8px; - font-weight:400; -} -.panel-head label.stat-pill{cursor:pointer} -.panel-head label.stat-pill input{margin:0;cursor:pointer} - -/* ============ 账号表 ============ */ -.table-wrap{flex:1;overflow:auto;min-height:0} -table{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px} -th{ - background:var(--surface-2); - padding:8px 12px; - text-align:left; - border-bottom:1px solid var(--line); - position:sticky; - top:0; - z-index:1; - color:var(--muted); - font-size:11px; - font-weight:600; - text-transform:uppercase; - letter-spacing:.04em; - white-space:nowrap; -} -td{padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:middle} -tbody tr:last-child td{border-bottom:0} -tbody tr{transition:.1s} -tbody tr:hover{background:var(--surface-2)} -.select-cell{width:36px} -.status-col{width:96px} -.account-name{font-weight:500;color:var(--text-strong)} -.account-sub{font-size:11px;color:var(--muted);margin-top:2px} -.account-check{display:inline-flex;align-items:center;cursor:pointer} -.check-box{ - width:16px;height:16px; - border-radius:4px; - border:1.5px solid var(--line-strong); - background:transparent; - display:flex;align-items:center;justify-content:center; - transition:.15s; - color:#fff; - font-size:10px; - font-weight:700; -} -.check-box.checked{ - border-color:var(--accent); - background:var(--accent); -} -.prod-tag{ - display:inline-flex; - max-width:200px; - overflow:hidden; - text-overflow:ellipsis; - white-space:nowrap; - background:var(--surface-2); - color:var(--text); - border:1px solid var(--line); - padding:2px 8px; - border-radius:999px; - font-size:11.5px; -} -.status-badge{ - display:inline-flex; - align-items:center; - padding:2px 8px; - border-radius:999px; - border:1px solid var(--line); - background:var(--surface-2); - font-size:11px; - font-weight:500; - white-space:nowrap; -} -.status-idle{color:var(--muted)} -.status-ready{color:var(--accent);background:var(--accent-soft);border-color:var(--accent-border)} -.status-success{color:var(--success);background:var(--success-soft);border-color:var(--success)} -.status-fail{color:var(--danger);background:var(--danger-soft);border-color:var(--danger)} -.status-warn{color:var(--warning);background:var(--warning-soft);border-color:var(--warning)} -.cred-ok{color:var(--success)} -.cred-miss{color:var(--danger)} - -/* ============ 日志区 ============ */ -.side-stack{ - display:flex; - flex-direction:column; - gap:14px; - min-height:0; - height:100%; -} -.log-wrap{flex:1;min-height:0} -.log-header{ - padding:8px 12px; - border-bottom:1px solid var(--line); - display:flex; - justify-content:space-between; - align-items:center; - gap:8px; - background:var(--surface); -} -.log-tabs{display:flex;gap:2px;align-items:center} -.log-tab{ - height:26px; - padding:0 10px; - border-radius:var(--radius-sm); - border:0; - background:transparent; - color:var(--muted); - cursor:pointer; - font-size:11.5px; - font-weight:500; - transition:.12s; -} -.log-tab:hover{color:var(--text-strong);background:var(--surface-2)} -.log-tab.active{ - background:var(--accent-soft); - color:var(--accent); -} -.log-panel{ - display:none; - flex:1; - min-height:0; - overflow:auto; - padding:10px 12px; - font-family:'SF Mono','Monaco','Consolas','Courier New',monospace; - font-size:11.5px; - line-height:1.7; - background:var(--field); -} -.log-panel.active{display:block} -.log-s{color:var(--success)} -.log-e{color:var(--danger)} -.log-i{color:var(--accent)} -.log-w{color:var(--warning)} -.log-d{color:var(--muted-2)} -.log-result{ - background:var(--surface); - padding:6px 9px; - margin:4px 0; - border-radius:var(--radius-sm); - word-break:break-all; - border:1px solid var(--line); -} -.log-cat{ - display:inline-block; - min-width:32px; - text-align:center; - margin-right:6px; - padding:0 5px; - border:1px solid var(--line); - border-radius:3px; - color:var(--muted); - font-size:10px; - line-height:1.5; - background:var(--surface); -} -.result-empty{color:var(--muted);padding:14px 4px;font-style:italic} - -/* ============ Toast ============ */ -.toast-container{ - position:fixed; - top:20px; - right:-420px; - z-index:9999; - transition:right .35s cubic-bezier(.16,1,.3,1); -} -.toast-container.show{right:20px} -.toast{ - background:var(--surface); - border:1px solid var(--line); - border-left-width:3px; - border-radius:var(--radius); - padding:12px 14px; - margin-bottom:8px; - min-width:300px; - max-width:380px; - box-shadow:var(--shadow); -} -.toast-title{ - font-size:12.5px; - font-weight:600; - margin-bottom:3px; - color:var(--text-strong); -} -.toast-body{ - font-size:12px; - color:var(--muted); - line-height:1.6; -} -.toast-success{border-left-color:var(--success)} -.toast-success .toast-title{color:var(--success)} -.toast-info{border-left-color:var(--accent)} -.toast-info .toast-title{color:var(--accent)} -.toast-warn{border-left-color:var(--warning)} -.toast-warn .toast-title{color:var(--warning)} -.toast-error{border-left-color:var(--danger)} -.toast-error .toast-title{color:var(--danger)} - -/* ============ Modal ============ */ -.modal-overlay{ - position:fixed; - inset:0; - background:rgba(0,0,0,.5); - z-index:9000; - display:none; - align-items:center; - justify-content:center; - backdrop-filter:blur(4px); -} -.modal-overlay.show{display:flex} -.modal{ - background:var(--surface); - border:1px solid var(--line); - border-radius:var(--radius); - width:480px; - max-width:92vw; - max-height:85vh; - overflow-y:auto; - box-shadow:var(--shadow); -} -.modal-header{ - display:flex; - align-items:center; - justify-content:space-between; - padding:14px 18px; - border-bottom:1px solid var(--line); -} -.modal-header h3{ - font-size:14px; - font-weight:600; - color:var(--text-strong); -} -.modal-close{ - width:28px;height:28px; - border:0;border-radius:var(--radius-sm); - background:transparent;color:var(--muted); - cursor:pointer;font-size:18px; - display:flex;align-items:center;justify-content:center; -} -.modal-close:hover{background:var(--surface-2);color:var(--text-strong)} -.modal-body{padding:18px} -.form-group{margin-bottom:14px} -.form-group:last-child{margin-bottom:0} -.form-label{ - display:block; - font-size:12px; - font-weight:500; - color:var(--text); - margin-bottom:6px; -} -.form-hint{ - font-size:11px; - color:var(--muted); - margin-top:4px; - line-height:1.5; -} -.form-row{display:flex;gap:12px} -.form-row .form-group{flex:1} -.form-input,.form-select{ - width:100%; - height:32px; - padding:0 10px; - background:var(--field); - color:var(--text-strong); - border:1px solid var(--line); - border-radius:var(--radius-sm); - font-size:12.5px; - transition:.15s; -} -.form-input:focus,.form-select:focus{outline:0;border-color:var(--accent);box-shadow:var(--focus)} -.toggle-row{ - display:flex; - align-items:center; - justify-content:space-between; - padding:6px 0; -} -.toggle-label{font-size:12.5px;color:var(--text-strong);font-weight:500} -.toggle{position:relative;width:36px;height:20px;cursor:pointer} -.toggle input{opacity:0;width:0;height:0} -.toggle-slider{ - position:absolute;inset:0; - background:var(--line-strong); - border-radius:999px; - transition:.2s; -} -.toggle-slider::before{ - content:""; - position:absolute; - left:2px;top:2px; - width:16px;height:16px; - background:#fff; - border-radius:50%; - transition:.2s; -} -.toggle input:checked+.toggle-slider{background:var(--accent)} -.toggle input:checked+.toggle-slider::before{transform:translateX(16px)} -.modal-footer{ - padding:12px 18px; - border-top:1px solid var(--line); - display:flex; - justify-content:flex-end; - gap:8px; -} - -/* ============ 响应式 ============ */ -@media (max-width: 1180px){ - html,body{height:auto;overflow:auto} - .app-shell{display:block;min-width:0;height:auto;overflow:visible} - .left{height:auto;overflow:visible;border-right:0;border-bottom:1px solid var(--line)} - .left-list{max-height:280px} - .right{overflow:visible;min-height:auto} - .status-grid{flex-wrap:wrap;gap:14px 24px} - .control-strip{flex-direction:column;align-items:stretch} - .schedule-bar{justify-content:flex-end} - .workspace{grid-template-columns:1fr;padding:14px;overflow:visible} - .table-wrap{max-height:520px} - .log-wrap{height:360px} - .toast-container{left:12px;right:12px;top:12px} - .toast-container.show{right:12px} - .toast{min-width:0;width:100%;max-width:none} -}