/** * WIP 分布矩阵 — 在制品透视表(规格型号 × 人员/工序,单元格=设备数量) * Y 轴 = 规格型号,X 轴 = 人员 或 工序,单元格 = 该交叉点的在制品设备数 */ import { useEffect, useMemo, useState } from "react"; import { Radio, Table, DatePicker } from "antd"; import { Loader2 } from "lucide-react"; import dayjs, { type Dayjs } from "dayjs"; import { fetchWipMatrix, type WipMatrixRow } from "../services/dashboardApi"; const { RangePicker } = DatePicker; // 每块最多显示的工序列数,超过则分块垂直排列,避免横向超长 const CHUNK_MATRIX = 6; // ⏰ 时间筛选(与全局概览一致) type DateRangeKey = "today" | "7d" | "30d" | "custom"; function rangeToParams(key: DateRangeKey, customRange: [Dayjs, Dayjs] | null) { if (key === "custom" && customRange) { return { since: customRange[0].startOf("day").toISOString(), until: customRange[1].endOf("day").toISOString(), }; } const since = dayjs().startOf("day"); if (key === "7d") return { since: since.subtract(7, "day").toISOString() }; if (key === "30d") return { since: since.subtract(30, "day").toISOString() }; return { since: since.toISOString() }; } export default function MatrixBoard() { // 固定按工序分布(已取消按人员分布) const [dateKey, setDateKey] = useState("today"); const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); const { since, until } = rangeToParams(dateKey, customRange); fetchWipMatrix("task_name", since, until) .then(setData) .catch(() => setData([])) .finally(() => setLoading(false)); }, [dateKey, customRange]); // 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、dimension_key 为列) // 列数超过 CHUNK_MATRIX 时分块垂直排列,避免一行十几个人员横向超长 const matrixChunks = useMemo(() => { const keys = Array.from(new Set(data.map((d) => d.dimension_key))); if (keys.length === 0) return []; const chunks: { columns: any[]; dataSource: any[]; keys: string[] }[] = []; for (let i = 0; i < keys.length; i += CHUNK_MATRIX) { const chunkKeys = keys.slice(i, i + CHUNK_MATRIX); const keySet = new Set(chunkKeys); const rowsMap = new Map(); for (const item of data) { if (!keySet.has(item.dimension_key)) continue; if (!rowsMap.has(item.spec_model)) { rowsMap.set(item.spec_model, { spec_model: item.spec_model, _assignees: {}, row_total: 0 }); } const row = rowsMap.get(item.spec_model); row[item.dimension_key] = (row[item.dimension_key] || 0) + item.count; row.row_total += item.count; // 记录每个交叉点的主负责人(中文名,去重) row._assignees[item.dimension_key] = item.assignees || []; } const columns: any[] = [ { title: "规格型号", dataIndex: "spec_model", fixed: "left", width: 170, sorter: (a: any, b: any) => String(a.spec_model).localeCompare(String(b.spec_model), "zh"), sortDirections: ["ascend", "descend"], render: (v: string) => {v}, }, ...chunkKeys.map((key) => ({ title: key, dataIndex: key, align: "center" as const, width: 120, render: (val: number, record: any) => { const assignees = record._assignees?.[key] || []; return (
{val || 0}
{assignees.length > 0 && (
{assignees.join("、")}
)}
); }, })), { title: "合计", dataIndex: "row_total", fixed: "right" as const, align: "center" as const, width: 90, render: (v: number) => {v} }, ]; chunks.push({ columns, dataSource: Array.from(rowsMap.values()), keys: chunkKeys }); } return chunks; }, [data]); return (

📊 WIP 分布矩阵

生产分布透视表:规格型号 × 工序 · 含在库/完成

{/* ⏰ 时间筛选(与全局概览一致) */}
{ setDateKey(e.target.value); setCustomRange(null); }} size="small" optionType="button" buttonStyle="solid" > 今天 近7天 近30天 自定义 {dateKey === "custom" && ( setCustomRange(dates as [Dayjs, Dayjs] | null)} style={{ width: 240 }} placeholder={["开始", "结束"]} /> )}
{loading && data.length === 0 ? (
) : matrixChunks.length === 0 ? (
暂无数据
) : ( /* 🔧 瀑布流:人员/工序列超 CHUNK_MATRIX 时,按列分块垂直向下排列 */
{matrixChunks.map((chunk, idx) => (
{idx > 0 &&
}
{ const totals: Record = {}; let rowTotal = 0; for (const row of pageData) { rowTotal += row.row_total || 0; for (const k of chunk.keys) totals[k] = (totals[k] || 0) + (row[k] || 0); } return ( 合计 {chunk.keys.map(k => ( {totals[k] || 0} ))} {rowTotal} ); }} /> ))} )} ); }