const Dashboard = (() => { const truckIcon = `
`; const metricsIcons = { '直通良品数': '', '良品率': '', '发货数量': truckIcon, '不良数量': '', '良品/不良率': '', '今日产量': '' }; // 合并良品率/不良率卡片 function rateCard(goodRate, badRate) { const icon = metricsIcons['良品/不良率']; const goodNum = parseFloat(String(goodRate).replace('%', '')) || 0; const badNum = parseFloat(String(badRate).replace('%', '')) || 0; return `
${icon}

良品/不良率

${goodRate}/${badRate}

`; } // 今日产量卡片(拼多多/圆通自动切换) function todayProductionCard(todayPdd, todayYt, activePlatform) { const isPdd = activePlatform === 'pdd'; const platformName = isPdd ? '拼多多' : '圆通'; const platformIcon = isPdd ? '' : ''; const value = isPdd ? todayPdd : todayYt; const color = isPdd ? { bg: '#f6af3dff', text: '#f6af3dff' } : { bg: '#1a6dd6', text: '#1a6dd6' }; const fillWidth = Math.min((value / 10000) * 100, 100); return `
${platformIcon}

今日${platformName}

${value}

`; } function metricsCard(title, value, badgeClass) { const colors = { success: { bg: '#10B981', text: '#02972f' }, warning: { bg: '#F59E0B', text: '#B45309' }, danger: { bg: '#EF4444', text: '#DC2626' } }; const color = colors[badgeClass] || colors.success; const isPercent = String(value).includes('%'); const numValue = parseFloat(String(value).replace('%', '')) || 0; const fillWidth = isPercent ? Math.min(numValue, 100) : Math.min((numValue / 100000) * 100, 100); const icon = metricsIcons[title] || metricsIcons['良品率']; const isTruck = title === '发货数量'; const iconBg = isTruck ? '#f5f5f5' : color.bg; return `
${icon}

${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 donutCanvas = document.getElementById('donut-chart'); if(donutCanvas){ donutCanvas.onmousemove = null; donutCanvas.onmouseleave = null; } const shipmentDonutCanvas = document.getElementById('shipment-donut-chart'); if(shipmentDonutCanvas){ shipmentDonutCanvas.onmousemove = null; shipmentDonutCanvas.onmouseleave = null; } // 清理日期选择器事件 const auditDateEl = document.getElementById('audit-date'); const platformSelect = document.getElementById('audit-platform-select'); const pddDateEl = document.getElementById('audit-date-pdd'); const ytDateEl = document.getElementById('audit-date-yt'); if(auditDateEl) auditDateEl.onchange = null; if(platformSelect) platformSelect.onchange = null; if(pddDateEl) pddDateEl.onchange = null; if(ytDateEl) ytDateEl.onchange = null; // 清理resize监听器 if(window.__dashboardResizeHandler){ window.removeEventListener('resize', window.__dashboardResizeHandler); window.__dashboardResizeHandler = null; } // 清理主题切换监听器 if(window.__dashboardThemeHandler){ window.removeEventListener('themeChanged', window.__dashboardThemeHandler); window.__dashboardThemeHandler = null; } // 清理重绘函数引用 window.__redrawTrendChart = 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; } // 动态计算审计框高度 const calculateAuditHeight = () => { const viewEl = document.getElementById('view'); if(!viewEl) return 460; const viewHeight = viewEl.clientHeight; const viewPadding = 40; // view的padding (20px * 2) // 获取其他元素的高度 const metricsEl = document.querySelector('.dashboard-metrics-4col'); const trendEl = document.querySelector('.card'); const metricsHeight = metricsEl ? metricsEl.offsetHeight : 60; const trendHeight = trendEl ? trendEl.offsetHeight : 260; const gaps = 24; // margin-top间距 (12px * 2) // 计算剩余高度 const remainingHeight = viewHeight - viewPadding - metricsHeight - trendHeight - gaps; // 最小高度300px,最大不超过剩余空间 return Math.max(300, Math.min(remainingHeight, 460)); }; // 设置审计框高度 const setAuditHeight = () => { const height = calculateAuditHeight(); const auditCard = document.getElementById('audit-card'); const pddCard = document.getElementById('audit-pdd-card'); const ytCard = document.getElementById('audit-yt-card'); if(auditCard) auditCard.style.height = height + 'px'; if(pddCard) pddCard.style.height = height + 'px'; if(ytCard) ytCard.style.height = height + 'px'; }; // 初始设置高度 setAuditHeight(); // 监听窗口大小变化 if(window.__dashboardResizeHandler){ window.removeEventListener('resize', window.__dashboardResizeHandler); } window.__dashboardResizeHandler = () => { setAuditHeight(); // 重绘趋势图 if(window.__redrawTrendChart) window.__redrawTrendChart(); }; window.addEventListener('resize', window.__dashboardResizeHandler); 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 ? '#ffffff' : '#0f1623', grid: isLight ? '#f1f5f9' : '#1e293b', text: isLight ? '#94a3b8' : '#64748b', textLabel: isLight ? '#1e293b' : '#e5e7eb', // 渐变色(参考图样式:紫色和橙色) pddGradient: ['#fbbf24', '#f59e0b', '#ea580c'], // 黄-橙 ytGradient: ['#a78bfa', '#8b5cf6', '#7c3aed'], // 紫色 // 填充区域渐变(从上到下透明度递减) pddFill: isLight ? ['rgba(251,191,36,0.3)', 'rgba(251,191,36,0.05)'] : ['rgba(251,191,36,0.25)', 'rgba(251,191,36,0.02)'], ytFill: isLight ? ['rgba(139,92,246,0.3)', 'rgba(139,92,246,0.05)'] : ['rgba(139,92,246,0.25)', 'rgba(139,92,246,0.02)'] }; }; // 当前时间维度:day, week, month window.__trendTimeRange = window.__trendTimeRange || 'day'; // 绘制趋势图 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 timeRange = window.__trendTimeRange || 'day'; // 处理高分辨率屏幕,避免字体模糊 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); // 根据时间维度生成时间点 const getTimePoints = () => { const points = []; const now = new Date(); if(timeRange === 'day') { // 最近30天 for(let i = 29; i >= 0; i--){ const d = new Date(now); d.setDate(d.getDate() - i); points.push(d.toISOString().split('T')[0]); } } else if(timeRange === 'week') { // 最近12周 for(let i = 11; i >= 0; i--){ const d = new Date(now); d.setDate(d.getDate() - i * 7); const weekStart = new Date(d); weekStart.setDate(d.getDate() - d.getDay() + 1); // 周一 points.push(weekStart.toISOString().split('T')[0]); } } else { // 最近12个月 for(let i = 11; i >= 0; i--){ const d = new Date(now); d.setMonth(d.getMonth() - i); points.push(d.toISOString().slice(0, 7)); // YYYY-MM } } return points; }; const timePoints = getTimePoints(); // 统计数据(去重MAC地址) const countByTime = (list) => { const uniqueMacs = {}; timePoints.forEach(t => uniqueMacs[t] = new Set()); (list||[]).forEach(r => { if(r.ts_cn && r.mac){ const date = r.ts_cn.split(' ')[0]; if(timeRange === 'day') { if(uniqueMacs[date] !== undefined) { uniqueMacs[date].add(r.mac); } } else if(timeRange === 'week') { // 找到对应的周 const recordDate = new Date(date); for(let i = 0; i < timePoints.length; i++){ const weekStart = new Date(timePoints[i]); const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6); if(recordDate >= weekStart && recordDate <= weekEnd){ uniqueMacs[timePoints[i]].add(r.mac); break; } } } else { // 月份 const month = date.slice(0, 7); if(uniqueMacs[month] !== undefined) { uniqueMacs[month].add(r.mac); } } } }); return timePoints.map(t => uniqueMacs[t].size); }; const pddCounts = countByTime(pddData); const ytCounts = countByTime(ytData); const maxCount = Math.max(...pddCounts, ...ytCounts, 1); // 绘制参数(使用逻辑尺寸而非物理像素) // 根据最大值动态调整左边距,确保Y轴标签不被截断 const maxDigits = Math.max(maxCount.toString().length, 2); const dynamicLeftPadding = Math.max(50, 25 + maxDigits * 10); const padding = {left: dynamicLeftPadding, right: 20, top: 20, bottom: 30}; const chartWidth = rect.width - padding.left - padding.right; const chartHeight = rect.height - padding.top - padding.bottom; // Y轴留白:给底部预留10%的空间,避免0值点画到边缘看不见 const yAxisMargin = chartHeight * 0.1; const effectiveChartHeight = chartHeight - yAxisMargin; // 保存图表数据供鼠标事件使用 chartData = {timePoints, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight}; // 清空画布 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轴刻度(大数值格式化显示) const formatNumber = (n) => { if(n >= 10000) return (n / 10000).toFixed(1) + '万'; if(n >= 1000) return (n / 1000).toFixed(1) + 'k'; return n.toString(); }; 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(formatNumber(value), padding.left - 8, y + 4); } // 绘制X轴标签 ctx.textAlign = 'center'; const labelInterval = timeRange === 'day' ? 5 : (timeRange === 'week' ? 2 : 2); timePoints.forEach((point, i) => { if(i % labelInterval === 0 || i === timePoints.length - 1){ const x = padding.left + (chartWidth / (timePoints.length - 1)) * i; let label; if(timeRange === 'day') { label = point.slice(5); // MM-DD } else if(timeRange === 'week') { label = point.slice(5); // MM-DD (周起始) } else { label = point.slice(2); // YY-MM } ctx.fillText(label, x, rect.height - 8); } }); // 绘制带填充区域的曲线(参考图样式) const today = new Date().toISOString().split('T')[0]; // 创建线条渐变 const createLineGradient = (gradientColors) => { const gradient = ctx.createLinearGradient(padding.left, 0, rect.width - padding.right, 0); gradient.addColorStop(0, gradientColors[0]); gradient.addColorStop(0.5, gradientColors[1]); gradient.addColorStop(1, gradientColors[2] || gradientColors[1]); return gradient; }; // 创建填充区域渐变(从上到下) const createFillGradient = (fillColors) => { const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight); gradient.addColorStop(0, fillColors[0]); gradient.addColorStop(1, fillColors[1]); return gradient; }; // 绘制贝塞尔曲线路径(复用) const drawCurvePath = (points) => { ctx.moveTo(points[0].x, points[0].y); for(let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; const next = points[i + 1] || curr; const prevPrev = points[i - 2] || prev; const tension = 0.35; const cp1x = prev.x + (curr.x - prevPrev.x) * tension; const cp1y = prev.y + (curr.y - prevPrev.y) * tension; const cp2x = curr.x - (next.x - prev.x) * tension; const cp2y = curr.y - (next.y - prev.y) * tension; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, curr.x, curr.y); } }; // 收集今日标签位置 const todayLabels = []; const drawAreaLine = (counts, gradientColors, fillColors, labelKey) => { const points = counts.map((count, i) => ({ x: padding.left + (chartWidth / (timePoints.length - 1)) * i, y: padding.top + chartHeight - yAxisMargin - (count / maxCount) * effectiveChartHeight })); // 1. 绘制填充区域 ctx.save(); ctx.beginPath(); drawCurvePath(points); // 闭合路径到底部 ctx.lineTo(points[points.length - 1].x, padding.top + chartHeight); ctx.lineTo(points[0].x, padding.top + chartHeight); ctx.closePath(); ctx.fillStyle = createFillGradient(fillColors); ctx.fill(); ctx.restore(); // 2. 绘制主线条 ctx.save(); ctx.strokeStyle = createLineGradient(gradientColors); ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); drawCurvePath(points); ctx.stroke(); ctx.restore(); // 3. 绘制数据点(只在今日或最后一个点显示) const mainColor = gradientColors[1]; counts.forEach((count, i) => { const x = points[i].x; const y = points[i].y; const isToday = timeRange === 'day' && timePoints[i] === today; const isLast = i === counts.length - 1; if(isToday || isLast) { // 高亮点 ctx.save(); ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fillStyle = mainColor; ctx.fill(); ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.restore(); if(isToday) todayLabels.push({x, y, count, color: mainColor, key: labelKey}); } }); }; // 先绘制圆通(紫色在下层),再绘制拼多多(橙色在上层) drawAreaLine(ytCounts, colors.ytGradient, colors.ytFill, 'yt'); drawAreaLine(pddCounts, colors.pddGradient, colors.pddFill, 'pdd'); // 智能绘制今日标签,显示在高亮点的左边(数字为0时不显示) const nonZeroLabels = todayLabels.filter(label => label.count > 0); if(nonZeroLabels.length === 2) { const [a, b] = nonZeroLabels; const yDiff = Math.abs(a.y - b.y); if(yDiff < 20) { // 两个标签太近,垂直错开显示在左边 const upper = a.y < b.y ? a : b; const lower = a.y < b.y ? b : a; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'right'; ctx.fillStyle = upper.color; ctx.fillText(upper.count.toString(), upper.x - 14, upper.y - 8); ctx.fillStyle = lower.color; ctx.fillText(lower.count.toString(), lower.x - 14, lower.y + 8); } else { // 正常显示在左边 nonZeroLabels.forEach(label => { ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'right'; ctx.fillStyle = label.color; ctx.fillText(label.count.toString(), label.x - 14, label.y + 4); }); } } else { nonZeroLabels.forEach(label => { ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'right'; ctx.fillStyle = label.color; ctx.fillText(label.count.toString(), label.x - 14, label.y + 4); }); } // 图例已在HTML中定义,无需在Canvas中绘制 }; // 初始化缓存数据 window.__auditCache = window.__auditCache || {pdd: [], yt: []}; if(pdd.list && pdd.list.length > 0) window.__auditCache.pdd = pdd.list; if(yt.list && yt.list.length > 0) window.__auditCache.yt = yt.list; // 初始绘制 drawTrendChart(pdd.list, yt.list); // 监听主题切换事件,立即重绘图表 const themeChangeHandler = () => { // 使用缓存数据或初始数据 const pddData = (window.__auditCache && window.__auditCache.pdd && window.__auditCache.pdd.length > 0) ? window.__auditCache.pdd : pdd.list; const ytData = (window.__auditCache && window.__auditCache.yt && window.__auditCache.yt.length > 0) ? window.__auditCache.yt : yt.list; drawTrendChart(pddData, ytData); }; window.addEventListener('themeChanged', themeChangeHandler); // 保存监听器引用以便清理 window.__dashboardThemeHandler = themeChangeHandler; // 保存重绘函数引用,供 resize 时调用 window.__redrawTrendChart = themeChangeHandler; // 添加鼠标悬停事件 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 {timePoints, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight} = chartData; // 查找最近的数据点 let nearestIndex = -1; let minDist = 15; for(let i = 0; i < timePoints.length; i++){ const x = padding.left + (chartWidth / (timePoints.length - 1)) * i; const pddY = padding.top + chartHeight - yAxisMargin - (pddCounts[i] / maxCount) * effectiveChartHeight; const ytY = padding.top + chartHeight - yAxisMargin - (ytCounts[i] / maxCount) * effectiveChartHeight; 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 point = timePoints[nearestIndex]; const pddCount = pddCounts[nearestIndex]; const ytCount = ytCounts[nearestIndex]; let label = point; if(timeRange === 'week') label = point + ' 周'; else if(timeRange === 'month') label = point + ' 月'; tooltip.innerHTML = `
    ${label}
    拼多多: ${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'; }; } // 时间维度切换事件(tabs样式) const trendRangeTabs = document.getElementById('trend-range-tabs'); if(trendRangeTabs){ trendRangeTabs.querySelectorAll('.trend-tab').forEach(tab => { tab.onclick = () => { // 更新active状态 trendRangeTabs.querySelectorAll('.trend-tab').forEach(t => { t.classList.remove('active'); t.style.background = ''; t.style.color = ''; }); tab.classList.add('active'); tab.style.background = 'var(--primary)'; tab.style.color = '#fff'; window.__trendTimeRange = tab.dataset.range; const pddData = window.__auditCache?.pdd || pdd.list; const ytData = window.__auditCache?.yt || yt.list; drawTrendChart(pddData, ytData); updateTrendStats(pddData, ytData); }; }); // 初始化active样式 const activeTab = trendRangeTabs.querySelector('.trend-tab.active'); if(activeTab){ activeTab.style.background = 'var(--primary)'; activeTab.style.color = '#fff'; } } // 更新趋势统计数据 const updateTrendStats = (pddData, ytData) => { const now = new Date(); const today = now.toISOString().split('T')[0]; const thisMonth = today.slice(0, 7); const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() + 1); const weekStartStr = weekStart.toISOString().split('T')[0]; // 统计本月产量(去重MAC) const countMonthUnique = (list) => { const macs = new Set(); (list || []).forEach(r => { if(r.ts_cn && r.mac && r.ts_cn.startsWith(thisMonth)){ macs.add(r.mac); } }); return macs.size; }; // 统计今日产量(去重MAC) const countTodayUnique = (list) => { const macs = new Set(); (list || []).forEach(r => { if(r.ts_cn && r.mac && r.ts_cn.startsWith(today)){ macs.add(r.mac); } }); return macs.size; }; // 统计本周产量(去重MAC) const countWeekUnique = (list) => { const macs = new Set(); (list || []).forEach(r => { if(r.ts_cn && r.mac){ const date = r.ts_cn.split(' ')[0]; if(date >= weekStartStr && date <= today){ macs.add(r.mac); } } }); return macs.size; }; const pddMonth = countMonthUnique(pddData); const ytMonth = countMonthUnique(ytData); const pddToday = countTodayUnique(pddData); const ytToday = countTodayUnique(ytData); const weekTotal = countWeekUnique(pddData) + countWeekUnique(ytData); // 更新DOM const el = (id) => document.getElementById(id); if(el('trend-total-pdd')) el('trend-total-pdd').textContent = pddMonth.toLocaleString(); if(el('trend-total-yt')) el('trend-total-yt').textContent = ytMonth.toLocaleString(); if(el('trend-today-pdd')) el('trend-today-pdd').textContent = pddToday.toLocaleString(); if(el('trend-today-yt')) el('trend-today-yt').textContent = ytToday.toLocaleString(); if(el('trend-week-total')) el('trend-week-total').textContent = weekTotal.toLocaleString(); // 状态判断 const statusEl = el('trend-status'); if(statusEl){ if(pddToday + ytToday > 0){ statusEl.textContent = '生产中'; statusEl.style.color = '#10b981'; } else { statusEl.textContent = '待机'; statusEl.style.color = '#6b7280'; } } // 绘制环形图 drawDonutChart(pddMonth, ytMonth); }; // 绘制环形图 const drawDonutChart = (pddCount, ytCount) => { const canvas = document.getElementById('donut-chart'); if(!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const size = 160; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; ctx.scale(dpr, dpr); const total = pddCount + ytCount; const pddPct = total > 0 ? Math.round((pddCount / total) * 100) : 0; const ytPct = total > 0 ? Math.round((ytCount / total) * 100) : 0; // 更新百分比显示 const pddPctEl = document.getElementById('donut-pdd-pct'); const ytPctEl = document.getElementById('donut-yt-pct'); if(pddPctEl) pddPctEl.textContent = pddPct + '%'; if(ytPctEl) ytPctEl.textContent = ytPct + '%'; const centerX = size / 2; const centerY = size / 2; const radius = 65; const lineWidth = 18; // 清空画布 ctx.clearRect(0, 0, size, size); if(total === 0) { // 无数据时显示灰色圆环 ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = lineWidth; ctx.stroke(); return; } // 绘制圆通部分(紫色) const ytAngle = (ytCount / total) * Math.PI * 2; ctx.beginPath(); ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + ytAngle); ctx.strokeStyle = '#8b5cf6'; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); // 绘制拼多多部分(橙色) ctx.beginPath(); ctx.arc(centerX, centerY, radius, -Math.PI / 2 + ytAngle, -Math.PI / 2 + Math.PI * 2); ctx.strokeStyle = '#f6af3dff'; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); // 中心显示总数 ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb'; ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(total.toLocaleString(), centerX, centerY - 8); ctx.font = '11px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('本月总产量', centerX, centerY + 14); // 保存数据供鼠标事件使用 canvas.__donutData = { pddCount, ytCount, total, centerX, centerY, radius, lineWidth, ytAngle, pddAngle: Math.PI * 2 - ytAngle }; }; const drawShipmentDonutChart = (stats) => { const canvas = document.getElementById('shipment-donut-chart'); if(!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const size = 160; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; ctx.scale(dpr, dpr); const legendEl = document.getElementById('shipment-donut-legend'); const total = (stats && typeof stats.total === 'number') ? stats.total : 0; const byPlatform = (stats && stats.by_platform) ? stats.by_platform : {}; const items = Object.entries(byPlatform).filter(([, v]) => (v || 0) > 0); items.sort((a, b) => (b[1] || 0) - (a[1] || 0)); const platformName = (k) => { const map = { pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版', unknown: '未知' }; return map[k] || k; }; const platformColor = (k, fallbackIndex) => { const fixed = { pdd: '#f6af3dff', tx: '#3b82f6', yt: '#8b5cf6' }; if(fixed[k]) return fixed[k]; const palette = ['#10b981', '#06b6d4', '#f97316', '#a3e635', '#ef4444', '#f59e0b']; return palette[fallbackIndex % palette.length]; }; const segments = items.map(([k, v], i) => ({ key: k, label: platformName(k), count: v || 0, color: platformColor(k, i) })); const centerX = size / 2; const centerY = size / 2; const radius = 65; const lineWidth = 18; ctx.clearRect(0, 0, size, size); if(total === 0 || segments.length === 0) { ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = lineWidth; ctx.stroke(); if(legendEl) legendEl.innerHTML = ''; return; } let acc = 0; segments.forEach(seg => { const portion = (seg.count / total) * Math.PI * 2; const start = acc; const end = acc + portion; seg.start = start; seg.end = end; ctx.beginPath(); ctx.arc(centerX, centerY, radius, -Math.PI / 2 + start, -Math.PI / 2 + end); ctx.strokeStyle = seg.color; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.stroke(); acc = end; }); ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb'; ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(total.toLocaleString(), centerX, centerY - 8); ctx.font = '11px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('发货总量', centerX, centerY + 14); if(legendEl) { legendEl.innerHTML = segments.map(seg => { const pct = total > 0 ? ((seg.count / total) * 100).toFixed(1) : '0.0'; return `
    ${pct}%
    ${seg.label}
    `; }).join(''); } canvas.__shipmentDonutData = { total, centerX, centerY, radius, lineWidth, segments }; }; const refreshShipmentPlatformStats = async(force=false) => { window.__shipmentStatsTs = window.__shipmentStatsTs || 0; const now = Date.now(); if(!force && (now - window.__shipmentStatsTs) < 60000 && window.__shipmentStatsCache) { drawShipmentDonutChart(window.__shipmentStatsCache); return; } try { const res = await fetch('/api/shipments/platform-stats', { headers: { 'Content-Type': 'application/json' }, credentials: 'include' }).then(r => r.ok ? r.json() : null); if(res && res.ok) { window.__shipmentStatsCache = res; window.__shipmentStatsTs = now; drawShipmentDonutChart(res); } } catch(e) { } }; // 初始化统计数据 updateTrendStats(pdd.list, yt.list); refreshShipmentPlatformStats(true); // 环形图鼠标悬浮事件 const donutCanvas = document.getElementById('donut-chart'); if(donutCanvas) { // 创建提示框 const donutTooltip = document.createElement('div'); donutTooltip.id = 'donut-tooltip'; donutTooltip.style.cssText = 'position:absolute;background:rgba(15,22,35,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);z-index:1000'; donutCanvas.parentElement.appendChild(donutTooltip); donutCanvas.onmousemove = (e) => { const data = donutCanvas.__donutData; if(!data || data.total === 0) { donutTooltip.style.display = 'none'; return; } const rect = donutCanvas.getBoundingClientRect(); const relativeX = e.clientX - rect.left - data.centerX; const relativeY = e.clientY - rect.top - data.centerY; const distance = Math.sqrt(relativeX * relativeX + relativeY * relativeY); // 检查是否在环形区域内 const innerRadius = data.radius - data.lineWidth / 2; const outerRadius = data.radius + data.lineWidth / 2; if(distance < innerRadius || distance > outerRadius) { donutTooltip.style.display = 'none'; return; } // 计算角度 let angle = Math.atan2(relativeY, relativeX); if(angle < 0) angle += Math.PI * 2; // 调整角度,使其从12点钟方向开始 angle = (angle + Math.PI / 2) % (Math.PI * 2); let platform, count, color; if(angle <= data.ytAngle) { // 圆通区域 platform = '圆通'; count = data.ytCount; color = '#8b5cf6'; } else { // 拼多多区域 platform = '拼多多'; count = data.pddCount; color = '#f6af3dff'; } const pct = ((count / data.total) * 100).toFixed(1); donutTooltip.innerHTML = `
    ${platform}
    数量: ${count.toLocaleString()}
    占比: ${pct}%
    `; donutTooltip.style.display = 'block'; // 智能定位:使用页面坐标而非容器坐标 const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // 先显示提示框以获取实际尺寸 donutTooltip.style.visibility = 'hidden'; donutTooltip.style.display = 'block'; const tooltipWidth = donutTooltip.offsetWidth; const tooltipHeight = donutTooltip.offsetHeight; donutTooltip.style.visibility = 'visible'; // 使用页面绝对坐标进行计算 const mousePageX = e.clientX; const mousePageY = e.clientY; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 水平定位:默认右侧,页面右侧空间不足时显示在左侧 let tooltipPageX, tooltipPageY; if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) { // 页面右侧有足够空间,显示在右侧 tooltipPageX = mousePageX + 15; } else { // 页面右侧空间不足,显示在左侧 tooltipPageX = mousePageX - tooltipWidth - 15; if(tooltipPageX < 10) tooltipPageX = 10; // 防止超出页面左边界 } // 垂直定位:鼠标上方10px tooltipPageY = mousePageY - 10; if(tooltipPageY < 10) tooltipPageY = mousePageY + 15; // 如果上方空间不足,显示在下方 if(tooltipPageY + tooltipHeight > viewportHeight - 10) tooltipPageY = viewportHeight - tooltipHeight - 10; // 转换回相对于容器的坐标 const containerRect = donutCanvas.parentElement.getBoundingClientRect(); const left = tooltipPageX - containerRect.left; const top = tooltipPageY - containerRect.top; donutTooltip.style.left = left + 'px'; donutTooltip.style.top = top + 'px'; }; donutCanvas.onmouseleave = () => { donutTooltip.style.display = 'none'; }; } const shipmentDonutCanvas = document.getElementById('shipment-donut-chart'); if(shipmentDonutCanvas) { const shipmentTooltip = document.createElement('div'); shipmentTooltip.id = 'shipment-donut-tooltip'; shipmentTooltip.style.cssText = 'position:absolute;background:rgba(15,22,35,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);z-index:1000'; shipmentDonutCanvas.parentElement.appendChild(shipmentTooltip); shipmentDonutCanvas.onmousemove = (e) => { const data = shipmentDonutCanvas.__shipmentDonutData; if(!data || !data.total || !data.segments || data.segments.length === 0) { shipmentTooltip.style.display = 'none'; return; } const rect = shipmentDonutCanvas.getBoundingClientRect(); const relativeX = e.clientX - rect.left - data.centerX; const relativeY = e.clientY - rect.top - data.centerY; const distance = Math.sqrt(relativeX * relativeX + relativeY * relativeY); const innerRadius = data.radius - data.lineWidth / 2; const outerRadius = data.radius + data.lineWidth / 2; if(distance < innerRadius || distance > outerRadius) { shipmentTooltip.style.display = 'none'; return; } let angle = Math.atan2(relativeY, relativeX); if(angle < 0) angle += Math.PI * 2; angle = (angle + Math.PI / 2) % (Math.PI * 2); const seg = (data.segments || []).find(s => angle >= s.start && angle <= s.end); if(!seg) { shipmentTooltip.style.display = 'none'; return; } const pct = ((seg.count / data.total) * 100).toFixed(1); shipmentTooltip.innerHTML = `
    ${seg.label}
    数量: ${seg.count.toLocaleString()}
    占比: ${pct}%
    `; shipmentTooltip.style.display = 'block'; shipmentTooltip.style.visibility = 'hidden'; shipmentTooltip.style.display = 'block'; const tooltipWidth = shipmentTooltip.offsetWidth; const tooltipHeight = shipmentTooltip.offsetHeight; shipmentTooltip.style.visibility = 'visible'; const mousePageX = e.clientX; const mousePageY = e.clientY; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let tooltipPageX, tooltipPageY; if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) { tooltipPageX = mousePageX + 15; } else { tooltipPageX = mousePageX - tooltipWidth - 15; if(tooltipPageX < 10) tooltipPageX = 10; } tooltipPageY = mousePageY - 10; if(tooltipPageY < 10) tooltipPageY = mousePageY + 15; if(tooltipPageY + tooltipHeight > viewportHeight - 10) tooltipPageY = viewportHeight - tooltipHeight - 10; const containerRect = shipmentDonutCanvas.parentElement.getBoundingClientRect(); const left = tooltipPageX - containerRect.left; const top = tooltipPageY - containerRect.top; shipmentTooltip.style.left = left + 'px'; shipmentTooltip.style.top = top + 'px'; }; shipmentDonutCanvas.onmouseleave = () => { shipmentTooltip.style.display = 'none'; }; } // 月度汇总功能 const showMonthlySummary = () => { const pddData = window.__auditCache?.pdd || pdd.list || []; const ytData = window.__auditCache?.yt || yt.list || []; const now = new Date(); const thisMonth = now.toISOString().slice(0, 7); const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().slice(0, 7); // 按日期统计(去重MAC) const countByDate = (list, monthPrefix) => { const dateMap = {}; (list || []).forEach(r => { if(r.ts_cn && r.mac && r.ts_cn.startsWith(monthPrefix)){ const date = r.ts_cn.split(' ')[0]; if(!dateMap[date]) dateMap[date] = new Set(); dateMap[date].add(r.mac); } }); return Object.entries(dateMap).map(([date, macs]) => ({date, count: macs.size})).sort((a,b) => a.date.localeCompare(b.date)); }; // 统计本月和上月 const pddThisMonth = countByDate(pddData, thisMonth); const ytThisMonth = countByDate(ytData, thisMonth); const pddLastMonth = countByDate(pddData, lastMonth); const ytLastMonth = countByDate(ytData, lastMonth); const sumCount = (arr) => arr.reduce((s, x) => s + x.count, 0); const pddThisTotal = sumCount(pddThisMonth); const ytThisTotal = sumCount(ytThisMonth); const pddLastTotal = sumCount(pddLastMonth); const ytLastTotal = sumCount(ytLastMonth); const thisTotal = pddThisTotal + ytThisTotal; const lastTotal = pddLastTotal + ytLastTotal; // 计算环比增长 const calcGrowth = (curr, prev) => { if(prev === 0) return curr > 0 ? '+100%' : '0%'; const pct = ((curr - prev) / prev * 100).toFixed(1); return pct >= 0 ? '+' + pct + '%' : pct + '%'; }; // 生成日期明细表格 const generateTable = (pddArr, ytArr) => { const allDates = [...new Set([...pddArr.map(x=>x.date), ...ytArr.map(x=>x.date)])].sort(); if(allDates.length === 0) return '
    暂无数据
    '; const pddMap = Object.fromEntries(pddArr.map(x => [x.date, x.count])); const ytMap = Object.fromEntries(ytArr.map(x => [x.date, x.count])); let rows = allDates.map(date => { const pdd = pddMap[date] || 0; const yt = ytMap[date] || 0; return `${date.slice(5)}${pdd}${yt}${pdd+yt}`; }).join(''); return `${rows}
    日期拼多多圆通合计
    `; }; // 创建模态框 const modal = document.createElement('div'); modal.id = 'monthly-summary-modal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn 0.2s'; modal.innerHTML = `

    月度汇总

    本月总产量
    ${thisTotal.toLocaleString()}
    环比 ${calcGrowth(thisTotal, lastTotal)}
    上月总产量
    ${lastTotal.toLocaleString()}
    ${lastMonth}
    拼多多本月
    ${pddThisTotal.toLocaleString()}
    圆通本月
    ${ytThisTotal.toLocaleString()}
    📅 本月日期明细
    ${generateTable(pddThisMonth, ytThisMonth)}
    `; document.body.appendChild(modal); // 关闭事件 modal.querySelector('#close-summary-modal').onclick = () => modal.remove(); modal.onclick = (e) => { if(e.target === modal) modal.remove(); }; document.addEventListener('keydown', function escHandler(e) { if(e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', escHandler); } }); }; // 绑定月度汇总按钮事件 const summaryBtn = document.getElementById('trend-summary-btn'); if(summaryBtn) summaryBtn.onclick = showMonthlySummary; // 刷新审计列表的函数 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 platformSelect = q('audit-platform-select'); const auditDateEl = q('audit-date'); const auditListEl = q('audit-list'); // 当前选中的平台 window.__currentAuditPlatform = 'pdd'; // 更新整合看板显示 const updateAuditDisplay = () => { const platform = window.__currentAuditPlatform; const params = platform === 'pdd' ? window.__pddParams : window.__ytParams; const sTime = toEpoch(params.start); const eTime = toEpoch(params.end); const cacheData = platform === 'pdd' ? window.__auditCache.pdd : window.__auditCache.yt; const filteredData = (cacheData || []).filter(r => { const t = toEpoch(r.ts_cn); if (t == null) return false; return (sTime == null || t >= sTime) && (eTime == null || t <= eTime); }); const platformIcon = platform === 'pdd' ? '' : ''; const listHtml = filteredData.slice(0, 100).map(r => `
  • ${platformIcon}${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • ` ).join('') || '
  • 暂无数据
  • '; if (auditListEl) auditListEl.innerHTML = listHtml; }; // 平台切换事件 if (platformSelect) { platformSelect.onchange = () => { window.__currentAuditPlatform = platformSelect.value; // 同步日期选择器 const otherDateEl = platformSelect.value === 'pdd' ? q('audit-date-pdd') : q('audit-date-yt'); if (auditDateEl && otherDateEl) { auditDateEl.value = otherDateEl.value; } updateAuditDisplay(); }; } // 整合看板日期筛选 if (auditDateEl) { auditDateEl.onchange = () => { const d = auditDateEl.value; const platform = window.__currentAuditPlatform; if (platform === 'pdd') { window.__pddParams = dateToRange(d); const pddDateEl = q('audit-date-pdd'); if (pddDateEl) pddDateEl.value = d; } else { window.__ytParams = dateToRange(d); const ytDateEl = q('audit-date-yt'); if (ytDateEl) ytDateEl.value = d; } refreshAuditLists(); updateAuditDisplay(); }; } 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(); // 立即刷新 }; } // 资源管理:缓存数据(已在初始化时创建) // 优化的刷新函数:一次请求同时更新趋势图和列表 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, dashRes] = 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:[]}; }), fetch('/api/dashboard', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', signal }).then(r => r.ok ? r.json() : null).catch((e)=>{ if(e.name === 'AbortError') console.log('[Dashboard] Dashboard请求被取消'); return null; }) ]); // 实时更新良品/不良率卡片和今日产量卡片 if(dashRes) { // 更新良品/不良率 const goodRateEl = document.getElementById('good-rate-value'); const badRateEl = document.getElementById('bad-rate-value'); if(goodRateEl) goodRateEl.textContent = dashRes.goodRate || '—'; if(badRateEl) badRateEl.textContent = dashRes.badRate || '—'; // 更新今日产量卡片 const productionCard = document.getElementById('today-production-card'); const platformNameEl = document.getElementById('today-platform-name'); const productionValueEl = document.getElementById('today-production-value'); const productionFillEl = document.getElementById('today-production-fill'); if(productionCard && platformNameEl && productionValueEl && productionFillEl) { const currentPlatform = productionCard.dataset.platform; const newPlatform = dashRes.activePlatform || 'pdd'; const isPdd = newPlatform === 'pdd'; const value = isPdd ? (dashRes.todayPdd || 0) : (dashRes.todayYt || 0); const platformName = isPdd ? '拼多多' : '圆通'; const color = isPdd ? '#f6af3dff' : '#1a6dd6'; const fillWidth = Math.min((value / 10000) * 100, 100); // 如果平台切换了,更新图标 if(currentPlatform !== newPlatform) { productionCard.dataset.platform = newPlatform; const iconEl = productionCard.querySelector('.metrics-icon'); if(iconEl) { iconEl.innerHTML = isPdd ? '' : ''; } const percentEl = productionCard.querySelector('.metrics-percent'); if(percentEl) percentEl.style.color = color; } platformNameEl.textContent = '今日' + platformName; productionValueEl.textContent = value; productionFillEl.style.width = fillWidth + '%'; productionFillEl.style.backgroundColor = color; } } clearTimeout(timeoutId); const duration = Date.now() - startTime; if(duration > 3000){ console.warn('[Dashboard] 请求耗时过长:', duration, 'ms'); } // 再次检查页面,避免切换后更新 if(location.hash.replace('#', '') !== '/dashboard'){ return; } // 缓存全部数据(不再限制30天,支持大数据量显示) window.__auditCache.pdd = pddRes.list || []; window.__auditCache.yt = ytRes.list || []; // 更新趋势图和统计数据 drawTrendChart(window.__auditCache.pdd, window.__auditCache.yt); updateTrendStats(window.__auditCache.pdd, window.__auditCache.yt); refreshShipmentPlatformStats(false); // 更新列表(应用筛选,只显示前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; // 更新整合看板 const updateAuditDisplay = () => { const platform = window.__currentAuditPlatform || 'pdd'; const params = platform === 'pdd' ? window.__pddParams : window.__ytParams; const sTime = toEpoch(params.start); const eTime = toEpoch(params.end); const cacheData = platform === 'pdd' ? window.__auditCache.pdd : window.__auditCache.yt; const filteredData = (cacheData || []).filter(r => { const t = toEpoch(r.ts_cn); if (t == null) return false; return (sTime == null || t >= sTime) && (eTime == null || t <= eTime); }); const platformIcon = platform === 'pdd' ? '' : ''; const listHtml = filteredData.slice(0, 100).map(r => `
  • ${platformIcon}${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • ` ).join('') || '
  • 暂无数据
  • '; const auditListEl = document.getElementById('audit-list'); if (auditListEl) auditListEl.innerHTML = listHtml; }; updateAuditDisplay(); } 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 `
    0
    拼多多本月产量
    0
    圆通本月产量
    审计趋势
    圆通 拼多多
    按天 按周 按月
    今日拼多多
    0
    今日圆通
    0
    本周合计
    0
    状态
    正常
    产量占比
    审计(本月)
    0%
    拼多多
    0%
    圆通
    发货(按机种)
    ${metricsCard('直通良品数', data.fpyCount || 0, 'success')} ${rateCard(data.goodRate || '—', data.badRate || '—')} ${metricsCard('发货数量', data.shipments, 'warning')} ${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
    审计看板
      ${pddList}
    `; } Router.register('/dashboard', render); })();