634 lines
28 KiB
JavaScript
634 lines
28 KiB
JavaScript
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 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.__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;
|
||
}
|
||
|
||
// 动态计算审计框高度
|
||
const calculateAuditHeight = () => {
|
||
const viewEl = document.getElementById('view');
|
||
if(!viewEl) return 460;
|
||
|
||
const viewHeight = viewEl.clientHeight;
|
||
const viewPadding = 40; // view的padding (20px * 2)
|
||
|
||
// 获取其他元素的高度
|
||
const metricsEl = document.querySelector('.dashboard-metrics-4col');
|
||
const trendEl = document.querySelector('.card');
|
||
|
||
const metricsHeight = metricsEl ? metricsEl.offsetHeight : 60;
|
||
const trendHeight = trendEl ? trendEl.offsetHeight : 260;
|
||
const gaps = 24; // margin-top间距 (12px * 2)
|
||
|
||
// 计算剩余高度
|
||
const remainingHeight = viewHeight - viewPadding - metricsHeight - trendHeight - gaps;
|
||
|
||
// 最小高度300px,最大不超过剩余空间
|
||
return Math.max(300, Math.min(remainingHeight, 460));
|
||
};
|
||
|
||
// 设置审计框高度
|
||
const setAuditHeight = () => {
|
||
const height = calculateAuditHeight();
|
||
const auditCard = document.getElementById('audit-card');
|
||
const pddCard = document.getElementById('audit-pdd-card');
|
||
const ytCard = document.getElementById('audit-yt-card');
|
||
if(auditCard) auditCard.style.height = height + 'px';
|
||
if(pddCard) pddCard.style.height = height + 'px';
|
||
if(ytCard) ytCard.style.height = height + 'px';
|
||
};
|
||
|
||
// 初始设置高度
|
||
setAuditHeight();
|
||
|
||
// 监听窗口大小变化
|
||
if(window.__dashboardResizeHandler){
|
||
window.removeEventListener('resize', window.__dashboardResizeHandler);
|
||
}
|
||
window.__dashboardResizeHandler = () => setAuditHeight();
|
||
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 ? '#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);
|
||
};
|
||
|
||
// 初始化缓存数据
|
||
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;
|
||
|
||
// 添加鼠标悬停事件
|
||
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 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 listHtml = filteredData.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>';
|
||
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] = 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;
|
||
|
||
// 更新整合看板
|
||
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 listHtml = filteredData.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 auditListEl = document.getElementById('audit-list');
|
||
if (auditListEl) auditListEl.innerHTML = listHtml;
|
||
};
|
||
updateAuditDisplay();
|
||
} catch(e) {
|
||
clearTimeout(timeoutId);
|
||
if(e.name !== 'AbortError'){
|
||
console.error('更新审计数据失败:', e);
|
||
}
|
||
} finally {
|
||
window.__auditBusy=false;
|
||
window.__auditAbortController = null;
|
||
}
|
||
};
|
||
|
||
// 刷新间隔10秒
|
||
window.__auditTimer=setInterval(refreshAll, 10000);
|
||
},0);
|
||
return `
|
||
<div>
|
||
<div class="dashboard-metrics-4col">
|
||
${metricsCard('直通良品率', data.fpyRate || '—', 'success')}
|
||
${metricsCard('良品率', data.goodRate, 'success')}
|
||
${metricsCard('发货数量', data.shipments, 'warning')}
|
||
${metricsCard('不良数量', data.badCount ?? data.defects, 'danger')}
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<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>
|
||
<div style="margin-top:12px">
|
||
<div id="audit-card" class="card" style="display:flex;flex-direction:column;height:460px">
|
||
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:8px">
|
||
<span>审计看板</span>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<select id="audit-platform-select" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;cursor:pointer">
|
||
<option value="pdd">拼多多</option>
|
||
<option value="yt">圆通</option>
|
||
</select>
|
||
<input id="audit-date" 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>
|
||
</div>
|
||
<ul id="audit-list" class="list" style="overflow-y:auto;flex:1;min-height:0">${pddList}</ul>
|
||
</div>
|
||
<div id="audit-pdd-card" class="card" style="display:none;flex-direction:column;height:460px">
|
||
<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>
|
||
<div id="audit-yt-card" class="card" style="display:none;flex-direction:column;height:460px">
|
||
<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);
|
||
})(); |