const Dashboard = (() => { function metricsCard(title, value, badgeClass) { return `
${title}
${value}
`; } // 清理函数 const cleanup = () => { if(window.__auditTimer){ clearInterval(window.__auditTimer); window.__auditTimer = null; } // 取消未完成的请求 if(window.__auditAbortController){ window.__auditAbortController.abort(); window.__auditAbortController = null; } // 清理Canvas事件监听器 const canvas = document.getElementById('trend-chart'); if(canvas){ canvas.onmousemove = null; canvas.onmouseleave = null; } // 清理日期选择器事件 const pddDateEl = document.getElementById('audit-date-pdd'); const ytDateEl = document.getElementById('audit-date-yt'); if(pddDateEl) pddDateEl.onchange = null; if(ytDateEl) ytDateEl.onchange = null; // 清理全局变量 window.__auditBusy = false; window.__pddParams = null; window.__ytParams = null; }; Router.onBeforeEach((path)=>{ if(path !== '/dashboard'){ cleanup(); } }); async function render() { const [dRes,pRes,yRes] = await Promise.allSettled([ API.dashboard(), API.auditPddQuiet(), API.auditYtQuiet() ]); const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' }; const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] }; const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] }; const pddList = (pdd.list||[]).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const ytList = (yt.list||[]).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; setTimeout(()=>{ // 清理旧的定时器和事件 if(window.__auditTimer){ clearInterval(window.__auditTimer); window.__auditTimer = null; } // 清理旧的事件监听器 const oldCanvas = document.getElementById('trend-chart'); if(oldCanvas){ oldCanvas.onmousemove = null; oldCanvas.onmouseleave = null; } window.__pddParams = window.__pddParams || {}; window.__ytParams = window.__ytParams || {}; window.__auditBusy=false; const toEpoch=(s)=>{try{if(!s)return null;let t=s.trim(); if(/Z$/.test(t) || /[\+\-]\d{2}:\d{2}$/.test(t)) return Date.parse(t); t=t.replace(/\//g,'-'); if(t.includes('T')){ if(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(t)) t = t + ':00'; t = t + (function(){const offMin=-new Date().getTimezoneOffset();const sign=offMin>=0?'+':'-';const hh=String(Math.floor(Math.abs(offMin)/60)).padStart(2,'0');const mm=String(Math.abs(offMin)%60).padStart(2,'0');return sign+hh+':'+mm;})() }else{ if(/^\d{4}-\d{2}-\d{2}$/.test(t)) t = t + ' 00:00:00'; if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(t)) t = t + ':00'; t = t.replace(' ', 'T') + (function(){const offMin=-new Date().getTimezoneOffset();const sign=offMin>=0?'+':'-';const hh=String(Math.floor(Math.abs(offMin)/60)).padStart(2,'0');const mm=String(Math.abs(offMin)%60).padStart(2,'0');return sign+hh+':'+mm;})() } return Date.parse(t); }catch(e){return null}}; // 获取当前主题的颜色 const getThemeColors = () => { const isLight = document.documentElement.getAttribute('data-theme') === 'light'; return { bg: isLight ? '#f8f9fb' : '#0c0f14', grid: isLight ? '#e5e7eb' : '#1a1f28', text: isLight ? '#6b7280' : '#6b7280', textLabel: isLight ? '#1a1d23' : '#e5e7eb' }; }; // 绘制趋势图 let chartData = null; const drawTrendChart = (pddData, ytData) => { const canvas = document.getElementById('trend-chart'); if(!canvas) return; const ctx = canvas.getContext('2d'); const rect = canvas.parentElement.getBoundingClientRect(); const colors = getThemeColors(); // 处理高分辨率屏幕,避免字体模糊 const dpr = window.devicePixelRatio || 1; canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; ctx.scale(dpr, dpr); // 获取最近30天的日期 const days = []; for(let i = 29; i >= 0; i--){ const d = new Date(); d.setDate(d.getDate() - i); days.push(d.toISOString().split('T')[0]); } // 统计每天的数据(去重MAC地址) const countByDay = (list) => { const uniqueMacsByDay = {}; days.forEach(d => uniqueMacsByDay[d] = new Set()); (list||[]).forEach(r => { if(r.ts_cn && r.mac){ const date = r.ts_cn.split(' ')[0]; if(uniqueMacsByDay[date] !== undefined) { uniqueMacsByDay[date].add(r.mac); } } }); return days.map(d => uniqueMacsByDay[d].size); }; const pddCounts = countByDay(pddData); const ytCounts = countByDay(ytData); const maxCount = Math.max(...pddCounts, ...ytCounts, 1); // 绘制参数(使用逻辑尺寸而非物理像素) const padding = {left: 40, right: 20, top: 20, bottom: 30}; const chartWidth = rect.width - padding.left - padding.right; const chartHeight = rect.height - padding.top - padding.bottom; // 保存图表数据供鼠标事件使用 chartData = {days, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight}; // 清空画布 ctx.fillStyle = colors.bg; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制网格线 ctx.strokeStyle = colors.grid; ctx.lineWidth = 1; for(let i = 0; i <= 4; i++){ const y = padding.top + (chartHeight / 4) * i; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(rect.width - padding.right, y); ctx.stroke(); } // 绘制Y轴刻度 ctx.fillStyle = colors.text; ctx.font = '11px sans-serif'; ctx.textAlign = 'right'; for(let i = 0; i <= 4; i++){ const value = Math.round(maxCount * (4 - i) / 4); const y = padding.top + (chartHeight / 4) * i; ctx.fillText(value.toString(), padding.left - 8, y + 4); } // 绘制X轴标签(每5天显示一个) ctx.textAlign = 'center'; days.forEach((day, i) => { if(i % 5 === 0 || i === days.length - 1){ const x = padding.left + (chartWidth / (days.length - 1)) * i; const label = day.slice(5); ctx.fillText(label, x, rect.height - 8); } }); // 绘制折线 const drawLine = (counts, color) => { ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); counts.forEach((count, i) => { const x = padding.left + (chartWidth / (days.length - 1)) * i; const y = padding.top + chartHeight - (count / maxCount) * chartHeight; if(i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // 绘制数据点 ctx.fillStyle = color; counts.forEach((count, i) => { const x = padding.left + (chartWidth / (days.length - 1)) * i; const y = padding.top + chartHeight - (count / maxCount) * chartHeight; ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill(); }); }; drawLine(pddCounts, '#f59e0b'); drawLine(ytCounts, '#3b82f6'); // 绘制图例 ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; ctx.fillStyle = '#f59e0b'; ctx.fillRect(padding.left, 5, 12, 12); ctx.fillStyle = colors.textLabel; ctx.fillText('拼多多', padding.left + 18, 15); ctx.fillStyle = '#3b82f6'; ctx.fillRect(padding.left + 80, 5, 12, 12); ctx.fillStyle = colors.textLabel; ctx.fillText('圆通', padding.left + 98, 15); }; // 初始绘制 drawTrendChart(pdd.list, yt.list); // 添加鼠标悬停事件 const canvas = document.getElementById('trend-chart'); const tooltip = document.getElementById('chart-tooltip'); if(canvas && tooltip){ canvas.onmousemove = (e) => { if(!chartData) return; const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const {days, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight} = chartData; // 查找最近的数据点 let nearestIndex = -1; let minDist = 15; for(let i = 0; i < days.length; i++){ const x = padding.left + (chartWidth / (days.length - 1)) * i; const pddY = padding.top + chartHeight - (pddCounts[i] / maxCount) * chartHeight; const ytY = padding.top + chartHeight - (ytCounts[i] / maxCount) * chartHeight; const distPdd = Math.sqrt((mouseX - x) ** 2 + (mouseY - pddY) ** 2); const distYt = Math.sqrt((mouseX - x) ** 2 + (mouseY - ytY) ** 2); const dist = Math.min(distPdd, distYt); if(dist < minDist){ minDist = dist; nearestIndex = i; } } if(nearestIndex >= 0){ const date = days[nearestIndex]; const pddCount = pddCounts[nearestIndex]; const ytCount = ytCounts[nearestIndex]; tooltip.innerHTML = `
    ${date}
    拼多多: ${pddCount}
    圆通: ${ytCount}
    `; tooltip.style.display = 'block'; // 获取提示框宽度以便放在鼠标左侧 const tooltipWidth = tooltip.offsetWidth || 100; tooltip.style.left = (e.clientX - rect.left - tooltipWidth - 10) + 'px'; tooltip.style.top = (e.clientY - rect.top - 10) + 'px'; } else { tooltip.style.display = 'none'; } }; canvas.onmouseleave = () => { tooltip.style.display = 'none'; }; } // 刷新审计列表的函数 const refreshAuditLists = async() => { if(window.__auditBusy) return; window.__auditBusy=true; const pdd2=await API.auditPddQuiet(window.__pddParams).catch(()=>({list:[]})); const yt2=await API.auditYtQuiet(window.__ytParams).catch(()=>({list:[]})); const sP=toEpoch(window.__pddParams.start), eP=toEpoch(window.__pddParams.end); const sY=toEpoch(window.__ytParams.start), eY=toEpoch(window.__ytParams.end); const pddView=(pdd2.list||[]).filter(r=>{const t=toEpoch(r.ts_cn); if(t==null) return false; return (sP==null||t>=sP)&&(eP==null||t<=eP);}); const ytView=(yt2.list||[]).filter(r=>{const t=toEpoch(r.ts_cn); if(t==null) return false; return (sY==null||t>=sY)&&(eY==null||t<=eY);}); const pddEls=pddView.map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const ytEls=ytView.map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const p=document.getElementById('audit-pdd'); if(p) p.innerHTML=pddEls; const y=document.getElementById('audit-yt'); if(y) y.innerHTML=ytEls; window.__auditBusy=false; }; // 设置日期筛选事件 const q = id => document.getElementById(id); const dateToRange = d => { if(!d) return {}; return { start: d + ' 00:00:00', end: d + ' 23:59:59' }; }; const pddDateEl=q('audit-date-pdd'); const ytDateEl=q('audit-date-yt'); if(pddDateEl){ pddDateEl.onchange=()=>{ const d=pddDateEl.value; window.__pddParams = dateToRange(d); refreshAuditLists(); // 立即刷新 }; } if(ytDateEl){ ytDateEl.onchange=()=>{ const d=ytDateEl.value; window.__ytParams = dateToRange(d); refreshAuditLists(); // 立即刷新 }; } // 资源管理:缓存数据(已禁用自动清理) window.__auditCache = window.__auditCache || {pdd: [], yt: []}; // 优化的刷新函数:一次请求同时更新趋势图和列表 const refreshAll = async() => { if(window.__auditBusy) { console.log('[Dashboard] 上次请求还在进行中,跳过本次刷新'); return; } // 检查是否还在dashboard页面 const currentPath = location.hash.replace('#', '') || '/dashboard'; if(currentPath !== '/dashboard'){ if(window.__auditTimer){ clearInterval(window.__auditTimer); window.__auditTimer = null; } return; } // 取消之前的请求 if(window.__auditAbortController){ window.__auditAbortController.abort(); } window.__auditAbortController = new AbortController(); window.__auditBusy=true; const startTime = Date.now(); // 设置超时保护:15秒后强制重置busy状态 const timeoutId = setTimeout(() => { console.warn('[Dashboard] 请求超时,强制重置状态'); window.__auditBusy = false; if(window.__auditAbortController){ window.__auditAbortController.abort(); window.__auditAbortController = null; } }, 15000); try { // 请求全部数据(不限制),传递AbortController信号 const signal = window.__auditAbortController.signal; const [pddRes, ytRes] = await Promise.all([ fetch('/api/audit/pdd', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', signal }).then(r => r.ok ? r.json() : {list:[]}).catch((e)=>{ if(e.name === 'AbortError') console.log('[Dashboard] PDD请求被取消'); return {list:[]}; }), fetch('/api/audit/yt', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', signal }).then(r => r.ok ? r.json() : {list:[]}).catch((e)=>{ if(e.name === 'AbortError') console.log('[Dashboard] YT请求被取消'); return {list:[]}; }) ]); clearTimeout(timeoutId); const duration = Date.now() - startTime; if(duration > 3000){ console.warn('[Dashboard] 请求耗时过长:', duration, 'ms'); } // 再次检查页面,避免切换后更新 if(location.hash.replace('#', '') !== '/dashboard'){ return; } // 缓存数据(只保留最近30天的数据以节省内存) const filterRecent30Days = (list) => { const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; return (list || []).filter(r => { if(!r.ts_cn) return false; const t = toEpoch(r.ts_cn); return t && t >= cutoff; }); }; window.__auditCache.pdd = filterRecent30Days(pddRes.list); window.__auditCache.yt = filterRecent30Days(ytRes.list); // 更新趋势图 drawTrendChart(window.__auditCache.pdd, window.__auditCache.yt); // 更新列表(应用筛选,只显示前100条) const sP=toEpoch(window.__pddParams.start), eP=toEpoch(window.__pddParams.end); const sY=toEpoch(window.__ytParams.start), eY=toEpoch(window.__ytParams.end); const pddView=(window.__auditCache.pdd||[]).filter(r=>{const t=toEpoch(r.ts_cn); if(t==null) return false; return (sP==null||t>=sP)&&(eP==null||t<=eP);}); const ytView=(window.__auditCache.yt||[]).filter(r=>{const t=toEpoch(r.ts_cn); if(t==null) return false; return (sY==null||t>=sY)&&(eY==null||t<=eY);}); const pddEls=pddView.slice(0, 100).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const ytEls=ytView.slice(0, 100).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const p=document.getElementById('audit-pdd'); if(p) p.innerHTML=pddEls; const y=document.getElementById('audit-yt'); if(y) y.innerHTML=ytEls; } catch(e) { clearTimeout(timeoutId); if(e.name !== 'AbortError'){ console.error('更新审计数据失败:', e); } } finally { window.__auditBusy=false; window.__auditAbortController = null; } }; // 刷新间隔10秒 window.__auditTimer=setInterval(refreshAll, 10000); },0); return `
    ${metricsCard('直通良品率', data.fpyRate || '—', 'success')} ${metricsCard('良品率', data.goodRate, 'success')} ${metricsCard('发货数量', data.shipments, 'warning')} ${metricsCard('不良数量', data.badCount ?? data.defects, 'danger')}
    审计趋势(最近30天)
    拼多多审计
      ${pddList}
    圆通审计
      ${ytList}
    `; } Router.register('/dashboard', render); })();