Files
track/frontend/src/pages/MatrixBoard.tsx

148 lines
6.1 KiB
TypeScript
Raw Normal View History

/**
* WIP 分布矩阵 — 在制品透视表(规格型号 × 人员/工序,单元格=设备数量)
* Y 轴 = 规格型号,X 轴 = 人员 或 工序,单元格 = 该交叉点的在制品设备数
*/
import { useEffect, useMemo, useState } from "react";
import { Radio, Table, DatePicker, Button } from "antd";
import { Loader2 } from "lucide-react";
import type { Dayjs } from "dayjs";
import { fetchWipMatrix, type WipMatrixRow } from "../services/dashboardApi";
const { RangePicker } = DatePicker;
// 每块最多显示的人员/工序列数,超过则分块垂直排列,避免横向超长
const CHUNK_MATRIX = 6;
export default function MatrixBoard() {
const [dimension, setDimension] = useState<"assignee" | "task_name">("assignee");
const [range, setRange] = useState<[Dayjs, Dayjs] | null>(null);
const [data, setData] = useState<WipMatrixRow[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchWipMatrix(
dimension,
range ? range[0].startOf("day").toISOString() : undefined,
range ? range[1].endOf("day").toISOString() : undefined,
)
.then(setData)
.catch(() => setData([]))
.finally(() => setLoading(false));
}, [dimension, range]);
// 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、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[] }[] = [];
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<string, any>();
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) => <span className="font-semibold text-gray-700">{v}</span>,
},
...chunkKeys.map((key) => ({
title: key,
dataIndex: key,
align: "center" as const,
width: 120,
render: (val: number, record: any) => {
const assignees = record._assignees?.[key] || [];
return (
<div className="text-center">
<div className="text-sm font-bold text-gray-800">{val || 0}</div>
{assignees.length > 0 && (
<div className="max-w-[100px] truncate text-[10px] text-gray-400" title={assignees.join("、")}>
{assignees.join("、")}
</div>
)}
</div>
);
},
})),
{ title: "合计", dataIndex: "row_total", fixed: "right" as const, align: "center" as const, width: 90, render: (v: number) => <span className="font-bold text-blue-600">{v}</span> },
];
chunks.push({ columns, dataSource: Array.from(rowsMap.values()) });
}
return chunks;
}, [data]);
return (
<div>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-xl font-bold text-gray-800">📊 WIP 分布矩阵</h2>
<p className="mt-1 text-sm text-gray-500">生产分布透视表:规格型号 × {dimension === "assignee" ? "人员" : "工序"} · 含在库/完成</p>
</div>
<div className="flex items-center gap-2">
<RangePicker
size="small"
value={range as any}
onChange={(dates) => setRange(dates as [Dayjs, Dayjs] | null)}
style={{ width: 240 }}
placeholder={["开始日期", "结束日期"]}
/>
{range && <Button size="small" onClick={() => setRange(null)}>全部</Button>}
<Radio.Group value={dimension} onChange={(e) => setDimension(e.target.value)} optionType="button" buttonStyle="solid">
<Radio.Button value="assignee">按人员分布</Radio.Button>
<Radio.Button value="task_name">按工序分布</Radio.Button>
</Radio.Group>
</div>
</div>
{loading && data.length === 0 ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : matrixChunks.length === 0 ? (
<div className="rounded-xl bg-white py-16 text-center text-sm text-gray-400 shadow-sm">暂无数据</div>
) : (
/* 🔧 瀑布流:人员/工序列超 CHUNK_MATRIX 时,按列分块垂直向下排列 */
<div className="flex flex-col gap-8">
{matrixChunks.map((chunk, idx) => (
<div key={idx} className="relative">
{idx > 0 && <div className="absolute -top-4 left-0 w-full border-t border-dashed border-gray-200" />}
<div className="rounded-xl bg-white p-4 shadow-sm">
<Table
columns={chunk.columns}
dataSource={chunk.dataSource}
loading={loading}
rowKey="spec_model"
bordered
size="small"
pagination={false}
scroll={{ x: "max-content" }}
/>
</div>
</div>
))}
</div>
)}
</div>
);
}