---
description: "从列表点进一条记录看的那一页：面包屑 + 标题 + 状态徽标 + Edit/Delete；下面按分区把字段用 DescriptionList 铺开。数据模型是「一条记录一次查询」，页面得同时把 loading 和 not-found 两态显式画出来"
---

# 详情页（单条记录的独立页）

从列表点进一条记录看的那一页：面包屑 + 标题 + 状态徽标 + Edit/Delete；下面按分区把字段用 DescriptionList 铺开。数据模型是「一条记录一次查询」，页面得同时把 loading 和 not-found 两态显式画出来。

<iframe src="/skills/dashboard/assets/demo-detail-page.html"
        style="width:100%;height:640px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：详情页"></iframe>

试这几下：

- 顶部**加载中 / 有数据 / 未找到**三态切着看 —— 骨架屏占位不跳版式，未找到给了「返回列表」出口
- 点**编辑** —— 弹出与列表复用的同一份表单弹窗；保存后 toast + `更新时间` 就地刷新
- 点**删除** —— 先确认再执行；删除成功后停在「未找到」态（真实版此时导航回 `/products` 列表）
- 面包屑右侧显示了这一页的**查询键形状** `["products", "detail", id]`

## 规矩

- **详情查询键固定形状**：`["<资源>", "detail", id]` —— 一条记录一次查询，靠 id 命中；真实版由 loader 里的 `ensureQueryData(<name>DetailQuery(params.id))` 预取，命中即直出
- **三态都要显式画**：`loading` 用 Skeleton 占位（版式不跳）/ `ready` 正文 / 找不到记录时 loader 抛 `notFound()`，路由渲染 not-found 视图并给回列表的出口
- **编辑复用列表用的同一份表单弹窗**（`<Resource>FormDialog`），编辑/新建只是 `mode` 不同 —— demo 里也是同一个 dialog 组件
- **破坏性操作先确认**，成功后返回 `/<资源>` 列表 + toast；真实版的删除 mutation 成功后应失效该资源的查询键
- **路由文件名的下划线约定**（真实版 only）：`<name>_.$id.tsx` 尾下划线让详情**跳出列表 route 的 layout**，独立成页；列表页的姓名列用 `<Link to="/<name>/$id">` 连过来

> 蓝本：`add-detail-page.md`[^add-detail-page]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-detail-page.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-detail-page.html"
    ```

[^add-detail-page]: [`add-detail-page.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-detail-page.md)，2026-07-28 读。
