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 `
${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 `
${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 `
`;
}
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}条)
`;
};
// 更新模态框内容
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 ``;
}).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 ``;
};
// 创建模态框
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')}
${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);
})();