---
description: "后台的「首选项」形状：一个页面塞多组不相关的开关和输入 —— 个人资料、账号、通知、危险区。左侧锚点定位，右侧逐区保存，破坏性操作走双确认。个人页、项目偏好、团队设置都是这一副骨架"
---

# 设置页（分区保存 + 危险区双确认）

后台的「首选项」形状：一个页面塞多组不相关的开关和输入 —— 个人资料、账号、通知、危险区。左侧锚点定位，右侧逐区保存，破坏性操作走双确认。个人页、项目偏好、团队设置都是这一副骨架。

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

试这几下：

- 改「个人资料」里的姓名 —— 该分区的**保存 / 还原**从灰变亮，左侧锚点旁亮起脏标 `●`；其它分区不受牵连
- 依次改**两个分区**再只保存其中一个 —— 只清那一区的脏，另一区照旧脏、锚点圆点还在
- 点左侧锚点 —— 平滑滚到对应分区；页面滚动时锚点自己跟着高亮切换
- 保存后按钮回灰、`●` 消失，toast 报明是哪一分区
- 危险区点**删除账户** —— 先弹确认框，再让你输入用户名 `chenhe`，输错「永久删除」按钮就是不启用

## 规矩

- **一区一份状态、一份已保存快照、一个保存按钮**：脏 = 当前值对已保存快照的浅 diff；`保存` 让快照吃掉当前值，`还原` 反过来。demo 每区用 `useSection(initial)` 自封装出这三件套；真实版每区一个 `useMutation`，脏由表单库（`form.formState.isDirty`）负责，保存成功后失效该资源的查询键 + toast
- **保存 / 还原按钮同 `dirty` 门控**：写入是分区级别的原子提交，避免「A 区在改、B 区半保存」的模糊状态；蓝本 Invariants 的「Save 后 toast」在 demo 里报明是哪一分区已保存
- **破坏性操作双确认**：先弹确认框说清后果，再让输入完整用户名精确匹配才启用红色主按钮；输错就禁用，不给「差不多就点」的机会。真实版把用户名换成当前用户的登录名从 session 取
- **锚点导航靠 `IntersectionObserver`**：观察带 `id` 的四个 `<section>`，`rootMargin` 圈住视口中间一条带，谁进带里谁 active；点击锚点用 `scrollIntoView({ behavior: "smooth" })` 并短暂锁定 IO 防过渡帧覆盖点击目标。真实版若有粘性顶栏，用 `scroll-margin-top` 让锚点跳转不被顶栏遮挡

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

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

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

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