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