feat(透视表): 列数超6时分块垂直排列 + 规格型号列升降序
- 人员/工序列超过 CHUNK_MATRIX(6) 时按列分块,多张透视表垂直向下排列,告别一行十几个人员横向超长 - 规格型号列支持点击升降序排序(中文字典序) - 每块独立合计
This commit is contained in:
@ -4,11 +4,15 @@
|
||||
*/
|
||||
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);
|
||||
@ -28,46 +32,63 @@ export default function MatrixBoard() {
|
||||
}, [dimension, range]);
|
||||
|
||||
// 🔧 核心:扁平数组 → 动态交叉表(规格型号为行、dimension_key 为列)
|
||||
const { columns, dataSource } = useMemo(() => {
|
||||
// 列数超过 CHUNK_MATRIX 时分块垂直排列,避免一行十几个人员横向超长
|
||||
const matrixChunks = 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 });
|
||||
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 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>
|
||||
);
|
||||
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>,
|
||||
},
|
||||
})),
|
||||
{ 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> },
|
||||
];
|
||||
...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> },
|
||||
];
|
||||
|
||||
return { columns: cols, dataSource: ds };
|
||||
chunks.push({ columns, dataSource: Array.from(rowsMap.values()) });
|
||||
}
|
||||
return chunks;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
@ -93,18 +114,34 @@ export default function MatrixBoard() {
|
||||
</div>
|
||||
</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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user