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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user