Skip to Content
UI playbookDetail pages

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/valueType render the same; hideInDescriptions for search-only columns.
  • Inline edit: ProDescriptions editable={{ onSave }} with editable: false per 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