仪表盘界面优化
This commit is contained in:
parent
bd27a69944
commit
819ac1cdaa
@ -698,7 +698,7 @@ const Dashboard = (() => {
|
||||
// 绘制网格线(增强版 - 虚线样式)
|
||||
ctx.strokeStyle = colors.grid;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.setLineDash([4, 4]);
|
||||
for(let i = 0; i <= 4; i++){
|
||||
const y = padding.top + (chartHeight / 4) * i;
|
||||
@ -1001,12 +1001,32 @@ const Dashboard = (() => {
|
||||
const tooltipWidth = tooltip.offsetWidth || 100;
|
||||
tooltip.style.left = (e.clientX - rect.left - tooltipWidth - 10) + 'px';
|
||||
tooltip.style.top = (e.clientY - rect.top - 10) + 'px';
|
||||
|
||||
// 绘制悬停竖虚线(先重绘图表再叠加)
|
||||
if(window.__redrawTrendChart) window.__redrawTrendChart();
|
||||
const hoverX = padding.left + (chartWidth / (timePoints.length - 1)) * nearestIndex;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const hoverCtx = canvas.getContext('2d');
|
||||
hoverCtx.save();
|
||||
hoverCtx.strokeStyle = 'rgba(148,163,184,0.7)';
|
||||
hoverCtx.lineWidth = 1;
|
||||
hoverCtx.setLineDash([4, 4]);
|
||||
hoverCtx.globalAlpha = 1;
|
||||
hoverCtx.beginPath();
|
||||
hoverCtx.moveTo(hoverX, padding.top);
|
||||
hoverCtx.lineTo(hoverX, padding.top + chartHeight);
|
||||
hoverCtx.stroke();
|
||||
hoverCtx.setLineDash([]);
|
||||
hoverCtx.restore();
|
||||
} else {
|
||||
tooltip.style.display = 'none';
|
||||
// 移除悬停线,重绘图表
|
||||
if(window.__redrawTrendChart) window.__redrawTrendChart();
|
||||
}
|
||||
};
|
||||
canvas.onmouseleave = () => {
|
||||
tooltip.style.display = 'none';
|
||||
if(window.__redrawTrendChart) window.__redrawTrendChart();
|
||||
};
|
||||
}
|
||||
|
||||
@ -1259,12 +1279,13 @@ const Dashboard = (() => {
|
||||
};
|
||||
};
|
||||
|
||||
const drawShipmentDonutChart = async (stats) => {
|
||||
// 纯同步绘制函数,不做任何 fetch
|
||||
const renderShipmentDonut = (segments, total, activeIdx) => {
|
||||
const canvas = document.getElementById('shipment-donut-chart');
|
||||
if(!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const size = 160;
|
||||
const size = 280; // 足够容纳引出线
|
||||
canvas.width = size * dpr;
|
||||
canvas.height = size * dpr;
|
||||
canvas.style.width = size + 'px';
|
||||
@ -1272,147 +1293,184 @@ const Dashboard = (() => {
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const legendEl = document.getElementById('shipment-donut-legend');
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const outerR = 82;
|
||||
const innerR = 56;
|
||||
const ringW = outerR - innerR;
|
||||
const paddingAngle = 8 * Math.PI / 180;
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
|| document.body.classList.contains('dark');
|
||||
const textColor = isDark ? '#f1f5f9' : '#1e293b';
|
||||
const subColor = '#94a3b8';
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
if(total === 0 || segments.length === 0) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, outerR, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = isDark ? '#334155' : '#e2e8f0';
|
||||
ctx.lineWidth = ringW;
|
||||
ctx.stroke();
|
||||
if(legendEl) legendEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算各段角度
|
||||
const availableAngle = Math.PI * 2 - paddingAngle * segments.length;
|
||||
let angle = -Math.PI / 2;
|
||||
segments.forEach((seg) => {
|
||||
const span = (seg.count / total) * availableAngle;
|
||||
seg.startAngle = angle;
|
||||
seg.endAngle = angle + span;
|
||||
seg.midAngle = angle + span / 2;
|
||||
angle = seg.endAngle + paddingAngle;
|
||||
});
|
||||
|
||||
// 第一遍:绘制所有扇段主环
|
||||
segments.forEach((seg, i) => {
|
||||
const isActive = i === activeIdx;
|
||||
const span = seg.endAngle - seg.startAngle;
|
||||
if(span < 0.01) return;
|
||||
const curOuterR = isActive ? outerR + 7 : outerR;
|
||||
const curRingW = isActive ? ringW + 5 : ringW;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, curOuterR, seg.startAngle, seg.endAngle);
|
||||
ctx.strokeStyle = seg.color;
|
||||
ctx.lineWidth = curRingW;
|
||||
ctx.lineCap = 'butt';
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// 第二遍:在所有主环上方绘制激活项外细圈,确保不被遮盖
|
||||
if(activeIdx >= 0 && activeIdx < segments.length) {
|
||||
const activeSeg = segments[activeIdx];
|
||||
const span = activeSeg.endAngle - activeSeg.startAngle;
|
||||
if(span >= 0.01) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, outerR + 32, activeSeg.startAngle, activeSeg.endAngle);
|
||||
ctx.strokeStyle = activeSeg.color;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'butt';
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// 激活项引出线
|
||||
if(activeIdx >= 0 && activeIdx < segments.length) {
|
||||
const seg = segments[activeIdx];
|
||||
const mid = seg.midAngle;
|
||||
const sinM = Math.sin(mid);
|
||||
const cosM = Math.cos(mid);
|
||||
const sx = cx + (outerR + 36) * cosM;
|
||||
const sy = cy + (outerR + 36) * sinM;
|
||||
const mx2 = cx + (outerR + 50) * cosM;
|
||||
const my2 = cy + (outerR + 50) * sinM;
|
||||
const lineLen = 24;
|
||||
const ex = mx2 + (cosM >= 0 ? 1 : -1) * lineLen;
|
||||
const ey = my2;
|
||||
const textX = ex + (cosM >= 0 ? 7 : -7);
|
||||
const anchor = cosM >= 0 ? 'left' : 'right';
|
||||
const pct = ((seg.count / total) * 100).toFixed(1);
|
||||
|
||||
ctx.strokeStyle = seg.color;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(mx2, my2);
|
||||
ctx.lineTo(ex, ey);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = seg.color;
|
||||
ctx.fill();
|
||||
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = anchor;
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(seg.count.toLocaleString(), textX, ey - 9);
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillStyle = subColor;
|
||||
ctx.fillText(`(${pct}%)`, textX, ey + 6);
|
||||
}
|
||||
|
||||
// 中心文字:默认空白,悬停时显示百分比+名称+数量
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
if(activeIdx >= 0 && activeIdx < segments.length) {
|
||||
const seg = segments[activeIdx];
|
||||
const pct = ((seg.count / total) * 100).toFixed(1);
|
||||
ctx.font = 'bold 20px sans-serif';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(`${pct}%`, cx, cy - 14);
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.fillStyle = seg.color;
|
||||
ctx.fillText(seg.label, cx, cy + 4);
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.fillStyle = subColor;
|
||||
ctx.fillText(seg.count.toLocaleString(), cx, cy + 20);
|
||||
}
|
||||
|
||||
// 更新图例
|
||||
if(legendEl) {
|
||||
legendEl.innerHTML = segments.map((seg, i) => {
|
||||
const pct = total > 0 ? ((seg.count / total) * 100).toFixed(1) : '0.0';
|
||||
const isActive = i === activeIdx;
|
||||
return `<div data-seg-idx="${i}" style="text-align:center;cursor:pointer;padding:4px 6px;border-radius:8px;background:${isActive ? 'rgba(255,255,255,0.06)' : 'transparent'}"><div style="font-size:15px;font-weight:700;color:${seg.color}">${pct}%</div><div style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px;justify-content:center"><span style="width:7px;height:7px;border-radius:50%;background:${seg.color};${isActive ? `box-shadow:0 0 8px ${seg.color}99` : ''}"></span>${seg.label}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
canvas.__shipmentDonutData = { total, cx, cy, outerR, innerR, segments };
|
||||
};
|
||||
|
||||
// 异步初始化:fetch 数据后缓存 segments,之后重绘只调用 renderShipmentDonut
|
||||
const drawShipmentDonutChart = async (stats) => {
|
||||
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));
|
||||
|
||||
// 获取机种列表以映射自定义机种名称
|
||||
let platformList = [];
|
||||
try {
|
||||
const platformRes = await fetch('/api/shipments/platforms', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
}).then(r => r.ok ? r.json() : null);
|
||||
if(platformRes && platformRes.list) {
|
||||
platformList = platformRes.list;
|
||||
}
|
||||
} catch(e) {
|
||||
// 忽略错误,使用默认映射
|
||||
if(!window.__shipmentPlatformList) {
|
||||
try {
|
||||
const res = await fetch('/api/shipments/platforms', { credentials: 'include' }).then(r => r.ok ? r.json() : null);
|
||||
window.__shipmentPlatformList = (res && res.list) ? res.list : [];
|
||||
} catch(e) { window.__shipmentPlatformList = []; }
|
||||
}
|
||||
const platformList = window.__shipmentPlatformList;
|
||||
|
||||
const platformName = (k) => {
|
||||
const map = {
|
||||
pdd: '拼多多',
|
||||
yt: '圆通',
|
||||
tx: '兔喜',
|
||||
mt: '美团',
|
||||
drf: '大润发',
|
||||
std: '标准版',
|
||||
unknown: '未知'
|
||||
};
|
||||
// 先从固定映射查找
|
||||
const map = { pdd:'拼多多', yt:'圆通', tx:'兔喜', mt:'美团', drf:'大润发', std:'标准版', unknown:'未知' };
|
||||
if(map[k]) return map[k];
|
||||
// 再从机种列表查找自定义机种名称
|
||||
const customPlatform = platformList.find(p => p.value === k);
|
||||
if(customPlatform) return customPlatform.label;
|
||||
// 最后返回原始值
|
||||
return k;
|
||||
const cp = platformList.find(p => p.value === k);
|
||||
return cp ? cp.label : k;
|
||||
};
|
||||
const platformColor = (k, i) => {
|
||||
const fixed = { pdd:'#3B82F6', yt:'#10B981', tx:'#F97316', mt:'#F59E0B' };
|
||||
if(fixed[k]) return fixed[k];
|
||||
return ['#06b6d4','#a3e635','#ef4444','#8b5cf6'][i % 4];
|
||||
};
|
||||
|
||||
const platformColor = (k, fallbackIndex) => {
|
||||
const fixed = {
|
||||
pdd: '#3B82F6',
|
||||
yt: '#10B981',
|
||||
tx: '#F97316'
|
||||
};
|
||||
if(fixed[k]) return fixed[k];
|
||||
const palette = ['#10b981', '#06b6d4', '#f97316', '#a3e635', '#ef4444', '#f59e0b'];
|
||||
return palette[fallbackIndex % palette.length];
|
||||
};
|
||||
const segments = items.map(([k, v], i) => ({
|
||||
key: k,
|
||||
label: platformName(k),
|
||||
count: v || 0,
|
||||
color: platformColor(k, i)
|
||||
key: k, label: platformName(k), count: v || 0, color: platformColor(k, i)
|
||||
}));
|
||||
|
||||
const centerX = size / 2;
|
||||
const centerY = size / 2;
|
||||
const outerRadius = 50;
|
||||
const innerRadius = 35;
|
||||
const lineWidth = outerRadius - innerRadius;
|
||||
// 缓存 segments 供后续同步重绘
|
||||
window.__shipmentSegments = segments;
|
||||
window.__shipmentTotal = total;
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
if(total === 0 || segments.length === 0) {
|
||||
// 绘制灰色圆环
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#e5e7eb';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.stroke();
|
||||
|
||||
// 中心显示0
|
||||
ctx.fillStyle = '#1e293b';
|
||||
ctx.font = 'bold 18px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('0', centerX, centerY - 3);
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillStyle = '#94a3b8';
|
||||
ctx.fillText('总发货', centerX, centerY + 10);
|
||||
|
||||
if(legendEl) legendEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算角度和间隙
|
||||
const paddingAngle = 5 * (Math.PI / 180); // 5度转弧度
|
||||
const segmentCount = segments.length;
|
||||
const totalPadding = paddingAngle * segmentCount;
|
||||
const availableAngle = Math.PI * 2 - totalPadding;
|
||||
|
||||
let currentAngle = -Math.PI / 2; // 从12点钟方向开始
|
||||
segments.forEach((seg, i) => {
|
||||
const portion = total > 0 ? (seg.count / total) * availableAngle : 0;
|
||||
|
||||
const start = currentAngle;
|
||||
const end = start + portion;
|
||||
|
||||
// 保存原始角度信息
|
||||
seg.originalStart = start;
|
||||
seg.originalEnd = end;
|
||||
|
||||
// 只绘制有意义的角度
|
||||
if(portion > 0.01) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, outerRadius, start, end);
|
||||
ctx.strokeStyle = seg.color;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'butt';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 下一个扇段的起始位置 = 当前扇段结束位置 + 间隙
|
||||
currentAngle = end + paddingAngle;
|
||||
});
|
||||
|
||||
// 中心显示总数(优化位置和大小)
|
||||
ctx.fillStyle = '#1e293b';
|
||||
ctx.font = 'bold 18px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(total.toLocaleString(), centerX, centerY - 3);
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillStyle = '#94a3b8';
|
||||
ctx.fillText('总发货', centerX, centerY + 10);
|
||||
|
||||
if(legendEl) {
|
||||
legendEl.innerHTML = segments.map(seg => {
|
||||
const pct = total > 0 ? ((seg.count / total) * 100).toFixed(1) : '0.0';
|
||||
return `<div style="text-align:center"><div style="font-size:16px;font-weight:700;color:${seg.color}">${pct}%</div><div style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px"><span style="width:8px;height:8px;border-radius:50%;background:${seg.color}"></span>${seg.label}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
canvas.__shipmentDonutData = { total, centerX, centerY, outerRadius, innerRadius, lineWidth, segments };
|
||||
renderShipmentDonut(segments, total, -1);
|
||||
};
|
||||
|
||||
const refreshShipmentPlatformStats = async(force=false) => {
|
||||
window.__shipmentStatsTs = window.__shipmentStatsTs || 0;
|
||||
const now = Date.now();
|
||||
if(!force && (now - window.__shipmentStatsTs) < 60000 && window.__shipmentStatsCache) {
|
||||
await drawShipmentDonutChart(window.__shipmentStatsCache);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -1535,99 +1593,47 @@ const Dashboard = (() => {
|
||||
|
||||
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);
|
||||
let currentActiveIdx = -1;
|
||||
|
||||
shipmentDonutCanvas.onmousemove = (e) => {
|
||||
const data = shipmentDonutCanvas.__shipmentDonutData;
|
||||
if(!data || !data.total || !data.segments || data.segments.length === 0) {
|
||||
shipmentTooltip.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
const segs = window.__shipmentSegments;
|
||||
const tot = window.__shipmentTotal;
|
||||
if(!segs || segs.length === 0 || !tot) 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 scaleX = shipmentDonutCanvas.offsetWidth > 0 ? (280 / shipmentDonutCanvas.offsetWidth) : 1;
|
||||
const scaleY = shipmentDonutCanvas.offsetHeight > 0 ? (280 / shipmentDonutCanvas.offsetHeight) : 1;
|
||||
const rx = (e.clientX - rect.left) * scaleX - 140;
|
||||
const ry = (e.clientY - rect.top) * scaleY - 140;
|
||||
const dist = Math.sqrt(rx * rx + ry * ry);
|
||||
|
||||
// 使用新的内外半径
|
||||
if(distance < data.innerRadius || distance > data.outerRadius) {
|
||||
shipmentTooltip.style.display = 'none';
|
||||
if(dist < 56 || dist > 100) {
|
||||
if(currentActiveIdx !== -1) { currentActiveIdx = -1; renderShipmentDonut(segs, tot, -1); }
|
||||
return;
|
||||
}
|
||||
|
||||
let angle = Math.atan2(relativeY, relativeX);
|
||||
if(angle < 0) angle += Math.PI * 2;
|
||||
|
||||
// 查找对应的扇段
|
||||
const seg = (data.segments || []).find(s => {
|
||||
// 将扇段角度转换到 0-2π 范围
|
||||
let segStart = s.originalStart;
|
||||
let segEnd = s.originalEnd;
|
||||
|
||||
// 如果角度是负数,转换到 0-2π 范围
|
||||
if(segStart < 0) segStart += Math.PI * 2;
|
||||
if(segEnd < 0) segEnd += Math.PI * 2;
|
||||
|
||||
// 处理跨越0度的情况
|
||||
if(segStart > segEnd) {
|
||||
// 扇段跨越0度(例如从350度到10度)
|
||||
return angle >= segStart || angle <= segEnd;
|
||||
} else {
|
||||
// 正常情况
|
||||
return angle >= segStart && angle <= segEnd;
|
||||
}
|
||||
let angle = Math.atan2(ry, rx);
|
||||
if(angle < -Math.PI / 2) angle += Math.PI * 2;
|
||||
|
||||
let foundIdx = -1;
|
||||
segs.forEach((seg, i) => {
|
||||
let s = seg.startAngle, en = seg.endAngle;
|
||||
if(s < -Math.PI / 2) { s += Math.PI * 2; en += Math.PI * 2; }
|
||||
let a = angle;
|
||||
if(a < s) a += Math.PI * 2;
|
||||
if(a >= s && a <= en) foundIdx = i;
|
||||
});
|
||||
|
||||
if(!seg) {
|
||||
shipmentTooltip.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const pct = ((seg.count / data.total) * 100).toFixed(1);
|
||||
shipmentTooltip.innerHTML = `<div style="color:${seg.color};font-weight:600">${seg.label}</div><div>数量: ${seg.count.toLocaleString()}</div><div>占比: ${pct}%</div>`;
|
||||
shipmentTooltip.style.display = 'block';
|
||||
|
||||
shipmentTooltip.style.visibility = 'hidden';
|
||||
shipmentTooltip.style.display = 'block';
|
||||
const tooltipWidth = shipmentTooltip.offsetWidth;
|
||||
const tooltipHeight = shipmentTooltip.offsetHeight;
|
||||
shipmentTooltip.style.visibility = 'visible';
|
||||
|
||||
// 智能定位
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
const mousePageX = e.clientX;
|
||||
const mousePageY = e.clientY;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let tooltipPageX, tooltipPageY;
|
||||
|
||||
if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) {
|
||||
tooltipPageX = mousePageX + 15;
|
||||
} else {
|
||||
tooltipPageX = mousePageX - tooltipWidth - 15;
|
||||
if(tooltipPageX < 10) tooltipPageX = 10;
|
||||
if(foundIdx !== currentActiveIdx) {
|
||||
currentActiveIdx = foundIdx;
|
||||
renderShipmentDonut(segs, tot, foundIdx);
|
||||
}
|
||||
|
||||
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 segs = window.__shipmentSegments;
|
||||
const tot = window.__shipmentTotal;
|
||||
if(currentActiveIdx !== -1) { currentActiveIdx = -1; if(segs && tot) renderShipmentDonut(segs, tot, -1); }
|
||||
};
|
||||
}
|
||||
|
||||
@ -2237,36 +2243,11 @@ const Dashboard = (() => {
|
||||
<!-- 右侧环形图卡片 -->
|
||||
<div class="card" style="flex:1;padding:20px;border-radius:16px;display:flex;flex-direction:column;background:var(--surface);min-height:340px">
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:12px">产量占比</div>
|
||||
<div style="flex:1;display:grid;grid-template-columns:repeat(2,1fr);gap:16px;align-items:start;align-content:center">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:flex-start;position:relative;min-width:0">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">审计(本月)</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;position:relative;height:160px;width:160px;flex-shrink:0">
|
||||
<canvas id="donut-chart" style="width:160px;height:160px"></canvas>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:12px;width:100%">
|
||||
<div style="text-align:center">
|
||||
<div id="donut-pdd-pct" style="font-size:20px;font-weight:700;color:#3B82F6">0%</div>
|
||||
<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px">
|
||||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#3B82F6;flex-shrink:0"></span>
|
||||
<span>拼多多</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div id="donut-yt-pct" style="font-size:20px;font-weight:700;color:#10B981">0%</div>
|
||||
<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px">
|
||||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#10B981;flex-shrink:0"></span>
|
||||
<span>圆通</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:flex-start;position:relative;min-width:0">
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">发货(按机种)</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;position:relative;height:160px;width:160px;flex-shrink:0">
|
||||
<canvas id="shipment-donut-chart" style="width:160px;height:160px"></canvas>
|
||||
</div>
|
||||
<div id="shipment-donut-legend" style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:12px;width:100%"></div>
|
||||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center">
|
||||
<div style="display:flex;align-items:center;justify-content:center;position:relative;height:280px;width:280px;flex-shrink:0">
|
||||
<canvas id="shipment-donut-chart" style="width:280px;height:280px"></canvas>
|
||||
</div>
|
||||
<div id="shipment-donut-legend" style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:12px;width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user