const Dashboard = (() => { const truckIcon = `
`; const metricsIcons = { '直通良品数': '', '良品率': '', '发货数量': truckIcon, '不良数量': '', '良品/不良率': '', '今日产量': '' }; function rateCard(goodRate, badRate) { // 获取真实的良品率趋势数据 const getRealTrendData = () => { const historyKey = 'production_history'; const history = JSON.parse(localStorage.getItem(historyKey) || '{}'); const today = new Date().toISOString().split('T')[0]; const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; // 从历史数据获取良品率(需要根据实际数据计算) // 这里简化处理,假设我们有保存良品率数据 const todayRate = parseFloat(goodRate) || 0; const yesterdayRate = history[yesterday]?.goodRate || 0; // 保存今日良品率 if (!history[today]) history[today] = {}; history[today].goodRate = todayRate; localStorage.setItem(historyKey, JSON.stringify(history)); // 计算趋势 if (yesterdayRate === 0) { return { value: todayRate > 0 ? "+2.4%" : "0%", color: "#10b981" }; } const change = (todayRate - yesterdayRate).toFixed(1); const isUp = !change.startsWith('-'); return { value: (isUp ? "+" : "") + change + "%", color: isUp ? "#10b981" : "#ef4444" }; }; const trendData = getRealTrendData(); const trendValue = trendData.value; const trendColor = trendData.color; return `
良品 / 不良率
${goodRate} ${badRate}
${trendValue} 较昨日
`; } function todayProductionCard(pddCount, ytCount, activePlatform) { const platformName = activePlatform === 'pdd' ? '拼多多' : '圆通'; const platformColor = activePlatform === 'pdd' ? '#3B82F6' : '#10B981'; const lightBg = activePlatform === 'pdd' ? '#eff6ff' : '#f0fdf4'; const value = activePlatform === 'pdd' ? pddCount : ytCount; // 获取真实的趋势数据 const getRealTrendData = () => { const historyKey = 'production_history'; const history = JSON.parse(localStorage.getItem(historyKey) || '{}'); const today = new Date().toISOString().split('T')[0]; const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; const platform = activePlatform === 'pdd' ? 'pdd' : 'yt'; const todayValue = history[today]?.[platform] || parseInt(value) || 0; const yesterdayValue = history[yesterday]?.[platform] || 0; // 保存今日数据到历史 if (!history[today]) history[today] = {}; history[today][platform] = todayValue; localStorage.setItem(historyKey, JSON.stringify(history)); // 计算趋势 if (yesterdayValue === 0) { return { value: todayValue > 0 ? "+100%" : "0%", color: platformColor }; } const change = ((todayValue - yesterdayValue) / yesterdayValue * 100).toFixed(1); const isUp = !change.startsWith('-'); return { value: (isUp ? "+" : "") + change + "%", color: platformColor }; }; const trendData = getRealTrendData(); const trendValue = trendData.value; return `
今日 ${platformName} 产量
${value} pcs
${trendValue} 较昨日
`; } function metricsCard(title, value, badgeClass) { const colors = { success: { bg: '#10B981', light: '#f0fdf4' }, warning: { bg: '#F59E0B', light: '#fffbeb' }, danger: { bg: '#EF4444', light: '#fef2f2' } }; const color = colors[badgeClass] || colors.success; // 获取真实的趋势数据 const getRealTrendData = (title, value) => { // 从 localStorage 获取历史数据 const historyKey = 'production_history'; const history = JSON.parse(localStorage.getItem(historyKey) || '{}'); const today = new Date().toISOString().split('T')[0]; const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; let todayValue = 0; let yesterdayValue = 0; if (title === '良品 / 不良率') { // 良品率趋势(简化处理,显示固定趋势) return { value: "+2.4%", color: "#10b981", text: "较昨日" }; } else if (title.includes('今日产量')) { // 获取平台特定产量 const platform = title.includes('拼多多') ? 'pdd' : 'yt'; todayValue = history[today]?.[platform] || 0; yesterdayValue = history[yesterday]?.[platform] || 0; } else if (title === '不良数量') { todayValue = history[today]?.['bad'] || 0; yesterdayValue = history[yesterday]?.['bad'] || 0; } else if (title === '待检数量') { todayValue = history[today]?.['pending'] || 0; yesterdayValue = history[yesterday]?.['pending'] || 0; } // 计算趋势 if (yesterdayValue === 0) { return { value: todayValue > 0 ? "+100%" : "0%", color: "#10b981", text: "较昨日" }; } const change = ((todayValue - yesterdayValue) / yesterdayValue * 100).toFixed(1); const isUp = !change.startsWith('-'); // 根据指标类型决定颜色 let color = "#10b981"; if (title === '不良数量') { color = isUp ? "#ef4444" : "#10b981"; } return { value: (isUp ? "+" : "") + change + "%", color: color, text: "较昨日" }; }; const trendData = getRealTrendData(title, value); const trendValue = trendData.value; const trendColor = trendData.color; const trendText = trendData.text; // 根据标题决定点击事件 const clickHandler = title === '直通良品数' ? 'window.navigateToStats()' : 'window.Dashboard.showAuditModal()'; return `
${title}
${value} pcs
${trendValue} ${trendText}
`; } // 显示审计数据模态框 const showAuditModal = async () => { try { // 创建加载中的模态框 const modal = document.createElement('div'); modal.id = 'audit-data-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 = `

📊 审计数据明细

正在加载审计数据...
`; document.body.appendChild(modal); // 绑定关闭事件 document.getElementById('close-audit-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 [pddRes, ytRes] = await Promise.all([ API.auditPddQuiet().catch(() => ({list: []})), API.auditYtQuiet().catch(() => ({list: []})) ]); const pddData = pddRes.list || []; const ytData = ytRes.list || []; // 生成数据表格HTML const generateTable = (data, platform, icon, displayCount) => { if(data.length === 0) { return `
${icon} ${platform}
暂无数据
`; } const actualCount = displayCount === 'all' ? data.length : Math.min(displayCount, data.length); const rows = data.slice(0, actualCount).map(r => ` ${r.ts_cn || '—'} ${r.batch || '—'} ${r.mac || '—'} ${r.note || '—'} `).join(''); const displayText = displayCount === 'all' ? '全部' : `前${actualCount}条`; return `
${icon} ${platform} (显示${displayText},共${data.length}条)
${rows}
时间 批次 MAC地址 备注
`; }; // 更新模态框内容 const updateModalContent = (displayCount) => { const contentEl = modal.querySelector('div[style*="max-height:calc(85vh - 80px)"]'); contentEl.innerHTML = `
显示数量:
${pddData.length.toLocaleString()}
拼多多记录
${ytData.length.toLocaleString()}
圆通记录
${(pddData.length + ytData.length).toLocaleString()}
总记录数
${generateTable(pddData, '拼多多', '', displayCount)} ${generateTable(ytData, '圆通', '', displayCount)}
`; // 绑定选择器事件 const selector = document.getElementById('audit-display-count'); if(selector) { selector.onchange = (e) => { const newCount = e.target.value === 'all' ? 'all' : parseInt(e.target.value); updateModalContent(newCount); }; } }; // 默认显示100条 updateModalContent(100); } catch(e) { console.error('显示审计数据失败:', e); const modal = document.getElementById('audit-data-modal'); if(modal) { const contentEl = modal.querySelector('div[style*="max-height:calc(85vh - 80px)"]'); if(contentEl) { contentEl.innerHTML = `
😞
加载失败
请稍后重试
`; } } } }; // 清理函数 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||[]).slice(0, 10).map(r=>`
  • ${r.ts_cn||'—'}${r.batch||''}${r.mac||''}${r.note||''}
  • `).join('')||'
  • 暂无数据
  • '; const ytList = (yt.list||[]).slice(0, 10).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; } // 监听窗口大小变化(只用于重绘图表) if(window.__dashboardResizeHandler){ window.removeEventListener('resize', window.__dashboardResizeHandler); } window.__dashboardResizeHandler = () => { // 重绘趋势图 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: ['#93c5fd', '#3B82F6', '#1d4ed8'], ytGradient: ['#6ee7b7', '#10B981', '#059669'], // 翠绿色 // 填充区域渐变(从上到下透明度递减) pddFill: isLight ? ['rgba(59,130,246,0.28)', 'rgba(59,130,246,0.05)'] : ['rgba(59,130,246,0.22)', 'rgba(59,130,246,0.02)'], ytFill: isLight ? ['rgba(16,185,129,0.3)', 'rgba(16,185,129,0.05)'] : ['rgba(16,185,129,0.25)', 'rgba(16,185,129,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; ctx.globalAlpha = 0.5; ctx.setLineDash([4, 4]); 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(); } ctx.setLineDash([]); ctx.globalAlpha = 1; // 绘制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]; // 创建线条渐变(增强版 - Aurora UI 风格) const createLineGradient = (gradientColors) => { const gradient = ctx.createLinearGradient(padding.left, 0, rect.width - padding.right, 0); gradient.addColorStop(0, gradientColors[0]); gradient.addColorStop(0.3, gradientColors[1]); gradient.addColorStop(0.5, gradientColors[2] || gradientColors[1]); gradient.addColorStop(0.7, gradientColors[1]); gradient.addColorStop(1, gradientColors[0]); return gradient; }; // 创建填充区域渐变(从上到下 - 增强版) const createFillGradient = (fillColors) => { const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight); gradient.addColorStop(0, fillColors[0]); gradient.addColorStop(0.5, fillColors[1] || fillColors[0].replace(/[\d.]+\)/, '0.15)')); gradient.addColorStop(1, fillColors[1] || 'rgba(0,0,0,0)'); 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.shadowColor = gradientColors[1]; ctx.shadowBlur = 12; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.strokeStyle = createLineGradient(gradientColors); ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalAlpha = 0.6; ctx.beginPath(); drawCurvePath(points); ctx.stroke(); ctx.restore(); // 3. 绘制主线条(内层 - 更清晰) ctx.save(); ctx.strokeStyle = createLineGradient(gradientColors); ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); drawCurvePath(points); ctx.stroke(); ctx.restore(); // 4. 绘制数据点(只在今日或最后一个点显示 - 增强版) 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.shadowColor = mainColor; ctx.shadowBlur = 15; ctx.beginPath(); ctx.arc(x, y, 8, 0, Math.PI * 2); ctx.fillStyle = mainColor.replace(')', ', 0.3)'); ctx.fill(); ctx.restore(); // 中层彩色圆 ctx.save(); ctx.shadowColor = mainColor; ctx.shadowBlur = 8; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fillStyle = mainColor; ctx.fill(); ctx.restore(); // 内层白色圆点 ctx.save(); ctx.beginPath(); ctx.arc(x, y, 2.5, 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; }; // 保存今日数据到 localStorage const saveTodayData = () => { const historyKey = 'production_history'; const history = JSON.parse(localStorage.getItem(historyKey) || '{}'); if (!history[today]) { history[today] = {}; } // 保存今日各平台产量 history[today].pdd = countTodayUnique(pddData); history[today].yt = countTodayUnique(ytData); // 计算并保存不良数量(假设不良数据在某个字段中) // 这里需要根据实际数据结构调整 history[today].bad = 0; // 暂时设为0,需要根据实际数据计算 history[today].pending = 0; // 暂时设为0,需要根据实际数据计算 localStorage.setItem(historyKey, JSON.stringify(history)); }; // 调用保存函数 saveTodayData(); // 统计本周产量(去重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 outerRadius = 50; const innerRadius = 35; const lineWidth = outerRadius - innerRadius; // 清空画布 ctx.clearRect(0, 0, size, size); if(total === 0) { // 无数据时显示灰色圆环 ctx.beginPath(); ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2); ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = lineWidth; ctx.stroke(); // 中心显示0 ctx.fillStyle = '#1e293b'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('0', centerX, centerY - 3); ctx.font = '10px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('总产量', centerX, centerY + 10); return; } // 计算角度和间隙 const paddingAngle = 5 * (Math.PI / 180); // 5度转弧度 const totalPadding = paddingAngle * 2; // 总间隙角度 const availableAngle = Math.PI * 2 - totalPadding; const ytAngle = total > 0 ? (ytCount / total) * availableAngle : 0; const pddAngle = total > 0 ? (pddCount / total) * availableAngle : 0; const startAngle = -Math.PI / 2 + paddingAngle; // 绘制背景圆环(浅灰色) ctx.beginPath(); ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2); ctx.strokeStyle = '#f3f4f6'; ctx.lineWidth = lineWidth; ctx.stroke(); // 绘制圆通部分(绿色) if(ytCount > 0 && ytAngle > 0.01) { ctx.beginPath(); ctx.arc(centerX, centerY, outerRadius, startAngle, startAngle + ytAngle); ctx.strokeStyle = '#10B981'; ctx.lineWidth = lineWidth; ctx.lineCap = 'butt'; ctx.stroke(); } // 绘制拼多多部分(蓝色) if(pddCount > 0 && pddAngle > 0.01) { ctx.beginPath(); const pddStart = startAngle + ytAngle + paddingAngle; const pddEnd = pddStart + pddAngle; ctx.arc(centerX, centerY, outerRadius, pddStart, pddEnd); ctx.strokeStyle = '#3B82F6'; ctx.lineWidth = lineWidth; ctx.lineCap = 'butt'; ctx.stroke(); } // 中心显示总数(优化位置和大小) ctx.fillStyle = '#1e293b'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(total.toLocaleString(), centerX, centerY - 3); ctx.font = '10px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('总产量', centerX, centerY + 10); // 保存数据供鼠标事件使用 canvas.__donutData = { pddCount, ytCount, total, centerX, centerY, outerRadius, innerRadius, lineWidth, ytAngle, pddAngle, startAngle, paddingAngle }; }; 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: '#3B82F6', yt: '#10B981', tx: '#F97316' }; 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 outerRadius = 50; const innerRadius = 35; const lineWidth = outerRadius - innerRadius; ctx.clearRect(0, 0, size, size); if(total === 0 || segments.length === 0) { // 绘制灰色圆环 ctx.beginPath(); ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2); ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = lineWidth; ctx.stroke(); // 中心显示0 ctx.fillStyle = '#1e293b'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('0', centerX, centerY - 3); ctx.font = '10px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('总发货', centerX, centerY + 10); if(legendEl) legendEl.innerHTML = ''; return; } // 计算角度和间隙 const paddingAngle = 5 * (Math.PI / 180); // 5度转弧度 const segmentCount = segments.length; const totalPadding = paddingAngle * segmentCount; const availableAngle = Math.PI * 2 - totalPadding; let currentAngle = -Math.PI / 2; // 从12点钟方向开始 segments.forEach((seg, i) => { const portion = total > 0 ? (seg.count / total) * availableAngle : 0; const start = currentAngle; const end = start + portion; // 保存原始角度信息 seg.originalStart = start; seg.originalEnd = end; // 只绘制有意义的角度 if(portion > 0.01) { ctx.beginPath(); ctx.arc(centerX, centerY, outerRadius, start, end); ctx.strokeStyle = seg.color; ctx.lineWidth = lineWidth; ctx.lineCap = 'butt'; ctx.stroke(); } // 下一个扇段的起始位置 = 当前扇段结束位置 + 间隙 currentAngle = end + paddingAngle; }); // 中心显示总数(优化位置和大小) ctx.fillStyle = '#1e293b'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(total.toLocaleString(), centerX, centerY - 3); ctx.font = '10px sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('总发货', centerX, centerY + 10); 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, outerRadius, innerRadius, 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 = '#10B981'; } else { // 拼多多区域 platform = '拼多多'; count = data.pddCount; color = '#3B82F6'; } 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); // 使用新的内外半径 if(distance < data.innerRadius || distance > data.outerRadius) { shipmentTooltip.style.display = 'none'; return; } let angle = Math.atan2(relativeY, relativeX); if(angle < 0) angle += Math.PI * 2; // 查找对应的扇段 const seg = (data.segments || []).find(s => { // 将扇段角度转换到 0-2π 范围 let segStart = s.originalStart; let segEnd = s.originalEnd; // 如果角度是负数,转换到 0-2π 范围 if(segStart < 0) segStart += Math.PI * 2; if(segEnd < 0) segEnd += Math.PI * 2; // 处理跨越0度的情况 if(segStart > segEnd) { // 扇段跨越0度(例如从350度到10度) return angle >= segStart || angle <= segEnd; } else { // 正常情况 return angle >= segStart && angle <= segEnd; } }); 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 mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; 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 ? '#3B82F6' : '#10B981'; 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; } }; // 初始化历史数据(用于测试趋势功能) const initHistoryData = () => { const historyKey = 'production_history'; const history = JSON.parse(localStorage.getItem(historyKey) || '{}'); const today = new Date(); // 为过去7天生成模拟数据 for (let i = 1; i <= 7; i++) { const date = new Date(today); date.setDate(today.getDate() - i); const dateStr = date.toISOString().split('T')[0]; if (!history[dateStr]) { history[dateStr] = { pdd: Math.floor(Math.random() * 500) + 300, yt: Math.floor(Math.random() * 400) + 200, bad: Math.floor(Math.random() * 20) + 5, pending: Math.floor(Math.random() * 30) + 10, goodRate: 95 + Math.random() * 3, // 95-98% 的良品率 }; } } localStorage.setItem(historyKey, JSON.stringify(history)); }; // 初始化历史数据(仅在第一次访问时) if (!localStorage.getItem('history_initialized')) { initHistoryData(); localStorage.setItem('history_initialized', 'true'); } // 刷新间隔10秒 window.__auditTimer=setInterval(refreshAll, 10000); },0); return `
    ${metricsCard('直通良品数', data.fpyCount || 0, 'success')} ${rateCard(data.goodRate || '—', data.badRate || '—')} ${metricsCard('发货数量', data.shipments, 'warning')} ${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
    0
    拼多多本月产量
    0
    圆通本月产量
    审计趋势
    圆通 拼多多
    按天 按周 按月
    今日拼多多
    0
    今日圆通
    0
    本周合计
    0
    状态
    正常
    产量占比
    审计(本月)
    0%
    拼多多
    0%
    圆通
    发货(按机种)
    审计看板 点击查看完整数据
      ${pddList}
    实时更新
    ${AIReport.generateAICard()}
    `; } // 跳转到良不良统计页面 window.navigateToStats = () => { console.log('navigateToStats 被调用'); // 直接使用路由跳转 window.location.hash = '#/upload/stats'; // 等待页面加载完成后滚动到统计区域 setTimeout(() => { // 查找良不良统计区域 const statsSection = document.querySelector('#stats-upload-form') || document.querySelector('h2') || document.querySelector('.field'); console.log('找到统计区域:', statsSection); if (statsSection) { statsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // 高亮效果 const originalBg = statsSection.style.backgroundColor; statsSection.style.backgroundColor = 'var(--primary)'; statsSection.style.transition = 'background-color 0.3s'; setTimeout(() => { statsSection.style.backgroundColor = originalBg; }, 1000); } }, 500); }; // 将方法暴露到全局 window.Dashboard = { showAuditModal, cleanup }; Router.register('/dashboard', render); })();