Detail pages
Detail pages — ProDescriptions + Steps + Timeline
Composition (from ant-design-pro profile/advanced): header (title, tags, owner actions, summary content, key number extraContent) → progress card (Steps: who did what when, current step = the pending decision) → result Alert (approved/rejected + note) → ProCard gutter={16} wrap with Info (ProDescriptions, 2 columns) and History (Timeline: actor · action · quoted note · time) → sticky footer with the decision.
- Reuse list columns:
<ProDescriptions columns={columns as ProDescriptionsItemProps<Row>[]} dataSource={r} />—valueEnum/valueTyperender the same;hideInDescriptionsfor search-only columns. - Inline edit:
ProDescriptions editable={{ onSave }}witheditable: falseper column. - Not found / no permission:
Result status="404"inside the page container with a primary “Về danh sách”.
Trang chi tiết + duyệt — Trang chi tiết: PageContainer (breadcrumb, tag, tóm tắt, số chính) + Steps tiến trình + Info + Timeline + nút quyết định cố định ở chân. Mở trong Playground →
Xem code (68 dòng)
// Trang chi tiết: PageContainer (breadcrumb, tag, tóm tắt, số chính) + Steps tiến trình + Info + Timeline + nút quyết định cố định ở chân.
// Không dùng PageContainer onBack (làm sập trang trên runtime này) — dùng breadcrumb.
import { useState } from "react";
import { ModalForm, PageContainer, ProCard, ProDescriptions, ProFormTextArea } from "@ant-design/pro-components";
import { Alert, App, Button, Space, Steps, Tag, Timeline, Typography } from "antd";
import { CheckOutlined, CloseOutlined } from "@ant-design/icons";
export default function Page() {
const { message } = App.useApp();
const [status, setStatus] = useState<"pending" | "approved" | "rejected">("pending");
const [note, setNote] = useState<string | null>(null);
const events = [
{ color: "blue", text: <><b>Trần Thị Lan</b> tạo đề xuất</>, at: "12:52 10/11/2026" },
...(status !== "pending" ? [{ color: status === "approved" ? "green" : "red", text: <><b>Nguyễn Thu Hà</b> {status === "approved" ? "duyệt" : "từ chối"}{note ? <> — <i>“{note}”</i></> : null}</>, at: "vừa xong" }] : []),
];
const decide = (to: "approved" | "rejected") => async (v: { note?: string }) => {
setStatus(to); setNote(v.note ?? null);
message.success(to === "approved" ? "Đã duyệt" : "Đã từ chối");
return true;
};
return (
<PageContainer
header={{
title: "Gia hạn Microsoft 365 Business (12 tháng, 10 user)",
breadcrumb: { items: [{ title: <a>Đề xuất</a> }, { title: "#9" }] },
tags: <Tag color={status === "pending" ? "processing" : status === "approved" ? "success" : "error"}>{status === "pending" ? "Chờ duyệt" : status === "approved" ? "Đã duyệt" : "Từ chối"}</Tag>,
}}
content={
<ProDescriptions column={{ xs: 1, sm: 2, lg: 3 }}>
<ProDescriptions.Item label="Người tạo">Trần Thị Lan · Hành chính</ProDescriptions.Item>
<ProDescriptions.Item label="Hạng mục">Phần mềm</ProDescriptions.Item>
<ProDescriptions.Item label="Cần trước">13/11/2026</ProDescriptions.Item>
</ProDescriptions>
}
extraContent={
<Space orientation="vertical" size={0} style={{ textAlign: "right" }}>
<Typography.Text type="secondary">Số tiền đề xuất</Typography.Text>
<Typography.Text strong style={{ fontSize: 28 }}>18.600.000 ₫</Typography.Text>
</Space>
}
footer={status === "pending" ? [
<ModalForm key="r" title="Từ chối đề xuất" width={480} trigger={<Button danger icon={<CloseOutlined />}>Từ chối</Button>} onFinish={decide("rejected")} modalProps={{ okText: "Từ chối", okButtonProps: { danger: true } }}>
<ProFormTextArea name="note" label="Lý do từ chối" rules={[{ required: true, whitespace: true, message: "Cần ghi lý do" }]} />
</ModalForm>,
<ModalForm key="a" title="Duyệt đề xuất" width={480} trigger={<Button type="primary" icon={<CheckOutlined />}>Duyệt</Button>} onFinish={decide("approved")} modalProps={{ okText: "Duyệt" }}>
<ProFormTextArea name="note" label="Ghi chú (tuỳ chọn)" />
</ModalForm>,
] : undefined}
>
<ProCard direction="column" ghost gutter={[0, 16]}>
<ProCard title="Tiến trình" headerBordered>
<Steps current={status === "pending" ? 1 : 2} status={status === "rejected" ? "error" : status === "approved" ? "finish" : "process"}
items={[{ title: "Gửi đề xuất", content: "Trần Thị Lan · 12:52 10/11" }, { title: status === "pending" ? "Quản lý duyệt" : status === "approved" ? "Đã duyệt" : "Từ chối", content: status === "pending" ? "Cần trước 13/11" : "Nguyễn Thu Hà" }]} />
</ProCard>
{status === "rejected" && <Alert type="error" showIcon title="Bị từ chối bởi Nguyễn Thu Hà" description={`Lý do: ${note}`} />}
<ProCard gutter={16} ghost wrap>
<ProCard title="Thông tin" headerBordered colSpan={{ xs: 24, lg: 15 }}>
<Typography.Paragraph>Bản hiện tại hết hạn cuối tháng, cần gia hạn để không gián đoạn email.</Typography.Paragraph>
</ProCard>
<ProCard title="Lịch sử" headerBordered colSpan={{ xs: 24, lg: 9 }}>
<Timeline items={events.map((e) => ({ color: e.color, children: <><div>{e.text}</div><Typography.Text type="secondary" style={{ fontSize: 12 }}>{e.at}</Typography.Text></> }))} />
</ProCard>
</ProCard>
</ProCard>
</PageContainer>
);
}
Last updated on