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";
|
"use client";
|
||||||
|
|
||||||
import type { CSSProperties } from "react";
|
import { Modal } from "antd";
|
||||||
import { Modal, Descriptions, Empty } from "antd";
|
import {
|
||||||
|
ClockCircleOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import type { DataRow } from "./table-filters";
|
import type { DataRow } from "./table-filters";
|
||||||
import { pickRecordTitle, formatCellValue } from "./record-detail";
|
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({
|
export function RecordDetailModal({
|
||||||
record,
|
record,
|
||||||
@@ -21,24 +188,153 @@ export function RecordDetailModal({
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={`记录详情:${record ? pickRecordTitle(record) : ""}`}
|
title={record ? `记录详情:${pickRecordTitle(record)}` : "记录详情"}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
width={900}
|
width={960}
|
||||||
|
styles={{ body: { padding: 0 } }}
|
||||||
>
|
>
|
||||||
{record && columns.length > 0 ? (
|
{record && (
|
||||||
<Descriptions column={2} bordered size="small">
|
<div>
|
||||||
{columns.map((col) => (
|
{/* 黑底信息头 */}
|
||||||
<Descriptions.Item key={col} label={col}>
|
<div
|
||||||
<span style={VALUE_STYLE}>{formatCellValue(record[col])}</span>
|
style={{
|
||||||
</Descriptions.Item>
|
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>
|
</TrackBlock>
|
||||||
) : (
|
))}
|
||||||
<Empty description="无数据" />
|
</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>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ describe("formatCellValue", () => {
|
|||||||
expect(formatCellValue("")).toBe("—");
|
expect(formatCellValue("")).toBe("—");
|
||||||
expect(formatCellValue(" ")).toBe("—");
|
expect(formatCellValue(" ")).toBe("—");
|
||||||
});
|
});
|
||||||
|
it("hyphen '-' (no-value marker) -> em dash", () => {
|
||||||
|
expect(formatCellValue("-")).toBe("—");
|
||||||
|
});
|
||||||
it("0 is NOT blank", () => {
|
it("0 is NOT blank", () => {
|
||||||
expect(formatCellValue(0)).toBe("0");
|
expect(formatCellValue(0)).toBe("0");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ export function pickRecordTitle(record: DataRow): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化单元格值用于详情 Modal 完整展示。
|
* 格式化单元格值用于详情 Modal 完整展示。
|
||||||
* null / 空串 / 纯空白 -> "—";其余 String(value)。0 视为非空(与 isBlank 一致)。
|
* null / "-" / 空串 / 纯空白 -> "—"(弱化显示);其余 String(value)。
|
||||||
|
* 注:生产数据中 "-" 表示"无值",展示时与空白同等弱化(仅展示语义,与筛选用的 isBlank 不同)。
|
||||||
*/
|
*/
|
||||||
export function formatCellValue(value: string | number | null): string {
|
export function formatCellValue(value: string | number | null): string {
|
||||||
if (value === null || value === undefined) return "—";
|
if (value === null || value === undefined) return "—";
|
||||||
const s = String(value);
|
const s = String(value);
|
||||||
if (s.trim() === "") return "—";
|
if (s === "-" || s.trim() === "") return "—";
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user