93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
|
|
/**
|
|||
|
|
* WIP 分布矩阵 — 在制品透视表(规格型号 × 人员/工序,单元格=设备数量)
|
|||
|
|
* Y 轴 = 规格型号,X 轴 = 人员 或 工序,单元格 = 该交叉点的在制品设备数
|
|||
|
|
*/
|
|||
|
|
import { useEffect, useMemo, useState } from "react";
|
|||
|
|
import { Radio, Table } from "antd";
|
|||
|
|
import { fetchWipMatrix, type WipMatrixRow } from "../services/dashboardApi";
|
|||
|
|
|
|||
|
|
export default function MatrixBoard() {
|
|||
|
|
const [dimension, setDimension] = useState<"assignee" | "task_name">("assignee");
|
|||
|
|
const [data, setData] = useState<WipMatrixRow[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setLoading(true);
|
|||
|
|
fetchWipMatrix(dimension)
|
|||
|
|
.then(setData)
|
|||
|
|
.catch(() => setData([]))
|
|||
|
|
.finally(() => setLoading(false));
|
|||
|
|
}, [dimension]);
|
|||
|
|
|
|||
|
|
// 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、dimension_key 为列)
|
|||
|
|
const { columns, dataSource } = useMemo(() => {
|
|||
|
|
const keys = Array.from(new Set(data.map((d) => d.dimension_key)));
|
|||
|
|
const rowsMap = new Map<string, any>();
|
|||
|
|
for (const item of data) {
|
|||
|
|
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 ds = Array.from(rowsMap.values());
|
|||
|
|
|
|||
|
|
const cols: any[] = [
|
|||
|
|
{ title: "规格型号", dataIndex: "spec_model", fixed: "left", width: 190, render: (v: string) => <span className="font-semibold text-gray-700">{v}</span> },
|
|||
|
|
...keys.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> },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return { columns: cols, dataSource: ds };
|
|||
|
|
}, [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>
|
|||
|
|
<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 className="rounded-xl bg-white p-4 shadow-sm">
|
|||
|
|
<Table
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={dataSource}
|
|||
|
|
loading={loading}
|
|||
|
|
rowKey="spec_model"
|
|||
|
|
bordered
|
|||
|
|
size="small"
|
|||
|
|
pagination={false}
|
|||
|
|
scroll={{ x: "max-content" }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|