diff --git a/frontend/js/components/dashboard.js b/frontend/js/components/dashboard.js
index 59cb468..f9fcf5e 100644
--- a/frontend/js/components/dashboard.js
+++ b/frontend/js/components/dashboard.js
@@ -43,7 +43,7 @@ const Dashboard = (() => {
const platformName = isPdd ? '拼多多' : '圆通';
const platformIcon = isPdd ? '
';
const value = isPdd ? todayPdd : todayYt;
- const color = isPdd ? { bg: '#e02e24', text: '#e02e24' } : { bg: '#1a6dd6', text: '#1a6dd6' };
+ const color = isPdd ? { bg: '#f6af3dff', text: '#f6af3dff' } : { bg: '#1a6dd6', text: '#1a6dd6' };
const fillWidth = Math.min((value / 10000) * 100, 100);
return `
@@ -119,6 +119,16 @@ const Dashboard = (() => {
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');
@@ -242,14 +252,20 @@ const Dashboard = (() => {
return Date.parse(t);
}catch(e){return null}};
- // 获取当前主题的颜色
+ // 获取当前主题的颜色(参考图样式)
const getThemeColors = () => {
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
return {
- bg: isLight ? '#f8f9fb' : '#0c0f14',
- grid: isLight ? '#e5e7eb' : '#1a1f28',
- text: isLight ? '#6b7280' : '#6b7280',
- textLabel: isLight ? '#1a1d23' : '#e5e7eb'
+ bg: isLight ? '#ffffff' : '#0f1623',
+ grid: isLight ? '#f1f5f9' : '#1e293b',
+ text: isLight ? '#94a3b8' : '#64748b',
+ textLabel: isLight ? '#1e293b' : '#e5e7eb',
+ // 渐变色(参考图样式:紫色和橙色)
+ pddGradient: ['#fbbf24', '#f59e0b', '#ea580c'], // 黄-橙
+ ytGradient: ['#a78bfa', '#8b5cf6', '#7c3aed'], // 紫色
+ // 填充区域渐变(从上到下透明度递减)
+ pddFill: isLight ? ['rgba(251,191,36,0.3)', 'rgba(251,191,36,0.05)'] : ['rgba(251,191,36,0.25)', 'rgba(251,191,36,0.02)'],
+ ytFill: isLight ? ['rgba(139,92,246,0.3)', 'rgba(139,92,246,0.05)'] : ['rgba(139,92,246,0.25)', 'rgba(139,92,246,0.02)']
};
};
@@ -356,8 +372,12 @@ const Dashboard = (() => {
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};
+ chartData = {timePoints, pddCounts, ytCounts, maxCount, padding, chartWidth, chartHeight, timeRange, yAxisMargin, effectiveChartHeight};
// 清空画布
ctx.fillStyle = colors.bg;
@@ -374,14 +394,19 @@ const Dashboard = (() => {
ctx.stroke();
}
- // 绘制Y轴刻度
+ // 绘制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(value.toString(), padding.left - 8, y + 4);
+ ctx.fillText(formatNumber(value), padding.left - 8, y + 4);
}
// 绘制X轴标签
@@ -402,69 +427,102 @@ const Dashboard = (() => {
}
});
- // 绘制柔和曲线(智能避免标签重叠)
+ // 绘制带填充区域的曲线(参考图样式)
const today = new Date().toISOString().split('T')[0];
- // 收集今日标签位置
- const todayLabels = [];
- const drawLineWithLabels = (counts, color, labelKey) => {
- ctx.strokeStyle = color;
- ctx.lineWidth = 2.5;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.beginPath();
-
- const points = counts.map((count, i) => ({
- x: padding.left + (chartWidth / (timePoints.length - 1)) * i,
- y: padding.top + chartHeight - (count / maxCount) * chartHeight
- }));
-
+ // 创建线条渐变
+ const createLineGradient = (gradientColors) => {
+ const gradient = ctx.createLinearGradient(padding.left, 0, rect.width - padding.right, 0);
+ gradient.addColorStop(0, gradientColors[0]);
+ gradient.addColorStop(0.5, gradientColors[1]);
+ gradient.addColorStop(1, gradientColors[2] || gradientColors[1]);
+ return gradient;
+ };
+
+ // 创建填充区域渐变(从上到下)
+ const createFillGradient = (fillColors) => {
+ const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight);
+ gradient.addColorStop(0, fillColors[0]);
+ gradient.addColorStop(1, fillColors[1]);
+ return gradient;
+ };
+
+ // 绘制贝塞尔曲线路径(复用)
+ const drawCurvePath = (points) => {
ctx.moveTo(points[0].x, points[0].y);
for(let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
const next = points[i + 1] || curr;
const prevPrev = points[i - 2] || prev;
- const tension = 0.3;
+ 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);
}
- ctx.stroke();
+ };
+
+ // 收集今日标签位置
+ const todayLabels = [];
+ const drawAreaLine = (counts, gradientColors, fillColors, labelKey) => {
+ const points = counts.map((count, i) => ({
+ x: padding.left + (chartWidth / (timePoints.length - 1)) * i,
+ y: padding.top + chartHeight - yAxisMargin - (count / maxCount) * effectiveChartHeight
+ }));
- // 绘制数据点并收集今日标签
+ // 1. 绘制填充区域
+ ctx.save();
+ ctx.beginPath();
+ drawCurvePath(points);
+ // 闭合路径到底部
+ ctx.lineTo(points[points.length - 1].x, padding.top + chartHeight);
+ ctx.lineTo(points[0].x, padding.top + chartHeight);
+ ctx.closePath();
+ ctx.fillStyle = createFillGradient(fillColors);
+ ctx.fill();
+ ctx.restore();
+
+ // 2. 绘制主线条
+ ctx.save();
+ ctx.strokeStyle = createLineGradient(gradientColors);
+ ctx.lineWidth = 2.5;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.beginPath();
+ drawCurvePath(points);
+ ctx.stroke();
+ ctx.restore();
+
+ // 3. 绘制数据点(只在今日或最后一个点显示)
+ const mainColor = gradientColors[1];
counts.forEach((count, i) => {
- const x = padding.left + (chartWidth / (timePoints.length - 1)) * i;
- const y = padding.top + chartHeight - (count / maxCount) * chartHeight;
+ 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) {
- ctx.beginPath();
- ctx.arc(x, y, 10, 0, Math.PI * 2);
- ctx.fillStyle = color + '33';
- ctx.fill();
+ if(isToday || isLast) {
+ // 高亮点
+ ctx.save();
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
- ctx.fillStyle = color;
+ ctx.fillStyle = mainColor;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
- todayLabels.push({x, y, count, color, key: labelKey});
- } else {
- ctx.beginPath();
- ctx.arc(x, y, 3, 0, Math.PI * 2);
- ctx.fillStyle = color;
- ctx.fill();
+ ctx.restore();
+ if(isToday) todayLabels.push({x, y, count, color: mainColor, key: labelKey});
}
});
};
- drawLineWithLabels(pddCounts, '#f59e0b', 'pdd');
- drawLineWithLabels(ytCounts, '#3b82f6', 'yt');
+ // 先绘制圆通(紫色在下层),再绘制拼多多(橙色在上层)
+ drawAreaLine(ytCounts, colors.ytGradient, colors.ytFill, 'yt');
+ drawAreaLine(pddCounts, colors.pddGradient, colors.pddFill, 'pdd');
// 智能绘制今日标签,显示在高亮点的左边(数字为0时不显示)
const nonZeroLabels = todayLabels.filter(label => label.count > 0);
@@ -500,17 +558,7 @@ const Dashboard = (() => {
});
}
- // 绘制图例
- ctx.font = '12px sans-serif';
- ctx.textAlign = 'left';
- ctx.fillStyle = '#f59e0b';
- ctx.fillRect(padding.left, 5, 12, 12);
- ctx.fillStyle = colors.textLabel;
- ctx.fillText('拼多多', padding.left + 18, 15);
- ctx.fillStyle = '#3b82f6';
- ctx.fillRect(padding.left + 80, 5, 12, 12);
- ctx.fillStyle = colors.textLabel;
- ctx.fillText('圆通', padding.left + 98, 15);
+ // 图例已在HTML中定义,无需在Canvas中绘制
};
// 初始化缓存数据
@@ -549,15 +597,15 @@ const Dashboard = (() => {
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} = chartData;
+ 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 - (pddCounts[i] / maxCount) * chartHeight;
- const ytY = padding.top + chartHeight - (ytCounts[i] / maxCount) * chartHeight;
+ 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);
@@ -590,16 +638,611 @@ const Dashboard = (() => {
};
}
- // 时间维度切换事件
- const trendRangeSelect = document.getElementById('trend-range-select');
- if(trendRangeSelect){
- trendRangeSelect.onchange = () => {
- window.__trendTimeRange = trendRangeSelect.value;
- const pddData = window.__auditCache?.pdd || pdd.list;
- const ytData = window.__auditCache?.yt || yt.list;
- drawTrendChart(pddData, ytData);
+ // 时间维度切换事件(tabs样式)
+ const trendRangeTabs = document.getElementById('trend-range-tabs');
+ if(trendRangeTabs){
+ trendRangeTabs.querySelectorAll('.trend-tab').forEach(tab => {
+ tab.onclick = () => {
+ // 更新active状态
+ trendRangeTabs.querySelectorAll('.trend-tab').forEach(t => {
+ t.classList.remove('active');
+ t.style.background = '';
+ t.style.color = '';
+ });
+ tab.classList.add('active');
+ tab.style.background = 'var(--primary)';
+ tab.style.color = '#fff';
+
+ window.__trendTimeRange = tab.dataset.range;
+ const pddData = window.__auditCache?.pdd || pdd.list;
+ const ytData = window.__auditCache?.yt || yt.list;
+ drawTrendChart(pddData, ytData);
+ updateTrendStats(pddData, ytData);
+ };
+ });
+ // 初始化active样式
+ const activeTab = trendRangeTabs.querySelector('.trend-tab.active');
+ if(activeTab){
+ activeTab.style.background = 'var(--primary)';
+ activeTab.style.color = '#fff';
+ }
+ }
+
+ // 更新趋势统计数据
+ const updateTrendStats = (pddData, ytData) => {
+ const now = new Date();
+ const today = now.toISOString().split('T')[0];
+ const thisMonth = today.slice(0, 7);
+ const weekStart = new Date(now);
+ weekStart.setDate(now.getDate() - now.getDay() + 1);
+ const weekStartStr = weekStart.toISOString().split('T')[0];
+
+ // 统计本月产量(去重MAC)
+ const countMonthUnique = (list) => {
+ const macs = new Set();
+ (list || []).forEach(r => {
+ if(r.ts_cn && r.mac && r.ts_cn.startsWith(thisMonth)){
+ macs.add(r.mac);
+ }
+ });
+
+ return macs.size;
+ };
+
+ // 统计今日产量(去重MAC)
+ const countTodayUnique = (list) => {
+ const macs = new Set();
+ (list || []).forEach(r => {
+ if(r.ts_cn && r.mac && r.ts_cn.startsWith(today)){
+ macs.add(r.mac);
+ }
+ });
+ return macs.size;
+ };
+
+ // 统计本周产量(去重MAC)
+ const countWeekUnique = (list) => {
+ const macs = new Set();
+ (list || []).forEach(r => {
+ if(r.ts_cn && r.mac){
+ const date = r.ts_cn.split(' ')[0];
+ if(date >= weekStartStr && date <= today){
+ macs.add(r.mac);
+ }
+ }
+ });
+ return macs.size;
+ };
+
+ const pddMonth = countMonthUnique(pddData);
+ const ytMonth = countMonthUnique(ytData);
+ const pddToday = countTodayUnique(pddData);
+ const ytToday = countTodayUnique(ytData);
+ const weekTotal = countWeekUnique(pddData) + countWeekUnique(ytData);
+
+ // 更新DOM
+ const el = (id) => document.getElementById(id);
+ if(el('trend-total-pdd')) el('trend-total-pdd').textContent = pddMonth.toLocaleString();
+ if(el('trend-total-yt')) el('trend-total-yt').textContent = ytMonth.toLocaleString();
+ if(el('trend-today-pdd')) el('trend-today-pdd').textContent = pddToday.toLocaleString();
+ if(el('trend-today-yt')) el('trend-today-yt').textContent = ytToday.toLocaleString();
+ if(el('trend-week-total')) el('trend-week-total').textContent = weekTotal.toLocaleString();
+
+ // 状态判断
+ const statusEl = el('trend-status');
+ if(statusEl){
+ if(pddToday + ytToday > 0){
+ statusEl.textContent = '生产中';
+ statusEl.style.color = '#10b981';
+ } else {
+ statusEl.textContent = '待机';
+ statusEl.style.color = '#6b7280';
+ }
+ }
+
+ // 绘制环形图
+ drawDonutChart(pddMonth, ytMonth);
+ };
+
+ // 绘制环形图
+ const drawDonutChart = (pddCount, ytCount) => {
+ const canvas = document.getElementById('donut-chart');
+ if(!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const dpr = window.devicePixelRatio || 1;
+ const size = 160;
+ canvas.width = size * dpr;
+ canvas.height = size * dpr;
+ canvas.style.width = size + 'px';
+ canvas.style.height = size + 'px';
+ ctx.scale(dpr, dpr);
+
+ const total = pddCount + ytCount;
+ const pddPct = total > 0 ? Math.round((pddCount / total) * 100) : 0;
+ const ytPct = total > 0 ? Math.round((ytCount / total) * 100) : 0;
+
+ // 更新百分比显示
+ const pddPctEl = document.getElementById('donut-pdd-pct');
+ const ytPctEl = document.getElementById('donut-yt-pct');
+ if(pddPctEl) pddPctEl.textContent = pddPct + '%';
+ if(ytPctEl) ytPctEl.textContent = ytPct + '%';
+
+ const centerX = size / 2;
+ const centerY = size / 2;
+ const radius = 65;
+ const lineWidth = 18;
+
+ // 清空画布
+ ctx.clearRect(0, 0, size, size);
+
+ if(total === 0) {
+ // 无数据时显示灰色圆环
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.strokeStyle = '#e5e7eb';
+ ctx.lineWidth = lineWidth;
+ ctx.stroke();
+ return;
+ }
+
+ // 绘制圆通部分(紫色)
+ const ytAngle = (ytCount / total) * Math.PI * 2;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + ytAngle);
+ ctx.strokeStyle = '#8b5cf6';
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = 'round';
+ ctx.stroke();
+
+ // 绘制拼多多部分(橙色)
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2 + ytAngle, -Math.PI / 2 + Math.PI * 2);
+ ctx.strokeStyle = '#f6af3dff';
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = 'round';
+ ctx.stroke();
+
+ // 中心显示总数
+ ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb';
+ ctx.font = 'bold 24px sans-serif';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(total.toLocaleString(), centerX, centerY - 8);
+ ctx.font = '11px sans-serif';
+ ctx.fillStyle = '#94a3b8';
+ ctx.fillText('本月总产量', centerX, centerY + 14);
+
+ // 保存数据供鼠标事件使用
+ canvas.__donutData = {
+ pddCount, ytCount, total, centerX, centerY, radius, lineWidth,
+ ytAngle, pddAngle: Math.PI * 2 - ytAngle
+ };
+ };
+
+ const drawShipmentDonutChart = (stats) => {
+ const canvas = document.getElementById('shipment-donut-chart');
+ if(!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const dpr = window.devicePixelRatio || 1;
+ const size = 160;
+ canvas.width = size * dpr;
+ canvas.height = size * dpr;
+ canvas.style.width = size + 'px';
+ canvas.style.height = size + 'px';
+ ctx.scale(dpr, dpr);
+
+ const legendEl = document.getElementById('shipment-donut-legend');
+ const total = (stats && typeof stats.total === 'number') ? stats.total : 0;
+ const byPlatform = (stats && stats.by_platform) ? stats.by_platform : {};
+ const items = Object.entries(byPlatform).filter(([, v]) => (v || 0) > 0);
+ items.sort((a, b) => (b[1] || 0) - (a[1] || 0));
+
+ const platformName = (k) => {
+ const map = {
+ pdd: '拼多多',
+ yt: '圆通',
+ tx: '兔喜',
+ mt: '美团',
+ drf: '大润发',
+ std: '标准版',
+ unknown: '未知'
+ };
+ return map[k] || k;
+ };
+
+ const platformColor = (k, fallbackIndex) => {
+ const fixed = {
+ pdd: '#f6af3dff',
+ tx: '#3b82f6',
+ yt: '#8b5cf6'
+ };
+ if(fixed[k]) return fixed[k];
+ const palette = ['#10b981', '#06b6d4', '#f97316', '#a3e635', '#ef4444', '#f59e0b'];
+ return palette[fallbackIndex % palette.length];
+ };
+ const segments = items.map(([k, v], i) => ({
+ key: k,
+ label: platformName(k),
+ count: v || 0,
+ color: platformColor(k, i)
+ }));
+
+ const centerX = size / 2;
+ const centerY = size / 2;
+ const radius = 65;
+ const lineWidth = 18;
+
+ ctx.clearRect(0, 0, size, size);
+
+ if(total === 0 || segments.length === 0) {
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.strokeStyle = '#e5e7eb';
+ ctx.lineWidth = lineWidth;
+ ctx.stroke();
+ if(legendEl) legendEl.innerHTML = '';
+ return;
+ }
+
+ let acc = 0;
+ segments.forEach(seg => {
+ const portion = (seg.count / total) * Math.PI * 2;
+ const start = acc;
+ const end = acc + portion;
+ seg.start = start;
+ seg.end = end;
+
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2 + start, -Math.PI / 2 + end);
+ ctx.strokeStyle = seg.color;
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = 'round';
+ ctx.stroke();
+
+ acc = end;
+ });
+
+ ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb';
+ ctx.font = 'bold 24px sans-serif';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(total.toLocaleString(), centerX, centerY - 8);
+ ctx.font = '11px sans-serif';
+ ctx.fillStyle = '#94a3b8';
+ ctx.fillText('发货总量', centerX, centerY + 14);
+
+ if(legendEl) {
+ legendEl.innerHTML = segments.map(seg => {
+ const pct = total > 0 ? ((seg.count / total) * 100).toFixed(1) : '0.0';
+ return `
`;
+ }).join('');
+ }
+
+ canvas.__shipmentDonutData = { total, centerX, centerY, radius, lineWidth, segments };
+ };
+
+ const refreshShipmentPlatformStats = async(force=false) => {
+ window.__shipmentStatsTs = window.__shipmentStatsTs || 0;
+ const now = Date.now();
+ if(!force && (now - window.__shipmentStatsTs) < 60000 && window.__shipmentStatsCache) {
+ drawShipmentDonutChart(window.__shipmentStatsCache);
+ return;
+ }
+ try {
+ const res = await fetch('/api/shipments/platform-stats', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include'
+ }).then(r => r.ok ? r.json() : null);
+ if(res && res.ok) {
+ window.__shipmentStatsCache = res;
+ window.__shipmentStatsTs = now;
+ drawShipmentDonutChart(res);
+ }
+ } catch(e) {
+ }
+ };
+
+ // 初始化统计数据
+ updateTrendStats(pdd.list, yt.list);
+ refreshShipmentPlatformStats(true);
+
+ // 环形图鼠标悬浮事件
+ const donutCanvas = document.getElementById('donut-chart');
+ if(donutCanvas) {
+ // 创建提示框
+ const donutTooltip = document.createElement('div');
+ donutTooltip.id = 'donut-tooltip';
+ donutTooltip.style.cssText = 'position:absolute;background:rgba(15,22,35,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);z-index:1000';
+ donutCanvas.parentElement.appendChild(donutTooltip);
+
+ donutCanvas.onmousemove = (e) => {
+ const data = donutCanvas.__donutData;
+ if(!data || data.total === 0) {
+ donutTooltip.style.display = 'none';
+ return;
+ }
+
+ const rect = donutCanvas.getBoundingClientRect();
+ const relativeX = e.clientX - rect.left - data.centerX;
+ const relativeY = e.clientY - rect.top - data.centerY;
+ const distance = Math.sqrt(relativeX * relativeX + relativeY * relativeY);
+
+ // 检查是否在环形区域内
+ const innerRadius = data.radius - data.lineWidth / 2;
+ const outerRadius = data.radius + data.lineWidth / 2;
+ if(distance < innerRadius || distance > outerRadius) {
+ donutTooltip.style.display = 'none';
+ return;
+ }
+
+ // 计算角度
+ let angle = Math.atan2(relativeY, relativeX);
+ if(angle < 0) angle += Math.PI * 2;
+ // 调整角度,使其从12点钟方向开始
+ angle = (angle + Math.PI / 2) % (Math.PI * 2);
+
+ let platform, count, color;
+ if(angle <= data.ytAngle) {
+ // 圆通区域
+ platform = '圆通';
+ count = data.ytCount;
+ color = '#8b5cf6';
+ } else {
+ // 拼多多区域
+ platform = '拼多多';
+ count = data.pddCount;
+ color = '#f6af3dff';
+ }
+
+ const pct = ((count / data.total) * 100).toFixed(1);
+ donutTooltip.innerHTML = `
${platform}
数量: ${count.toLocaleString()}
占比: ${pct}%
`;
+ donutTooltip.style.display = 'block';
+
+ // 智能定位:使用页面坐标而非容器坐标
+ const mouseX = e.clientX - rect.left;
+ const mouseY = e.clientY - rect.top;
+
+ // 先显示提示框以获取实际尺寸
+ donutTooltip.style.visibility = 'hidden';
+ donutTooltip.style.display = 'block';
+ const tooltipWidth = donutTooltip.offsetWidth;
+ const tooltipHeight = donutTooltip.offsetHeight;
+ donutTooltip.style.visibility = 'visible';
+
+ // 使用页面绝对坐标进行计算
+ const mousePageX = e.clientX;
+ const mousePageY = e.clientY;
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ // 水平定位:默认右侧,页面右侧空间不足时显示在左侧
+ let tooltipPageX, tooltipPageY;
+
+ if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) {
+ // 页面右侧有足够空间,显示在右侧
+ tooltipPageX = mousePageX + 15;
+ } else {
+ // 页面右侧空间不足,显示在左侧
+ tooltipPageX = mousePageX - tooltipWidth - 15;
+ if(tooltipPageX < 10) tooltipPageX = 10; // 防止超出页面左边界
+ }
+
+ // 垂直定位:鼠标上方10px
+ tooltipPageY = mousePageY - 10;
+ if(tooltipPageY < 10) tooltipPageY = mousePageY + 15; // 如果上方空间不足,显示在下方
+ if(tooltipPageY + tooltipHeight > viewportHeight - 10) tooltipPageY = viewportHeight - tooltipHeight - 10;
+
+ // 转换回相对于容器的坐标
+ const containerRect = donutCanvas.parentElement.getBoundingClientRect();
+ const left = tooltipPageX - containerRect.left;
+ const top = tooltipPageY - containerRect.top;
+
+ donutTooltip.style.left = left + 'px';
+ donutTooltip.style.top = top + 'px';
+ };
+
+ donutCanvas.onmouseleave = () => {
+ donutTooltip.style.display = 'none';
};
}
+
+ const shipmentDonutCanvas = document.getElementById('shipment-donut-chart');
+ if(shipmentDonutCanvas) {
+ const shipmentTooltip = document.createElement('div');
+ shipmentTooltip.id = 'shipment-donut-tooltip';
+ shipmentTooltip.style.cssText = 'position:absolute;background:rgba(15,22,35,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);z-index:1000';
+ shipmentDonutCanvas.parentElement.appendChild(shipmentTooltip);
+
+ shipmentDonutCanvas.onmousemove = (e) => {
+ const data = shipmentDonutCanvas.__shipmentDonutData;
+ if(!data || !data.total || !data.segments || data.segments.length === 0) {
+ shipmentTooltip.style.display = 'none';
+ return;
+ }
+
+ const rect = shipmentDonutCanvas.getBoundingClientRect();
+ const relativeX = e.clientX - rect.left - data.centerX;
+ const relativeY = e.clientY - rect.top - data.centerY;
+ const distance = Math.sqrt(relativeX * relativeX + relativeY * relativeY);
+
+ const innerRadius = data.radius - data.lineWidth / 2;
+ const outerRadius = data.radius + data.lineWidth / 2;
+ if(distance < innerRadius || distance > outerRadius) {
+ shipmentTooltip.style.display = 'none';
+ return;
+ }
+
+ let angle = Math.atan2(relativeY, relativeX);
+ if(angle < 0) angle += Math.PI * 2;
+ angle = (angle + Math.PI / 2) % (Math.PI * 2);
+
+ const seg = (data.segments || []).find(s => angle >= s.start && angle <= s.end);
+ if(!seg) {
+ shipmentTooltip.style.display = 'none';
+ return;
+ }
+
+ const pct = ((seg.count / data.total) * 100).toFixed(1);
+ shipmentTooltip.innerHTML = `
${seg.label}
数量: ${seg.count.toLocaleString()}
占比: ${pct}%
`;
+ shipmentTooltip.style.display = 'block';
+
+ shipmentTooltip.style.visibility = 'hidden';
+ shipmentTooltip.style.display = 'block';
+ const tooltipWidth = shipmentTooltip.offsetWidth;
+ const tooltipHeight = shipmentTooltip.offsetHeight;
+ shipmentTooltip.style.visibility = 'visible';
+
+ const mousePageX = e.clientX;
+ const mousePageY = e.clientY;
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ let tooltipPageX, tooltipPageY;
+ if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) {
+ tooltipPageX = mousePageX + 15;
+ } else {
+ tooltipPageX = mousePageX - tooltipWidth - 15;
+ if(tooltipPageX < 10) tooltipPageX = 10;
+ }
+ tooltipPageY = mousePageY - 10;
+ if(tooltipPageY < 10) tooltipPageY = mousePageY + 15;
+ if(tooltipPageY + tooltipHeight > viewportHeight - 10) tooltipPageY = viewportHeight - tooltipHeight - 10;
+
+ const containerRect = shipmentDonutCanvas.parentElement.getBoundingClientRect();
+ const left = tooltipPageX - containerRect.left;
+ const top = tooltipPageY - containerRect.top;
+
+ shipmentTooltip.style.left = left + 'px';
+ shipmentTooltip.style.top = top + 'px';
+ };
+
+ shipmentDonutCanvas.onmouseleave = () => {
+ shipmentTooltip.style.display = 'none';
+ };
+ }
+
+ // 月度汇总功能
+ const showMonthlySummary = () => {
+ const pddData = window.__auditCache?.pdd || pdd.list || [];
+ const ytData = window.__auditCache?.yt || yt.list || [];
+ const now = new Date();
+ const thisMonth = now.toISOString().slice(0, 7);
+ const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().slice(0, 7);
+
+ // 按日期统计(去重MAC)
+ const countByDate = (list, monthPrefix) => {
+ const dateMap = {};
+ (list || []).forEach(r => {
+ if(r.ts_cn && r.mac && r.ts_cn.startsWith(monthPrefix)){
+ const date = r.ts_cn.split(' ')[0];
+ if(!dateMap[date]) dateMap[date] = new Set();
+ dateMap[date].add(r.mac);
+ }
+ });
+ return Object.entries(dateMap).map(([date, macs]) => ({date, count: macs.size})).sort((a,b) => a.date.localeCompare(b.date));
+ };
+
+ // 统计本月和上月
+ const pddThisMonth = countByDate(pddData, thisMonth);
+ const ytThisMonth = countByDate(ytData, thisMonth);
+ const pddLastMonth = countByDate(pddData, lastMonth);
+ const ytLastMonth = countByDate(ytData, lastMonth);
+
+ const sumCount = (arr) => arr.reduce((s, x) => s + x.count, 0);
+ const pddThisTotal = sumCount(pddThisMonth);
+ const ytThisTotal = sumCount(ytThisMonth);
+ const pddLastTotal = sumCount(pddLastMonth);
+ const ytLastTotal = sumCount(ytLastMonth);
+ const thisTotal = pddThisTotal + ytThisTotal;
+ const lastTotal = pddLastTotal + ytLastTotal;
+
+ // 计算环比增长
+ const calcGrowth = (curr, prev) => {
+ if(prev === 0) return curr > 0 ? '+100%' : '0%';
+ const pct = ((curr - prev) / prev * 100).toFixed(1);
+ return pct >= 0 ? '+' + pct + '%' : pct + '%';
+ };
+
+ // 生成日期明细表格
+ const generateTable = (pddArr, ytArr) => {
+ const allDates = [...new Set([...pddArr.map(x=>x.date), ...ytArr.map(x=>x.date)])].sort();
+ if(allDates.length === 0) return '
暂无数据
';
+
+ const pddMap = Object.fromEntries(pddArr.map(x => [x.date, x.count]));
+ const ytMap = Object.fromEntries(ytArr.map(x => [x.date, x.count]));
+
+ let rows = allDates.map(date => {
+ const pdd = pddMap[date] || 0;
+ const yt = ytMap[date] || 0;
+ return `
| ${date.slice(5)} | ${pdd} | ${yt} | ${pdd+yt} |
`;
+ }).join('');
+
+ return `
`;
+ };
+
+ // 创建模态框
+ 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() => {
@@ -789,7 +1432,7 @@ const Dashboard = (() => {
const isPdd = newPlatform === 'pdd';
const value = isPdd ? (dashRes.todayPdd || 0) : (dashRes.todayYt || 0);
const platformName = isPdd ? '拼多多' : '圆通';
- const color = isPdd ? '#e02e24' : '#1a6dd6';
+ const color = isPdd ? '#f6af3dff' : '#1a6dd6';
const fillWidth = Math.min((value / 10000) * 100, 100);
// 如果平台切换了,更新图标
@@ -823,21 +1466,14 @@ const Dashboard = (() => {
return;
}
- // 缓存数据(只保留最近30天的数据以节省内存)
- const filterRecent30Days = (list) => {
- const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
- return (list || []).filter(r => {
- if(!r.ts_cn) return false;
- const t = toEpoch(r.ts_cn);
- return t && t >= cutoff;
- });
- };
+ // 缓存全部数据(不再限制30天,支持大数据量显示)
+ window.__auditCache.pdd = pddRes.list || [];
+ window.__auditCache.yt = ytRes.list || [];
- window.__auditCache.pdd = filterRecent30Days(pddRes.list);
- window.__auditCache.yt = filterRecent30Days(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);
@@ -885,28 +1521,110 @@ const Dashboard = (() => {
},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')}
-
-
- 审计趋势
-
-
-
-
-
+
审计看板
@@ -919,14 +1637,14 @@ const Dashboard = (() => {
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提示:留空表示不限制该日期,可以导出全部数据
+
+
+
+
+
+
+