---
description: "详情页把一个记录切成几张 tab（概览 / 活动 / 设置…），当前 tab 存在 URL 里，链接可分享、后退键能回；每张 tab 的数据首次切到才拉，切回不再取数。资源有多个视角、不适合塞进一屏时，就是它"
---

# 记录选项卡（详情页内 tab 分区 + 懒加载）

详情页把一个记录切成几张 tab（概览 / 活动 / 设置…），当前 tab 存在 URL 里，链接可分享、后退键能回；每张 tab 的数据首次切到才拉，切回不再取数。资源有多个视角、不适合塞进一屏时，就是它。

<iframe src="/skills/dashboard/assets/demo-record-tabs.html"
        style="width:100%;height:640px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：记录选项卡"></iframe>

试这几下：

- 点**概览 / 活动 / 设置** —— URL hash 立刻变成 `#tab=<key>`；tab 标签后面出「•」代表已缓存
- 首次切到某 tab —— 出骨架屏 ~400ms 后展示；再切回，秒开（缓存命中，不重复取数）
- 设置里翻两个开关再点**保存设置** —— toast + 写回缓存；下次切回值仍在
- 设置里点**取消订单** —— 先弹确认；确认后 toast，订单头状态角标变「已取消」
- 直接在浏览器地址栏改 hash（或刷新 iframe）—— 页面就落到该 tab

## 规矩

- **当前 tab 在 URL 里**（分享/后退/前进可用），不是局部 state。demo 用 `location.hash`（`#tab=…`）模拟；真实版走嵌套路由 `/<resource>/$id/<tab>`（TanStack Router 的 `createFileRoute` + `validateSearch`）
- **每个 tab 首次切到才拉数据**（懒加载），加载中出骨架屏；base-ui `Tabs` 会挂载所有面板，所以每个 panel 各自的查询要**用当前 tab 门控**，未选中时不发请求
- **已加载过的 tab 缓存不重复取数**。demo 用 `useState` 存 `{ [tab]: data }`；真实版按 TanStack Query 查询键 `["<resource>", "detail", id, "<tab>"]` 天然缓存，切回是纯读缓存
- **变更必 toast**（保存设置 / 取消订单 均有），**破坏性操作先弹确认**（取消订单）；真实版取消订单成功后失效 `["<resource>","detail",id]` 查询键，让概览重取

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

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

    ```html
    --8<-- "skills/dashboard/assets/demo-record-tabs.html"
    ```

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