---
description: "一屏两栈：左边是资源列表始终挂载，右边是选中那条的详情。做「一屏看多条 + 一次编一条」的场景，比进详情页再退回列表少一次上下文切换，也不打断列表的搜索/排序状态"
---

# 主从视图（Master-detail split）

一屏两栈：左边是资源列表始终挂载，右边是选中那条的详情。做「一屏看多条 + 一次编一条」的场景，比进详情页再退回列表少一次上下文切换，也不打断列表的搜索/排序状态。

<iframe src="/skills/dashboard/assets/demo-master-detail.html"
        style="width:100%;height:660px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：主从视图"></iframe>

试这几下：

- 点左侧任意订单 —— 右侧详情联动切换，选中行高亮，列表位置**不重置**
- 详情里按**编辑**改字段 → **保存**，左侧对应行同步刷新，toast 提示
- 顶部搜索框过滤单号/名称/客户；改搜索关键字不影响右侧当前选中
- **删除**先弹确认；删完选中自动移到下一条，主列表原地更新
- 把窗口拖窄到 720px 以下：只显示列表；点一条进详情，详情内有「← 返回列表」

## 规矩

- **选中锚点唯一**：真实版把选中放进 URL（嵌套路由 `/<resource>/$id`），列表始终挂载、搜索/排序参数原样保留；demo 简化成 `useState(selId)`，刷新即丢，但外壳交互一致
- **主表和详情各自成栈**：真实版 list query 一直缓存着，详情面板独立跑 `<name>DetailQuery(id)`，两边加载互不阻塞；demo 共用一份内存数组，选行只是切 id
- **id 失效走面板本地的 not-found**：真实版 detail 的 loader `throw notFound()`，触发路由自己的 `notFoundComponent`，只替换面板不掀翻整个分栏；demo 里删除会把选中挪到下一条，选空时右侧渲染「订单不存在」占位
- **编辑 = 保存 + 失效查询键**：真实版调 `useUpdate<Name>` mutation，成功后 react-query invalidate 该资源的 list 与 detail 两处，两侧自动重取；demo 直接 `setRows(...)` 写同一份内存源，效果一致但没有加载/错误态
- **关闭详情保留列表参数**：真实版关闭按钮 `navigate({ to: "/<name>", search: (prev) => prev })`；demo 只在窄屏有「返回列表」，宽屏两栏并列不需要关闭
- 破坏性操作先过确认弹窗；任何变更成功后 toast

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

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

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

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