536 lines
25 KiB
HTML
536 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>斗鱼抢购系统</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Microsoft YaHei',sans-serif;background:#0d1117;color:#c9d1d9;display:flex;height:100vh}
|
|
|
|
/* ========== 左侧商品列表 ========== */
|
|
.left{width:280px;min-width:280px;background:#161b22;border-right:1px solid#30363d;display:flex;flex-direction:column}
|
|
.left-header{padding:10px;border-bottom:1px solid#30363d}
|
|
.left-list{flex:1;overflow-y:auto;padding:8px}
|
|
.left-list .prod-item{display:flex;align-items:center;gap:8px;padding:8px;margin:4px 0;background:#21262d;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:.2s}
|
|
.left-list .prod-item:hover{border-color:#58a6ff}
|
|
.left-list .prod-item.active{border-color:#f78166;background:#1a1f2b}
|
|
.left-list .prod-item img{width:40px;height:40px;border-radius:4px;object-fit:cover;background:#0d1117}
|
|
.left-list .prod-item .pinfo{flex:1;min-width:0}
|
|
.left-list .prod-item .pname{font-size:13px;font-weight:bold;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.left-list .prod-item .pmeta{font-size:11px;color:#8b949e;display:flex;gap:8px}
|
|
.pmeta .pscore{color:#d2991d}
|
|
.pmeta .pstock{color:#7ee787}
|
|
.pmeta .pstock.low{color:#f78166}
|
|
|
|
/* ========== 右侧主区域 ========== */
|
|
.right{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative}
|
|
|
|
/* 工具栏 */
|
|
.toolbar{padding:8px 14px;background:#161b22;border-bottom:1px solid#30363d;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
.btn{padding:6px 12px;border:1px solid#30363d;border-radius:5px;background:#21262d;color:#c9d1d9;cursor:pointer;font-size:13px;white-space:nowrap;transition:.2s}
|
|
.btn:hover{background:#30363d}
|
|
.btn-red{background:#da3633;border-color:#da3633;color:#fff;font-size:14px;padding:8px 16px}
|
|
.btn-red:hover{background:#c62828}
|
|
.btn-green{background:#238636;border-color:#238636;color:#fff;font-size:14px;padding:8px 16px}
|
|
.btn-green:hover{background:#1b6b2a}
|
|
.btn-blue{background:#1f6feb;border-color:#1f6feb;color:#fff;font-size:14px;padding:8px 16px}
|
|
.btn-blue:hover{background:#1158c7}
|
|
.btn-cancel{background:#6e7681;border-color:#6e7681;color:#fff;font-size:14px;padding:8px 16px}
|
|
.btn-cancel:hover{background:#555}
|
|
|
|
/* 定时信息栏 */
|
|
.schedule-bar{padding:10px 16px;background:#161b22;border-bottom:1px solid#30363d;display:flex;gap:16px;align-items:center;flex-wrap:wrap;font-size:14px}
|
|
.schedule-bar label{color:#8b949e;font-size:14px;font-weight:bold;white-space:nowrap}
|
|
.schedule-bar .time-box{background:#0d1117;padding:6px 14px;border-radius:4px;font-family:'Consolas',monospace;font-size:16px;font-weight:bold}
|
|
.schedule-bar .time-box.server{color:#58a6ff}
|
|
.schedule-bar .time-box.offset{color:#f0883e}
|
|
.schedule-bar input{padding:6px 12px;background:#0d1117;color:#fff;border:2px solid#30363d;border-radius:4px;font-size:16px;font-weight:bold}
|
|
.schedule-bar input#targetTime{width:150px;text-align:center}
|
|
.schedule-bar input#advance{width:80px;text-align:center}
|
|
.schedule-bar .hint{color:#8b949e;font-size:12px}
|
|
.countdown-badge{background:#da3633;color:#fff;padding:6px 16px;border-radius:4px;font-weight:bold;font-size:16px}
|
|
|
|
/* 账号表格 */
|
|
.table-wrap{flex:1;overflow-y:auto;padding:8px 14px;min-height:150px}
|
|
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
th{background:#21262d;padding:8px;text-align:left;border-bottom:2px solid#30363d;position:sticky;top:0;z-index:1;color:#8b949e;font-size:13px}
|
|
td{padding:6px 8px;border-bottom:1px solid#21262d;vertical-align:middle}
|
|
tr:hover{background:#1a1f2b}
|
|
.cookie-td{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#6e7681;font-size:11px}
|
|
.prod-tag{display:inline-block;background:#238636;color:#fff;padding:3px 10px;border-radius:3px;font-size:12px}
|
|
.status-td{font-size:16px}
|
|
|
|
/* 日志区 */
|
|
.log-wrap{border-top:2px solid#30363d;background:#0d1117;display:flex;flex-direction:column;height:200px}
|
|
.log-header{padding:8px 16px;background:#161b22;border-bottom:1px solid#30363d;display:flex;justify-content:space-between;align-items:center;font-size:13px}
|
|
.log-body{flex:1;overflow-y:auto;padding:10px 16px;font-family:'Consolas','Courier New',monospace;font-size:13px;line-height:1.8}
|
|
.log-s{color:#7ee787}
|
|
.log-e{color:#f78166}
|
|
.log-i{color:#58a6ff}
|
|
.log-w{color:#f0883e}
|
|
.log-result{background:#1a1f2b;padding:2px 8px;margin:2px 0;border-radius:3px;word-break:break-all}
|
|
|
|
/* ========== 右侧滑入提示 ========== */
|
|
.toast-container{position:fixed;top:20px;right:-420px;z-index:9999;transition:right .4s ease}
|
|
.toast-container.show{right:20px}
|
|
.toast{background:#161b22;border:2px solid#30363d;border-radius:10px;padding:16px 20px;margin-bottom:10px;min-width:350px;box-shadow:0 8px 30px rgba(0,0,0,.5)}
|
|
.toast-title{font-size:16px;font-weight:bold;margin-bottom:6px}
|
|
.toast-body{font-size:13px;color:#8b949e;line-height:1.6}
|
|
.toast-success{border-color:#238636}.toast-success .toast-title{color:#7ee787}
|
|
.toast-info{border-color:#1f6feb}.toast-info .toast-title{color:#58a6ff}
|
|
.toast-warn{border-color:#d2991d}.toast-warn .toast-title{color:#d2991d}
|
|
.toast-error{border-color:#da3633}.toast-error .toast-title{color:#f78166}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- 右侧滑入提示容器 -->
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<!-- ==================== 左侧商品列表 ==================== -->
|
|
<div class="left">
|
|
<div class="left-header">
|
|
<button class="btn btn-blue" onclick="loadProducts()" style="width:100%">🔄 加载商品</button>
|
|
</div>
|
|
<div class="left-list" id="prodList">
|
|
<div style="text-align:center;color:#8b949e;padding:40px 10px;font-size:13px">点击"加载商品"获取列表</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== 右侧 ==================== -->
|
|
<div class="right">
|
|
<div class="toolbar">
|
|
<button class="btn btn-red" onclick="startNow()">⚡ 立即抢购</button>
|
|
<button class="btn btn-green" id="btnSchedule" onclick="startSchedule()">⏰ 开启定时</button>
|
|
<button class="btn btn-cancel" id="btnCancel" onclick="cancelSchedule()" style="display:none">⏹ 停止定时</button>
|
|
<span style="color:#8b949e;font-size:14px;margin-left:8px">账号:<b id="accCnt" style="color:#58a6ff">0</b></span>
|
|
<span style="color:#8b949e;font-size:14px"> 选中商品:<b id="selProd" style="color:#f78166"> 未选</b></span>
|
|
<button class="btn btn-blue" onclick="reloadAccounts()" style="font-size:12px;padding:4px 10px">🔄 加载账号</button>
|
|
<button class="btn btn-blue" onclick="queryAllScore()" style="font-size:12px;padding:4px 10px">💰 查询积分</button>
|
|
<span id="cdBadge" class="countdown-badge" style="display:none"></span>
|
|
</div>
|
|
|
|
<div class="schedule-bar">
|
|
<label>服务器时间:</label>
|
|
<span class="time-box server" id="svrTime">--</span>
|
|
<label>目标时间:</label>
|
|
<input type="time" id="targetTime" value="18:00:00" step="1">
|
|
<label>提前(ms):</label>
|
|
<input type="number" id="advance" value="300" min="0" max="5000" step="100" style="width:90px;text-align:center">
|
|
<span class="hint">(定时触发=目标-提前量)</span>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px"><input type="checkbox" id="chkAll" onchange="toggleAll()"></th>
|
|
<th>账号名</th>
|
|
<th>Cookie</th>
|
|
<th>商品</th>
|
|
<th>积分</th>
|
|
<th style="width:80px">状态</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="accTable"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="log-wrap">
|
|
<div class="log-header">
|
|
<span>📋 运行日志(自动刷新)</span>
|
|
<button class="btn" onclick="clearLog()" style="font-size:12px;padding:4px 10px">清空</button>
|
|
</div>
|
|
<div class="log-body" id="logBody">
|
|
<div class="log-i">[系统] 就绪,等待操作...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let accounts=[], products=[], selProdId=null, selProdName='', selAccs=new Set();
|
|
let scheduleActive=false, countdownTimer=null, lastResults={};
|
|
let statusMap={}; // ← 加这行
|
|
|
|
async function init(){
|
|
// 恢复缓存的选择
|
|
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'); // ← 提前读取
|
|
|
|
if(savedAccs) selAccs=new Set(JSON.parse(savedAccs));
|
|
if(savedProd){ selProdId=savedProd; selProdName=savedProdName||''; document.getElementById('selProd').textContent=selProdName; }
|
|
if(savedTime) document.getElementById('targetTime').value=savedTime;
|
|
if(savedAdv) document.getElementById('advance').value=savedAdv;
|
|
if(savedStatus) statusMap=JSON.parse(savedStatus); // ← 提前恢复
|
|
|
|
|
|
await loadAccounts();
|
|
|
|
await loadProducts(); // ← 加这行,启动自动加载商品
|
|
|
|
updateTime();
|
|
|
|
clearLog(); // ← 加这行,启动时清空日志区
|
|
|
|
|
|
//localStorage.removeItem('statusMap'); // ← 加这行
|
|
// statusMap={}; // ← 加这行 这两行删除就保存成功失败得状态
|
|
|
|
// 清空后端旧结果
|
|
await fetch('/api/clear-results', {method:'POST'});
|
|
|
|
setInterval(updateTime,1000);
|
|
setInterval(checkResults,2000);
|
|
}
|
|
|
|
function updateTime(){
|
|
fetch('/api/time').then(r=>r.json()).then(d=>{
|
|
document.getElementById('svrTime').textContent=d.serverTime||'--';
|
|
}).catch(()=>{});
|
|
}
|
|
|
|
//加载账号
|
|
async function reloadAccounts(){
|
|
let r=await fetch('/api/reload-accounts');
|
|
let d=await r.json();
|
|
await loadAccounts();
|
|
toast('✅ 账号已刷新',`共 ${d.count} 个账号`,'success');
|
|
}
|
|
|
|
//并发查询函数
|
|
async function queryAllScore() {
|
|
addLog('正在并发查询积分...', 'i');
|
|
let tasks = [];
|
|
for (let i = 0; i < accounts.length; i++) {
|
|
tasks.push((async (idx) => {
|
|
let a = accounts[idx];
|
|
try {
|
|
console.log('账号对象:', JSON.stringify(a));
|
|
console.log('Cookie长度:', a.cookie?.length, '前50字符:', a.cookie?.substring(0, 50));
|
|
|
|
let r = await fetch('/api/query-score', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
body: a.cookie
|
|
});
|
|
let d = await r.json();
|
|
let score = (d.score !== undefined && d.score !== null && d.score !== '') ? d.score : 'N/A';
|
|
// ✅ 用账号名存积分
|
|
localStorage.setItem('accScore_' + a.name, score); // ← 改成 a.name
|
|
let el = document.getElementById('score_' + idx);
|
|
if (el) el.textContent = score;
|
|
} catch (e) { }
|
|
})(i));
|
|
}
|
|
await Promise.all(tasks);
|
|
addLog('积分查询完成', 's');
|
|
toast('✅ 积分查询完成', '', 'success');
|
|
}
|
|
|
|
|
|
// ==================== 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=`<div class="toast-title">${title}</div><div class="toast-body">${body}</div>`;
|
|
container.appendChild(div);
|
|
// 触发滑入
|
|
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);
|
|
}
|
|
|
|
// ==================== 商品 ====================
|
|
async function loadProducts(){
|
|
addLog('正在从斗鱼加载商品...','i');
|
|
try{
|
|
await fetch('/api/load-products');
|
|
let r=await fetch('/api/products'); products=await r.json();
|
|
renderProducts();
|
|
addLog(`✅ 加载完成,共 ${products.length} 个商品`,'s');
|
|
toast('✅ 加载成功',`共加载 ${products.length} 个商品`,'success');
|
|
}catch(e){
|
|
addLog('加载失败,请检查网络','e');
|
|
toast('❌ 加载失败','请检查网络连接','error');
|
|
}
|
|
}
|
|
function renderProducts(){
|
|
let h='';
|
|
products.forEach(p=>{
|
|
h+=`<div class="prod-item${selProdId==p.commodityId?' active':''}" onclick="selProduct('${p.commodityId}','${escHtml(p.commodityName)}')">
|
|
<img src="${p.pic}" onerror="this.style.display='none'">
|
|
<div class="pinfo"><div class="pname">${p.commodityName}</div>
|
|
<div class="pmeta"><span class="pscore">💰${p.score}</span><span class="pstock${p.count<500?' low':''}">📦${p.count}</span></div></div>
|
|
</div>`;
|
|
});
|
|
document.getElementById('prodList').innerHTML=h||'<div style="text-align:center;color:#8b949e;padding:40px">暂无商品</div>';
|
|
}
|
|
|
|
function selProduct(id, name) {
|
|
selProdId = id;
|
|
selProdName = name;
|
|
document.getElementById('selProd').textContent = name;
|
|
// ✅ 用账号名存商品绑定
|
|
selAccs.forEach(i => {
|
|
let accountName = accounts[i].name; // 用账号名作为 key
|
|
localStorage.setItem('accProd_' + accountName, id);
|
|
localStorage.setItem('accProdName_' + accountName, name);
|
|
});
|
|
|
|
// selAccs.clear();
|
|
// localStorage.setItem('selAccs', JSON.stringify([...selAccs]));
|
|
renderProducts();
|
|
renderAccounts();
|
|
}
|
|
|
|
// ==================== 账号 ====================
|
|
// async function loadAccounts(){
|
|
// let r=await fetch('/api/accounts'); accounts=await r.json();
|
|
// renderAccounts();
|
|
// }
|
|
|
|
async function loadAccounts() {
|
|
let r = await fetch('/api/accounts');
|
|
accounts = await r.json();
|
|
|
|
let validNames = new Set(accounts.map(a => a.name));
|
|
|
|
for (let key in localStorage) {
|
|
// 只清理商品和积分缓存,保留 selAccs、statusMap、targetTime、advance
|
|
if (key.startsWith('accProd_') || key.startsWith('accProdName_') || key.startsWith('accScore_')) {
|
|
let name = key.substring(key.indexOf('_') + 1);
|
|
if (!validNames.has(name)) {
|
|
localStorage.removeItem(key);
|
|
console.log('清理无效缓存:', key);
|
|
}
|
|
}
|
|
}
|
|
renderAccounts();
|
|
}
|
|
|
|
|
|
function renderAccounts() {
|
|
document.getElementById('accCnt').textContent = accounts.length;
|
|
let h = accounts.map((a, i) => {
|
|
let isSel = selAccs.has(i);
|
|
// ✅ 用账号名读取商品、积分、状态
|
|
let prodName = localStorage.getItem('accProdName_' + a.name) || '-';
|
|
let prodTag = prodName !== '-' ? `<span class="prod-tag">${prodName}</span>` : '-';
|
|
let scoreVal = localStorage.getItem('accScore_' + a.name) || '-'; // ← 改成 a.name
|
|
let st = statusMap[a.name]; // ← 已经是 a.name,保持
|
|
let statusStr = '-';
|
|
if (st === true) statusStr = '✅';
|
|
else if (st === false) statusStr = '❌';
|
|
return `<tr>
|
|
<td>
|
|
<label style="display:flex;align-items:center;cursor:pointer" onclick="togAcc(${i})">
|
|
<div style="width:18px;height:18px;border-radius:4px;border:2px solid ${isSel ? '#238636' : '#30363d'};background:${isSel ? '#238636' : 'transparent'};display:flex;align-items:center;justify-content:center;transition:.2s">
|
|
${isSel ? '<span style="color:#fff;font-size:12px">✓</span>' : ''}
|
|
</div>
|
|
</label>
|
|
</td>
|
|
<td style="font-weight:bold">${a.name}</td>
|
|
<td class="cookie-td" title="${a.cookie}">${a.cookie}</td>
|
|
<td>${prodTag}</td>
|
|
<td id="score_${i}">${scoreVal}</td>
|
|
<td class="status-td" id="st_${i}">${statusStr}</td>
|
|
</tr>`;
|
|
});
|
|
document.getElementById('accTable').innerHTML = h.join('');
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
function togAcc(i){
|
|
if(selAccs.has(i)) selAccs.delete(i); else selAccs.add(i);
|
|
localStorage.setItem('selAccs',JSON.stringify([...selAccs]));
|
|
renderAccounts();
|
|
}
|
|
|
|
// ==================== 立即抢购(不弹窗) ====================
|
|
async function startNow(){
|
|
let ap=buildAP();
|
|
if(!ap) return;
|
|
addLog(`⚡ 立即抢购: ${Object.keys(ap).length}个账号 → ${selProdName}`,'i');
|
|
toast('⚡ 抢购中...',`${Object.keys(ap).length} 个账号正在抢购 ${selProdName}`,'info',3000);
|
|
let r=await fetch('/api/seckill',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({accountProducts:ap})});
|
|
let d=await r.json();
|
|
addLog(d.msg||'已提交','i');
|
|
}
|
|
|
|
// ==================== 定时抢购 ====================
|
|
async function startSchedule(){
|
|
let ap=buildAP();
|
|
if(!ap) return;
|
|
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;
|
|
|
|
addLog(`⏰ 设置定时: ${fullTime}, 提前=${adv}ms`,'i');
|
|
let r=await fetch('/api/schedule',{method:'POST',headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({accountProducts:ap,targetTime:fullTime,advance:adv})});
|
|
let d=await r.json();
|
|
if(d.success){
|
|
scheduleActive=true;
|
|
// 保存定时信息
|
|
localStorage.setItem('targetTime',timeStr);
|
|
localStorage.setItem('advance',adv);
|
|
|
|
// 锁定:关闭自动,固定当前值
|
|
document.getElementById('btnSchedule').style.display='none';
|
|
document.getElementById('btnCancel').style.display='inline-block';
|
|
addLog(`✅ 定时已开启! 触发: ${d.triggerTime} (剩余${d.remainSecond}秒)`,'s');
|
|
toast('⏰ 定时已开启',`触发时刻: ${d.triggerTime}<br>剩余时间: <b>${formatRemain(d.remainSecond)}</b><br>商品: ${selProdName}<br>账号: ${Object.keys(ap).length} 个`,'success',5000);
|
|
startCountdown(d.remainSecond);
|
|
} else {
|
|
addLog('❌ 设置失败: '+(d.error||'未知'),'e');
|
|
toast('❌ 定时失败',d.error||'未知错误','error');
|
|
}
|
|
}
|
|
async function cancelSchedule(){
|
|
scheduleActive=false;
|
|
// 清除缓存的定时
|
|
localStorage.removeItem('targetTime');
|
|
localStorage.removeItem('advance');
|
|
|
|
// 恢复自动
|
|
document.getElementById('btnSchedule').style.display='inline-block';
|
|
document.getElementById('btnCancel').style.display='none';
|
|
stopCountdown();
|
|
addLog('⚠️ 定时已手动停止','w');
|
|
toast('⏹ 定时已停止','抢购任务已取消','warn');
|
|
}
|
|
|
|
function startCountdown(sec) {
|
|
stopCountdown();
|
|
let badge = document.getElementById('cdBadge');
|
|
badge.style.display = 'inline-block';
|
|
let startTime = Date.now();
|
|
let endTime = startTime + sec * 1000;
|
|
|
|
countdownTimer = setInterval(() => {
|
|
let now = Date.now();
|
|
let remain = Math.max(0, Math.ceil((endTime - now) / 1000));
|
|
if (remain <= 0) {
|
|
badge.textContent = '🚀 触发中...';
|
|
stopCountdown();
|
|
scheduleActive = false;
|
|
document.getElementById('btnSchedule').style.display = 'inline-block';
|
|
document.getElementById('btnCancel').style.display = 'none';
|
|
setTimeout(() => badge.style.display = 'none', 5000);
|
|
} else {
|
|
badge.textContent = `⏱ ${formatRemain(remain)}`;
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
|
|
function stopCountdown(){
|
|
clearInterval(countdownTimer);
|
|
document.getElementById('cdBadge').style.display='none';
|
|
}
|
|
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')}`;
|
|
}
|
|
|
|
// ==================== 结果刷新 ====================
|
|
async function checkResults() {
|
|
try {
|
|
let r = await fetch('/api/results');
|
|
let d = await r.json();
|
|
if (JSON.stringify(d) !== JSON.stringify(lastResults) && Object.keys(d).length > 0) {
|
|
lastResults = d;
|
|
let newStatusMap = {};
|
|
let h = '<div class="log-i">=== 最新结果 ===</div>';
|
|
for (let [k, v] of Object.entries(d)) {
|
|
let cls = v.includes('成功') ? 'log-s' : 'log-e';
|
|
h += `<div class="log-result ${cls}"><b>${k}</b>: ${v}</div>`;
|
|
let name = k.split('_')[0];
|
|
// ✅ 直接用账号名作为 key,不再用 findIndex
|
|
if (!(name in newStatusMap)) newStatusMap[name] = false;
|
|
if (v.includes('成功')) newStatusMap[name] = true;
|
|
}
|
|
statusMap = newStatusMap;
|
|
localStorage.setItem('statusMap', JSON.stringify(statusMap));
|
|
renderAccounts();
|
|
let el = document.getElementById('logBody');
|
|
el.innerHTML += h;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
|
|
|
|
|
|
function buildAP() {
|
|
if (selAccs.size === 0) {
|
|
toast('⚠️ 提示', '请勾选要抢购的账号', 'warn');
|
|
return null;
|
|
}
|
|
let ap = {};
|
|
selAccs.forEach(i => {
|
|
let accountName = accounts[i].name; // ← 先取账号名
|
|
let cid = localStorage.getItem('accProd_' + accountName); // ← 用账号名读
|
|
if (cid) {
|
|
ap[accountName] = cid;
|
|
}
|
|
});
|
|
if (Object.keys(ap).length === 0) {
|
|
toast('⚠️ 提示', '勾选的账号未绑定商品', 'warn');
|
|
return null;
|
|
}
|
|
return ap;
|
|
}
|
|
|
|
|
|
function hasAnyAccProd(){
|
|
for(let i=0; i<accounts.length; i++){
|
|
if (localStorage.getItem('accProd_' + accounts[i].name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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 += `<div class="log-${type}">[${timeStr}] ${msg}</div>`;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
|
|
function clearLog(){
|
|
document.getElementById('logBody').innerHTML='<div class="log-i">[系统] 就绪,等待操作...</div>';
|
|
lastResults={};
|
|
}
|
|
|
|
function escHtml(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |