Restyle record detail modal as grouped card (match demo design)

- Dark header banner: work-order badge, total-row/workshop, delivery date
- Conditional missing-parts alert banner
- Process-node section with 4 track blocks; base-config section with
  highlighted key fields and full-width long-text fields; catch-all section
- Grey out empty / '-' / blank values
- formatCellValue now treats '-' as empty (added test)
This commit is contained in:
Misaka_Company
2026-06-24 12:41:33 +08:00
parent a4afa3a8f5
commit 964dfbb94f
3 changed files with 320 additions and 20 deletions

View File

@@ -1,14 +1,181 @@
"use client";
import type { CSSProperties } from "react";
import { Modal, Descriptions, Empty } from "antd";
import { Modal } from "antd";
import {
ClockCircleOutlined,
AppstoreOutlined,
UnorderedListOutlined,
WarningOutlined,
RightOutlined,
} from "@ant-design/icons";
import type { ReactNode } from "react";
import type { DataRow } from "./table-filters";
import { pickRecordTitle, formatCellValue } from "./record-detail";
const VALUE_STYLE: CSSProperties = {
whiteSpace: "pre-wrap",
wordBreak: "break-all",
};
/* ── 字段分组定义(键名对应真实数据列;未列出的列自动落入"其他信息")── */
const BANNER_KEYS = ["工令号", "总排号", "车间", "交货日期"];
const MISSING_KEY = "缺件明细";
const TRACK_GROUPS: { title: string; fields: string[] }[] = [
{
title: "【1】备料与物料准备",
fields: ["执行卡下发日期", "物料类别", "焊接领料日期", "领料单签收日期", "库房发出日期"],
},
{
title: "【2】前序加工隔膜 / 喷涂)",
fields: ["隔膜接收", "隔离膜片接收", "车波纹日期", "烘洗", "喷涂发出", "喷涂回来"],
},
{
title: "【3】焊接与装配",
fields: ["焊接接收日期", "膜片焊", "壳焊接员", "表壳焊接日期"],
},
{
title: "【4】校验 / 测试 / 入库",
fields: ["超压日期", "退火日期", "氦测日期", "调校人", "调试日期", "检验员", "检验日期", "入库日期"],
},
];
const SPEC_HIGHLIGHT = ["客户名称", "产品型号", "量程", "数量"];
const SPEC_FIELDS = [
"客户名称", "产品型号", "量程", "数量",
"隔膜类型", "隔膜大小", "隔膜材质", "膜片尺寸", "膜片材质",
"生产订单号", "订单号", "签订日期", "接单日期", "经办人", "盘号", "位号", "标准",
];
const SPEC_FULL = ["技术参数", "特殊要求", "备注", "新参数"];
const DEFINED_KEYS = new Set<string>([
...BANNER_KEYS,
MISSING_KEY,
...TRACK_GROUPS.flatMap((g) => g.fields),
...SPEC_FIELDS,
...SPEC_FULL,
]);
/* ── 卡片风子组件(对标 demo.tsx 的 Field / Section / TrackBlock── */
function Field({
label,
value,
highlight = false,
full = false,
}: {
label: string;
value: string | number | null;
highlight?: boolean;
full?: boolean;
}) {
const display = formatCellValue(value);
const isEmpty = display === "—";
return (
<div
style={{
gridColumn: full ? "1 / -1" : undefined,
display: "flex",
flexDirection: "column",
}}
>
<span style={{ fontSize: 11, color: "#64748b", marginBottom: 2, lineHeight: 1 }}>
{label}
</span>
<span
style={{
fontSize: 14,
lineHeight: 1.3,
wordBreak: "break-word",
color: isEmpty ? "#cbd5e1" : highlight ? "#2563eb" : "#1e293b",
fontWeight: isEmpty ? 400 : highlight ? 700 : 500,
}}
>
{display}
</span>
</div>
);
}
function Section({
icon,
title,
gridCols = 3,
children,
}: {
icon: ReactNode;
title: string;
gridCols?: number;
children: ReactNode;
}) {
return (
<div style={{ marginBottom: 24 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 12,
borderBottom: "1px solid #f1f5f9",
paddingBottom: 8,
}}
>
<span style={{ fontSize: 18, color: "#3b82f6", lineHeight: 1, display: "flex" }}>
{icon}
</span>
<h3 style={{ fontSize: 16, fontWeight: 700, color: "#1e293b", margin: 0 }}>{title}</h3>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
rowGap: 16,
columnGap: 24,
}}
>
{children}
</div>
</div>
);
}
function TrackBlock({ title, children }: { title: string; children: ReactNode }) {
return (
<div
style={{
background: "#f8fafc",
borderRadius: 6,
padding: 12,
border: "1px solid #e2e8f0",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 12,
fontWeight: 700,
color: "#334155",
borderBottom: "1px solid #e2e8f0",
paddingBottom: 6,
marginBottom: 8,
}}
>
<RightOutlined style={{ fontSize: 12, color: "#94a3b8" }} />
{title}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
columnGap: 12,
rowGap: 10,
}}
>
{children}
</div>
</div>
);
}
/* ── 主组件 ── */
export function RecordDetailModal({
record,
@@ -21,24 +188,153 @@ export function RecordDetailModal({
open: boolean;
onClose: () => void;
}) {
const colsSet = new Set(columns);
const has = (k: string) => colsSet.has(k);
const val = (k: string): string | number | null =>
record && has(k) ? (record[k] ?? null) : null;
const others = columns.filter((c) => !DEFINED_KEYS.has(c));
const missingVal = val(MISSING_KEY);
const missingEmpty = formatCellValue(missingVal) === "—";
const workOrder = val("工令号");
const workOrderDisplay = formatCellValue(workOrder);
const showFadedWorkOrder = workOrderDisplay !== "—";
return (
<Modal
title={`记录详情:${record ? pickRecordTitle(record) : ""}`}
title={record ? `记录详情:${pickRecordTitle(record)}` : "记录详情"}
open={open}
onCancel={onClose}
footer={null}
width={900}
width={960}
styles={{ body: { padding: 0 } }}
>
{record && columns.length > 0 ? (
<Descriptions column={2} bordered size="small">
{columns.map((col) => (
<Descriptions.Item key={col} label={col}>
<span style={VALUE_STYLE}>{formatCellValue(record[col])}</span>
</Descriptions.Item>
{record && (
<div>
{/* 黑底信息头 */}
<div
style={{
position: "relative",
background: "#1e293b",
color: "#fff",
padding: "16px 24px",
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
overflow: "hidden",
}}
>
{showFadedWorkOrder && (
<div
style={{
position: "absolute",
right: -8,
top: -22,
fontSize: 120,
fontWeight: 900,
fontFamily: "monospace",
color: "rgba(51,65,85,0.4)",
userSelect: "none",
pointerEvents: "none",
lineHeight: 1,
}}
>
{workOrderDisplay}
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: 16, position: "relative", zIndex: 1 }}>
<div
style={{
background: "#2563eb",
padding: "8px 12px",
borderRadius: 8,
textAlign: "center",
minWidth: 64,
}}
>
<div style={{ fontSize: 10, color: "#bfdbfe", marginBottom: 4, lineHeight: 1 }}></div>
<div style={{ fontSize: 20, fontWeight: 700, fontFamily: "monospace", lineHeight: 1 }}>
{workOrderDisplay}
</div>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<span style={{ fontSize: 12, color: "#94a3b8", marginBottom: 2 }}> / </span>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
<span style={{ fontSize: 18, fontWeight: 700 }}>{formatCellValue(val("总排号"))}</span>
<span style={{ color: "#64748b" }}>|</span>
<span style={{ fontSize: 14, fontWeight: 500 }}>{formatCellValue(val("车间"))}</span>
</div>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8, position: "relative", zIndex: 1 }}>
<span style={{ fontSize: 12, color: "#94a3b8" }}></span>
<span style={{ fontWeight: 700, fontSize: 16, color: "#34d399" }}>
{formatCellValue(val("交货日期"))}
</span>
</div>
</div>
{/* 缺件预警 */}
{has(MISSING_KEY) && !missingEmpty && (
<div
style={{
background: "#fef2f2",
borderBottom: "1px solid #fecaca",
padding: "10px 24px",
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<WarningOutlined style={{ color: "#dc2626" }} />
<span style={{ fontSize: 14, color: "#b91c1c", fontWeight: 700 }}></span>
<span style={{ fontSize: 14, color: "#dc2626", wordBreak: "break-word" }}>
{missingVal === null || missingVal === undefined ? "" : String(missingVal)}
</span>
</div>
)}
{/* 明细主体 */}
<div style={{ padding: 24 }}>
{/* 工序节点(提至最前) */}
{TRACK_GROUPS.some((g) => g.fields.some(has)) && (
<Section icon={<ClockCircleOutlined />} title="工序节点" gridCols={2}>
{TRACK_GROUPS.filter((g) => g.fields.some(has)).map((g) => (
<TrackBlock key={g.title} title={g.title}>
{g.fields.filter(has).map((f) => (
<Field key={f} label={f} value={record[f]} />
))}
</Descriptions>
) : (
<Empty description="无数据" />
</TrackBlock>
))}
</Section>
)}
{/* 基础配置与规格 */}
{SPEC_FIELDS.some(has) && (
<Section icon={<AppstoreOutlined />} title="基础配置与规格" gridCols={3}>
{SPEC_FIELDS.filter(has).map((f) => (
<Field key={f} label={f} value={record[f]} highlight={SPEC_HIGHLIGHT.includes(f)} />
))}
{SPEC_FULL.filter(has).map((f) => (
<Field key={f} label={f} value={record[f]} full />
))}
</Section>
)}
{/* 其他信息(兜底:未被分组的列) */}
{others.length > 0 && (
<Section icon={<UnorderedListOutlined />} title="其他信息" gridCols={3}>
{others.map((f) => (
<Field key={f} label={f} value={record[f]} />
))}
</Section>
)}
</div>
</div>
)}
</Modal>
);

View File

@@ -29,6 +29,9 @@ describe("formatCellValue", () => {
expect(formatCellValue("")).toBe("—");
expect(formatCellValue(" ")).toBe("—");
});
it("hyphen '-' (no-value marker) -> em dash", () => {
expect(formatCellValue("-")).toBe("—");
});
it("0 is NOT blank", () => {
expect(formatCellValue(0)).toBe("0");
});

View File

@@ -16,11 +16,12 @@ export function pickRecordTitle(record: DataRow): string {
/**
* 格式化单元格值用于详情 Modal 完整展示。
* null / 空串 / 纯空白 -> "—";其余 String(value)。0 视为非空(与 isBlank 一致)。
* null / "-" / 空串 / 纯空白 -> "—"(弱化显示);其余 String(value)。
* 注:生产数据中 "-" 表示"无值",展示时与空白同等弱化(仅展示语义,与筛选用的 isBlank 不同)。
*/
export function formatCellValue(value: string | number | null): string {
if (value === null || value === undefined) return "—";
const s = String(value);
if (s.trim() === "") return "—";
if (s === "-" || s.trim() === "") return "—";
return s;
}