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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 账号作战表
- 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}
-}