ERP/frontend/js/components/dashboard.js

634 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
})();