From d9ec15b72fd7b04369af35ba55787681c07ad758 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Fri, 28 Aug 2026 16:17:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E6=95=88=E8=83=BD=E5=88=86=E6=9E=90):?= =?UTF-8?q?=20=E4=BA=BA=E5=91=98=E8=A7=86=E5=9B=BE=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=87=E7=89=87=E7=80=91=E5=B8=83=E6=B5=81?= =?UTF-8?q?=EF=BC=8C=E5=91=8A=E5=88=AB=E6=A8=AA=E5=90=91=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设备按 CHUNK_SIZE=6 切块,向下追加渲染多个 ECharts 图(垂直瀑布流) - capabilityOption 改为 getCapabilityOption 工厂函数,每块独立构建 Option - 去除横向 overflow 与分页器,网页原生向下延伸 - grid.top 加大到 80 避免图例遮挡,X 轴标签简化(序列号+身份证) - 点击柱子下钻支持块内索引→全局设备映射,Tooltip 读取对应块数据 --- .../src/pages/admin/AnalyticsDashboard.tsx | 121 ++++++++---------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/frontend/src/pages/admin/AnalyticsDashboard.tsx b/frontend/src/pages/admin/AnalyticsDashboard.tsx index 06f9e59..05c4c89 100644 --- a/frontend/src/pages/admin/AnalyticsDashboard.tsx +++ b/frontend/src/pages/admin/AnalyticsDashboard.tsx @@ -59,7 +59,7 @@ function alignName(name: string) { return name.length === 2 ? name[0] + " " + name[1] : name; } -const CAP_PAGE_SIZE = 20; // 人员视图每页设备数 +const CHUNK_SIZE = 6; // 人员视图瀑布流每块设备数 export default function AnalyticsDashboard() { const [searchParams, setSearchParams] = useSearchParams(); @@ -92,8 +92,6 @@ export default function AnalyticsDashboard() { const [flowLoading, setFlowLoading] = useState(false); // 📅 设备生产总天数口径:自然天 / 工作日 const [totalDaysMode, setTotalDaysMode] = useState<"natural" | "workdays">("natural"); - // 📄 人员视图分页 - const [capPage, setCapPage] = useState(0); const [error, setError] = useState(null); // ─── 弹窗下钻:设备备注(all=true 表示流转图点击,展示全部) ── @@ -202,12 +200,13 @@ export default function AnalyticsDashboard() { }, [recordQuery, assigneeIds]); // 能力图谱:点柱子 → 按当前人员筛选过滤备注 - const handleZrClick = useCallback((chart: any, e: any) => { + const handleZrClick = useCallback((chart: any, e: any, chunkStart = 0) => { const coord = chart.convertFromPixel({ seriesIndex: 0 }, [e.offsetX, e.offsetY]); if (!coord || coord.length < 1) return; const dataIndex = Math.round(coord[0]); - if (dataIndex < 0 || dataIndex >= categoriesRef.current.length) return; - const sn = categoriesRef.current[dataIndex]; + const globalIdx = chunkStart + dataIndex; // 瀑布流块内索引 → 全局设备索引 + if (globalIdx < 0 || globalIdx >= categoriesRef.current.length) return; + const sn = categoriesRef.current[globalIdx]; if (sn) setRecordQuery({ sn, all: false }); }, []); @@ -228,22 +227,32 @@ export default function AnalyticsDashboard() { setProductSns([]); }; - // ─── 柱状图:人员视图(X=设备身份证,系列=人员,居中紧凑,消除幽灵占位) ── - const capabilityOption = useMemo(() => { - const allCategories = capability?.categories ?? []; - const allDevices = capability?.devices ?? []; - const allSeries = capability?.series ?? []; - // 🔧 分页切片:每页只显示 CAP_PAGE_SIZE 台设备,避免超宽无滚动 - const start = capPage * CAP_PAGE_SIZE; - const end = Math.min(start + CAP_PAGE_SIZE, allCategories.length); - const categories = allCategories.slice(start, end); - const devices = allDevices.slice(start, end); - const series = allSeries.map((s) => ({ ...s, data: s.data.slice(start, end) })); - // 每台设备上真正产生耗时的人员 seriesIndex 数组 - const activePerDevice = devices.map((_, devIdx) => - series + // ─── 人员视图:设备按 CHUNK_SIZE 切块(瀑布流,向下追加渲染) ── + const capabilityChunks = useMemo(() => { + if (!capability || !capability.devices?.length) return []; + const chunks = []; + const total = capability.devices.length; + for (let i = 0; i < total; i += CHUNK_SIZE) { + chunks.push({ + start: i, // 该块在全部设备中的起始索引(点击下钻用) + devices: capability.devices.slice(i, i + CHUNK_SIZE), + categories: capability.categories.slice(i, i + CHUNK_SIZE), + series: capability.series.map(s => ({ + name: s.name, + data: s.data.slice(i, i + CHUNK_SIZE), + })), + }); + } + return chunks; + }, [capability]); + + // ─── 单个块(6台设备)的 ECharts Option 工厂函数(无横向滚动,向下追加) ── + const getCapabilityOption = (chunkDevices: any[], chunkCategories: string[], chunkSeries: any[]): EChartsCoreOption => { + // 该块内每台设备上真正产生耗时的人员 seriesIndex + const activePerDevice = chunkDevices.map((_, devIdx) => + chunkSeries .map((s, sIdx) => (s.data[devIdx] && s.data[devIdx].value != null ? sIdx : -1)) - .filter((idx) => idx !== -1), + .filter(idx => idx !== -1), ); return { tooltip: { @@ -253,8 +262,8 @@ export default function AnalyticsDashboard() { formatter: (p: any) => { const items = Array.isArray(p) ? p : [p]; const idx = items[0]?.dataIndex ?? 0; - const sn = categories[idx] ?? "—"; - const dev = devices[idx]; + const sn = chunkCategories[idx] ?? "—"; + const dev = chunkDevices[idx]; const ext = dev?.external_serial; const rows = items .filter((it: any) => (it.data?.status ?? "—") !== "—") @@ -278,16 +287,14 @@ export default function AnalyticsDashboard() { }, }, legend: { top: 0, type: "scroll" }, - grid: { left: 48, right: 24, top: 60, bottom: 96 }, + grid: { left: 48, right: 24, top: 80, bottom: 48 }, // top 加大,避免图例遮挡柱子 xAxis: { type: "category", - data: devices.map((d) => - [d.material_name, d.spec_model, d.external_serial, d.product_sn].filter(Boolean).join("\n"), - ), - axisLabel: { fontSize: 10, interval: 0, lineHeight: 13 }, + data: chunkDevices.map((d) => (d.external_serial ? `${d.external_serial}\n${d.product_sn}` : d.product_sn)), + axisLabel: { fontSize: 11, interval: 0, color: "#666", lineHeight: 14 }, }, yAxis: { type: "value", name: "耗时(小时)" }, - series: series.map((s, sIdx) => ({ + series: chunkSeries.map((s, sIdx) => ({ name: s.name, type: "custom", encode: { x: 0, y: 1 }, @@ -306,44 +313,28 @@ export default function AnalyticsDashboard() { const localIndex = activeSeries.indexOf(sIdx); if (localIndex === -1) return; - const barWidth = 24; // 黄金粗细,绝不妥协 - const gap = 6; // 紧凑的柱间距 + const barWidth = 24; + const gap = 6; const totalWidth = activeSeries.length * barWidth + (activeSeries.length - 1) * gap; - - // 核心:彻底消除幽灵占位,让存活的柱子绝对居中对齐 const centerX = api.coord([devIdx, 0])[0]; const x = centerX - totalWidth / 2 + localIndex * (barWidth + gap); - const valY = api.coord([devIdx, val])[1]; const y0 = api.coord([devIdx, 0])[1]; - const height = Math.max(y0 - valY, 3); // 至少 3px,0 值/极小值也有柱子 + const height = Math.max(y0 - valY, 3); const y = y0 - height; - return { type: "rect", shape: { x, y, width: barWidth, height, r: [3, 3, 0, 0] }, style: api.style(), }; }, - data: s.data.map((d, i) => { + data: s.data.map((d: any, i: number) => { if (d.value == null) return null; - return { - ...d, - value: [i, d.value], // custom 必须的 [x, y] 坐标格式 - actualValue: d.value, // 供 tooltip 读取的真实值 - }; + return { ...d, value: [i, d.value], actualValue: d.value }; }), })), }; - }, [capability, capPage]); - - // 分页信息 - const capTotalPages = capability ? Math.max(1, Math.ceil(capability.devices.length / CAP_PAGE_SIZE)) : 0; - const capViewCount = capability - ? Math.min(CAP_PAGE_SIZE, Math.max(0, capability.devices.length - capPage * CAP_PAGE_SIZE)) - : 0; - // 查询/筛选变化后回到第 1 页 - useEffect(() => { setCapPage(0); }, [capability]); + }; // ─── 有数据的日期集合(用于日历蓝点) ── const activityDates = useMemo(() => { @@ -782,22 +773,18 @@ export default function AnalyticsDashboard() { /> ) : ( -
-
- -
-
- )} - {/* 分页器:设备多时分页查看,避免超宽无滚动 */} - {capTotalPages > 1 && ( -
- - {capPage + 1} / {capTotalPages} 页 · 每页 {CAP_PAGE_SIZE} 台 - + /* 🔧 瀑布流:设备按 CHUNK_SIZE 切块向下追加渲染,告别横向滚动 */ +
+ {capabilityChunks.map((chunk, idx) => ( +
+ {idx > 0 &&
} + handleZrClick(chart, e, chunk.start)} + /> +
+ ))}
)}