# CLAUDE.md

本文件是我（Claude）在 `cn-rdms-web` 项目中的个人工作笔记，沉淀团队既有规范（来源：`AGENTS.md`）与协作惯例。每次进入仓库前先读这一份，避免重复踩坑。

> 本文件仅本地保留，已加入 `.gitignore`，请勿提交。

---

## 0. 行为基线（最重要，先记住）

- **描述现状以代码、配置、文档可直接验证的事实为准**；不引入历史实现/过渡方案/猜测。
- **默认精简回答**：先给结论 → 改动点 → 验证方式 → 必要风险。**除非用户主动要求详细，否则不要展开**——不复述清单、不列每条改动的小理由、不堆"汇总"段。用户只让分析就停在分析层，不主动跳到实现。
- **分析/解释类回答不要堆代码层面描述**：默认用业务/逻辑语言说清楚结构、差异与结论；不要大段贴源码、不要罗列 `file:line`、不要把"实现细节"当解释。只有用户明确要求看代码、或非贴不可的关键佐证（如某行就是争议焦点），才贴最少代码片段。
- **进入实施阶段前，先说目标、涉及模块、预计改动点、验证方式**。
- **最小改动原则**：只改当前任务必需的范围，不顺手重构无关代码。
- **不主动执行 git 操作**（status/diff/add/commit/restore/reset/checkout 全部不主动跑），除非用户明确要求。识别用户改动优先用 Read 直接看文件。
- 工作树脏的时候，**不要回退与当前任务无关的变更**。
- 静态校验默认只跑 `pnpm typecheck`；UI/交互/样式类任务**默认不补也不跑前端测试**，除非用户明确要求。

---

## 1. 项目骨架（认知地图）

| 维度 | 现状 |
|---|---|
| 应用 | RDMS 系统的 Vue 3 后台前端 |
| 包管理 | `pnpm`（>=8.7.0），Node `>=20.19.0` |
| 工具链 | Vite 7、TypeScript、Pinia、Element Plus、UnoCSS |
| 工作区 | `packages/*`，通过 `@sa/*` 引用 |
| 别名 | `@` → `src`；`~` → 仓库根 |
| 端口 | dev 9527 / preview 9725 |
| 环境文件 | `.env`、`.env.dev`、`.env.prod` |

**已经形成闭环的五条主线，后续改动顺着做，不平行起新的：**

1. **路由来源统一**：页面文件 + 自定义路由 → `elegant-router` 生成 → `build/plugins/router.ts` 集中补 `meta`。
2. **权限入口统一**：常量路由 / 权限路由分流；`route store` 负责初始化、菜单生成、缓存路由、面包屑。
3. **请求入口统一**：所有业务请求走 `src/service/request/index.ts`。
4. **页面套路统一**：列表页 = 搜索区 + 表格区 + 操作弹层/抽屉 + `modules/*` 子组件。
5. **衍生资产统一**：页面资源白名单从路由结构生成，不手工维护第二份。

---

## 2. 关键目录速查

| 路径 | 职责 |
|---|---|
| `src/views` | 业务页面（编排层薄） |
| `src/components` | 共享组件 |
| `src/layouts` | 应用壳、头部、侧栏、菜单、标签页、主题抽屉 |
| `src/store/modules` | Pinia 模块：app / auth / route / tab / theme / dict |
| `src/service/api` | 接口封装、参数归一化、查询字符串拼装、返回类型对齐 |
| `src/service/request` | 统一请求实例、鉴权、加密、错误处理、token 刷新 |
| `src/router/routes` | 自定义路由 |
| `src/router/elegant` | **生成产物，不要手改** |
| `src/theme/settings.ts` | 默认主题与布局设置 |
| `build/plugins/router.ts` | elegant-router 配置 + 路由 meta 生成 |
| `src/hooks/common/table.ts` | 列表页表格 hook 主入口 |
| `src/hooks/common/form.ts` | 表单校验与表单实例 hook |
| `src/constants/status-tag.ts` | 业务对象状态颜色（ElTag type）集中配置 |
| `src/styles/scss/element-plus.scss` | 表格/弹层/按钮/表单 密度与公共壳样式 |
| `packages/*` | 项目内本地共享库 |
| `docs/` | 架构/权限/页面规范文档，做相关改动前先查 |

---

## 3. 生成文件（不要手改）

- `src/router/elegant/imports.ts`
- `src/router/elegant/routes.ts`
- `src/router/elegant/transform.ts`
- `src/typings/elegant-router.d.ts`
- `src/typings/components.d.ts`
- `docs/frontend-page-resource-manifest.json`

**再生命令：**
- 路由产物过期 → `pnpm gen-route`
- 页面资源清单需同步 → `pnpm gen:page-resource-manifest`

---

## 4. 路由与导航

- 新增业务页：通过页面文件 + `build/plugins/router.ts` 补齐，**不要在多个位置重复注册**。
- `meta.icon` = Iconify 图标；`meta.localIcon` = 本地 SVG。**不要混用字段语义。**
- `meta` 中心落点是 `build/plugins/router.ts`，新页的 `icon`/`order`/`roles`/`keepAlive` 在那里集中维护。
- `meta.constant = true` → 常量路由；其他默认权限路由。常量路由维护入口是 `build/plugins/router.ts` 和 `src/router/routes/custom-routes.ts`。
- `i18nKey` 是兼容字段，不是新页必须补齐项。

### 4.1 对象上下文业务域（重要陷阱）

- `product`、`project` 这类业务域，**入口页是设计如此**：先进业务域入口页 → 再选对象建上下文。**不要把"入口页是可点击菜单"误判成 bug。**
- 入口页（如 `product_list -> /product/list -> view.product_list`）可作为左侧一级菜单实际命中页。这 ≠ 已进入对象上下文态。
- **遇到"点入口页后布局壳消失、只剩内容页"**：先查是否动态权限路由模式 + 后端 `get-user-routes` 是否缺业务域根路由。**不要直接把入口菜单从"菜单"改成"目录"**。
- 在 `VITE_AUTH_ROUTE_MODE=dynamic` 下，若后端只返回叶子页（如缺 `product -> layout.base`，只返 `product_list`），前端必须在动态路由归一化阶段**补回本地业务域骨架**，不能让入口裸挂为顶层 `view.*`。
- 对象上下文稳定来源仍是本地路由骨架；动态路由兼容只能"补骨架 + 对齐入口"，不能反推。
- 新增业务域时同步检查：本地静态骨架、`src/constants/object-context.ts` 中的 `domainKey/entryRouteKey/entryRoutePath/fallbackDefaultRouteKey`、动态路由归一化、对象上下文 store、头部菜单切换。

---

## 5. 分层职责

| 层 | 该做 | 不该做 |
|---|---|---|
| `src/views` | 编排状态、表单行为、组合 store/service | 散落 URL 拼接、token 注入、错误提示、权限路由推导 |
| `src/components` | 可复用 UI / 局部业务部件 | 长期堆只服务单页面的复杂流程 |
| `src/service/api` | 接口封装、参数归一化、查询拼装、类型对齐 | 在 views/store/components 重复手写接口地址和序列化 |
| `src/service/request` | 统一鉴权/加密/成功码/token 刷新/错误处理 | 平行引入新的 axios/fetch 链绕开封装 |
| `src/store/modules` | 跨页面共享状态 | 把临时局部状态堆进全局 store |
| `src/router` & `build/plugins/router.ts` | 路由/菜单/权限标识/首页/路由 meta | 在页面里临时写条件分支替代正式配置 |
| `src/layouts` & `src/theme` | 全局布局壳与主题 | 在业务页面复制平行布局/主题状态 |

---

## 6. 业务页面开发风格

- **页面组件保持"编排层薄"**：页面文件主管搜索参数、表格 hook、列定义、弹层开关、接口编排。
- 列表页拆同目录 `modules/*`：搜索组件、操作弹层、详情抽屉、资源面板等。
- **参考实现**：系统管理下 `user`/`role`/`menu`/`dict`。
- 列表 hook 优先复用：`src/hooks/common/table.ts` 的 `useUIPaginatedTable`、`useTableOperate`、`defaultTransform`。
- 表单 hook 优先复用：`src/hooks/common/form.ts` 的 `useForm`、`useFormRules`。
- **业务口径是"内网中文优先"**：新页不必强行国际化；但已有大量 `$t(...)` 的页面继续开发时，保持局部一致，不要中文/i18n 混用。

---

## 7. 表格、搜索区、操作列

### 7.1 搜索区（强约束）

- **必须用** `src/components/custom/table-search-fields.vue` 的 `fields` 声明式配置，不得手写 `ElRow/ElCol/ElFormItem` 骨架。
- 仅当字段存在复杂联动、自定义插槽或 `TableSearchFields` 明确无法承载时，才退回 `src/components/custom/table-search-panel.vue`，并在实施说明中写明原因。
- **搜索区按钮组固定在第一行最后一格**；存在折叠时按钮顺序固定为 **展开/收起 → 重置 → 查询**。**不允许**因查询条件不足、展开收起或响应式样式把按钮提前或挤到下一行。
- `columns` 表示首行总格数，**最后 1 格永远留给按钮**；字段不足 `columns - 1` 由组件补空占位；超过则进入展开区。
- 4 个查询条件的场景必须 `:columns="4"`（3 条件 + 按钮）。
- 搜索模块只接 `model` 和必要选项，只发 `reset`/`search`，**不直接承载列表请求**。
- 详细规范见 `docs/table-search-fields-usage.md`。

### 7.2 表格

- 操作列优先复用 `src/components/custom/business-table-action-cell.tsx`。
- 操作数 ≤ 2：直出；操作数 > 2：**1 个直出主按钮 + 1 个更多按钮**。
- `ElCard` 承载 `ElTable height="100%"` 时，`body-class` 优先用公共类 **`business-table-card-body`**（由 `src/styles/scss/element-plus.scss` 维护）。**不要为每页新建 `xxx-table-card-body` 私有样式**。历史私有类不强制专项回改，触达再收敛。
- 表格/按钮/弹层/表单的尺寸与间距标准走 `element-plus.scss` 和公共组件，**不要在业务页散落写局部尺寸作为事实标准**。

---

## 8. 表单与弹层（强约束）

### 8.1 组件选择

- 标准组合：`ElDialog / ElDrawer / ElForm / ElScrollbar / #footer`。
- 轻中量表单：`src/components/custom/business-form-dialog.vue`。
- 字段较多 / 需保留列表上下文 / 重型控件：`src/components/custom/business-form-drawer.vue`。
- 表单分组：`src/components/custom/business-form-section.vue`。

### 8.2 Dialog 宽度三档（按纯表单字段数）

| 字段数 | preset | 默认列数 | 目标宽度 |
|---|---|---|---|
| ≤ 6 | `sm` | 单列 | 520px |
| 7 ~ 14 | `md` | 双列 | 720px |
| > 14 | `lg` | 双列为主 | 960px |

- 实际宽度上限：`calc(100vw - 32px)`。
- **不因为单个 textarea 自动升档**，不做列数响应式折叠。
- 归到 `sm` 时不能只改 preset，**字段布局也要落到单列**：常规 `ElCol` 用 `span=24`，除非已判定为复合内容特例。

### 8.3 复合内容特例

左右分栏 / 表单+表格 / 表单+树 / 关系编辑器 / 时间线 / 大段说明区 → 不强按字段数归类，按内容复杂度评估 `md`/`lg` 或更宽。**只有无法合理归入"纯表单三档"时才允许特例。**

### 8.4 表单布局

- 常规 CRUD：`label-position="top"` + `ElRow + ElCol` 双列 + `gutter=16`。
- 普通字段 `span=12`；长文本/重量级字段 `span=24`。
- 字段 ≤ 6 默认按单列理解。

### 8.5 其他

- **禁止**用页面级宽范围样式覆盖整页 `.business-form-dialog` 来统一放大；如需特殊宽度，必须精确作用于目标弹框，不误伤同页其他 dialog。
- 底部按钮固定 **取消 → 确认**，右对齐。
- 单选组/开关字段优先复用既有钩子：`business-form-radio-group`、`business-form-switch-field`。
- **权限按钮默认"无权限不渲染"**；只有业务状态暂时不可操作但仍需让用户感知入口存在时，才允许保留禁用态。

### 8.6 全局反馈（Toast / Message）

- **全局反馈通道只有一个**：`window.$message`（`src/components/common/app-provider.vue` 注入的 `ElMessage`），全仓 30+ 处都用它。**不要平行引入 `ElNotification` / 自定义 toast**；要求"全局风格切换"则单独立项，不要在小改动里悄悄启动。
- **type 语义**（4 种 type → 3 类视觉语义）：
  - `error` → 错误（红）：操作失败、明确异常
  - `warning` → 告警（橙）：用户即将出错、风险确认
  - `success` → 通知-成功（绿）：操作成功
  - `info` → 通知-信息（蓝）：信息告知、默认兜底说明
- **type 选错就丑**：`warning` 是"出错警告"，不要拿来表达普通信息（用 `info`）；`info` 是"信息告知"，不要拿来报错（用 `error`）。
- **"先做 A 再做 B" 的引导性提示**：用 `ElFormItem :error="msg"` 红字内联（跟校验同款），**不要用 toast**——toast 适合事后反馈、不阻断流程，对引导性提示体验差。
- **全局视觉**（实色背景 + 白字 + 阴影 + `$radius` 圆角）由 `src/styles/scss/element-plus.scss` 末尾的 `.el-message` 块统一维护，**业务页面禁止覆盖** `.el-message-*` 样式。要调颜色就改 `element-plus.scss`，不要在业务页 scoped 散落。

```ts
window.$message?.success('保存成功');
window.$message?.error('保存失败：xxx');
window.$message?.warning('当前修改未保存，确认离开？');
window.$message?.info('未选择计划开始日期，已按今日为基准计算');
```

---

## 9. 接口、路由、权限

- 默认走 `src/service/request/index.ts`，不另造鉴权/加密/错误处理/token 刷新。
- 接口前缀、服务常量优先复用 `src/constants/service.ts`。
- 后端契约变化时同步检查 `src/service/api/*`、`src/typings/api/*`、相关页面、说明文档。
- 路由/菜单/权限改动时同步检查 `build/plugins/router.ts`、`src/router/routes/*`、`src/store/modules/route/*`、相关文档。
- 路由产物过期：改源配置 + `pnpm gen-route`，**不要把手工修补生成文件当常规方案**。

---

## 10. 运行时字典

- 由 `src/store/modules/dict/index.ts` 管理，登录后通过 `/system/dict-data/frontend-cache` 初始化。**不要在页面重复直调字典接口。**
- 字典编码常量收敛在 `src/constants/dict.ts`。**不要散落硬编码 `dictType`。**
- **不要猜字典编码**：先从后端接口文档/字段契约/系统字典管理页确认真实 `dictType`，再写入常量。
- 常量加中文注释：对应业务字段 + 编码确认来源。
- 后端编码带历史命名痕迹（如 `rdms_product_direction`）时，前端常量名按真实业务语义命名，**不扩散历史误导**。

### 字典使用方式

| 场景 | 组件/Hook |
|---|---|
| 表单下拉 | `src/components/custom/dict-select.vue` |
| 普通文案回显 | `src/components/custom/dict-text.vue` |
| 标签态回显 | `src/components/custom/dict-tag.vue`（标签颜色业务页自决） |
| script setup / TSX 列格式化 / 复杂判断 | `src/hooks/business/dict.ts` 的 `useDict(dictCode)` |

`useDict` 常用能力：`dictOptions`、`getItem`、`getLabel`、`getLabels`、`hasValue`。

`DictSelect` 默认只展示启用项；需包含禁用项显式 `:only-enabled="false"`。

```vue
<DictSelect v-model="form.directionCode" :dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE" />
<DictText :dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE" :value="row.directionCode" />
<DictTag :dict-code="SYSTEM_USER_COMPANY_DICT_CODE" :value="row.companyCode" type="info" />
```

```ts
const { getLabel, getLabels } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const directionLabel = getLabel(row.directionCode);
const directionLabels = getLabels(row.directionCodes, { separator: '，' });
```

---

## 11. 页面资源 & 菜单目录（三层不同概念）

- `component` = 渲染哪个页面组件
- 菜单目录 = 挂在哪个业务目录、最终 URL
- 页面资源 = 白名单中选择并回填组件信息

**不要混淆**：组件键 `view.system_dict` ≠ 必须挂 `/system/dict`，同一个组件允许在新业务目录下复用。

页面资源白名单的标准路径是**参考路径，不应反向覆盖菜单树已确定的最终 URL**。

菜单编辑器/页面资源选择逻辑改动时，保证"组件可解析、资源合法、最终 URL 由菜单树决定"，**不要强绑标准路径与父目录前缀**。

---

## 12. ID 类型铁律（强约束，必须严格执行）

> 后端主键 ID / 用户 ID / 对象 ID / 雪花 ID / Long ID **一律按 `string` 接收和传递**。

**原因**：JS `number` 无法稳定承载 Long 精度；序列化精度丢失；`number/string` 键不一致 → 回显/筛选/映射/路由参数/对象上下文异常。

### 落实范围（全部）
`typings`、API 返回类型、表单 model、组件 props/emits、`ElSelect` 的 value、路由参数、查询参数、`Map` 键、筛选条件、store 状态 → **全部 `string` / `string[]`**。

### 禁止写法
- ❌ `Number(id)` / `+id` / `parseInt(id)` / `parseFloat(id)` / `Math.floor(id)`
- ❌ 任何"为了比较/传参/回填/提交而把 ID 转 number"

### 比较与映射
- ✅ `id === targetId`
- ✅ `Map<string, T>` / `Set<string>`
- ❌ 不混用 `number/string` 双口径

### 后端契约风险（关键）
- 后端暂返数值型 ID 时，**前端在 `typings` / API 适配层 / 进业务层前转 `string`**，不要按 `number` 扩散。
- **但如果后端把超 JS 安全整数的 Long 直接作为 JSON 数字返回，前端再 `String(number)` 只能得到"已经丢精度后的错误字符串"**。这种情况必须明确记为接口契约风险，不能误判为"已安全处理"。
- 最稳妥契约：**后端 Long ID 直接按字符串返回**；前端全链路按字符串。后端未改，前端也不得新增 `number` 口径 ID。

### API 适配层兜底（操作约束）
- 所有从后端接收的数值型 ID 字段，**必须**在 `src/service/api/*` 的 normalize/map 函数里显式 `String(rawId)` 一次——**不管后端返回 string、number、还是混合**。
- 业务层（views / store / 组件 / `Map` key / 路由参数）**只接收 string**，从不需要自己 `String()`。
- 与"后端是否已经全局 Long → String"**无关**：
  - 后端做了 → 双保险
  - 后端没做但取值在 JS 安全整数内 → 单层防御也对（实际值不丢精度）
  - 后端没做且取值超安全整数 → 不安全，必须推后端改
- **不开"按取值范围豁免"的口子**：哪怕后端说"这个字段永远是两位数"（如 `infra_file_config.id`），前端照样 `String()`。否则后续会冒出"projectStatus 是 Long 但只有 0-99，也可以保留 number"等连锁例外，铁律字面被掏空。

### 历史代码原则
不再新增 `number` 口径；当前任务触达相关链路时**顺手矫正**；不要继续复制历史写法。

---

## 13. 代码约定

- 优先用别名导入（`@/...`、`~/...`），避免长相对路径。
- 与 TypeScript 严格模式兼容。
- 沿用 Vue SFC 风格：`script setup`、类型化 store、职责单一的小型 composable/helper。
- UI 沿用 `src/layouts` 和 `src/theme` 现有模式，不平行引入新设计体系。
- **注释克制**：只在代码本身不直观时补必要中文说明；不删原有有效注释；不写没信息量的注释。
- 中文内容用 UTF-8，自检显示；**不要用改成英文规避编码问题**。
- Node ESM 脚本：避免 `__filename`/`__dirname` 这类下划线悬挂命名。
- 批量异步并发优先 `Promise.all(...)`，不在循环里默认 `await`。
- 手写 `new Promise(...)` 用 block 写法，不要写成隐式返回的单表达式箭头函数。
- 函数若同时承担"判断 + 转换 + 组装 + 递归"，拆 helper。

---

## 14. 校验

### 14.1 校验口径

| 任务类型 | 默认校验 |
|---|---|
| 前端页面/交互/样式 | `pnpm typecheck`，不主动跑测试 |
| 需更严格静态检查 | 加 `pnpm lint` |
| 涉及路由 | 加 `pnpm gen-route` |
| 影响页面资源清单/菜单资源选择/页面白名单 | 加 `pnpm gen:page-resource-manifest` |

### 14.2 静态校验自查清单
- 调用链是否闭环？改动是否在正确分层？
- 路由/菜单/权限标识/主题状态/资源注册 是否前后一致？
- 改动范围是否控制在最小集合？
- 文档/类型/接口封装/生成产物 是否需要同步更新？

---

## 15. 提交规范

- **`pre-commit` 执行 `pnpm typecheck && pnpm lint && git diff --exit-code`**：能跑 ≠ 能提交。
- `pnpm lint` 会跑 `eslint . --fix`：提交失败后检查是否有被自动修复但未重新暂存的文件。
- 推荐提交方式：`pnpm commit:zh`（交互选 type/scope/description）。
- 手动提交：`git commit -m "type(scope): 描述"`，参考 `docs/前端提交规范与示例.md`。
- `commit-msg` 钩子校验 Conventional Commits。

---

## 16. 协作记忆（与本仓库用户共事）

- 用户语言：**中文**（始终用中文回复）。
- **不主动跑 git 命令**（用户已强调）。
- 默认精简、结论先行。
- 工作树脏时不要回退无关变更。
- 改架构/权限/页面规范前先翻 `docs/`，避免与现有约定冲突。
- 改布局/主题时同时检查 `src/layouts/*` 与 `src/store/modules/theme/*`。
- 改路由/菜单时同时检查 `build/plugins/router.ts` 与 `src/router/routes/*`。

---

## 17. 常用命令速查

```bash
pnpm typecheck                       # 最小静态校验
pnpm lint                             # eslint . --fix
pnpm gen-route                        # 重新生成路由产物
pnpm gen:page-resource-manifest       # 同步页面资源清单
pnpm commit:zh                        # 交互式提交（推荐）
pnpm dev                              # dev server (9527)
pnpm preview                          # preview server (9725)
```

---

## 18. 业务对象状态颜色

- 集中文件：`src/constants/status-tag.ts`
- 各业务域 `statusCode → ElTag type` 在此统一维护，**不要在各页面散落硬编码**。
- 已支持域：`projectExecution`、`projectTask`；预留：`project`、`product`、`requirement`、`workOrder`。
- helper：`getStatusTagType(domain, statusCode)`，未匹配回退 `'info'`。
- 业务模块写薄包装，例如 `getExecutionStatusTagType(code) = getStatusTagType('projectExecution', code)`。
- 新增对象域：在 `StatusDomain` 加枚举 + `statusTagTypeRegistry` 加对应 map；调用方写一个 wrapper 即可。
- 后端契约：未来若状态字典返颜色字段，调用方优先取后端值，缺失时回退 helper（前端兜底）。

---

## 19. 防重复提交（两层联防，强约束）

> 用户双击、键盘连按 Enter、`ElMessageBox.confirm` 的"确定"按钮无内置 loading 都会让同一写操作发多次。两层防御缺一不可。

### 两层各自的职责

| 层 | 谁负责 | 行为 |
|---|---|---|
| 视觉层 | `business-form-dialog.vue` / `business-form-drawer.vue` | submit 触发后立即把"确认"按钮置 loading + disabled，挡住二次点击 |
| 逻辑层（兜底） | `src/service/request/dedupe.ts`（已通过 `withDedupe` 包住 `request` 实例） | 写操作 pending 期内复用同一 Promise，不真正发出第二次请求 |

### 业务侧关注点

- **不要裸手写** `<ElButton @click="submit">` 调接口；用 `business-form-dialog` / `business-form-drawer` 包；非要用裸 `ElButton` 时**必须**自行绑 `:loading` 并在 await 期间锁住。
- **`ElMessageBox.confirm` 的"确定"按钮没 loading 能力**——不要尝试改它，靠第二层兜底就够。
- **新接口默认享受去重**，调用方零改动；不要在 `src/service/api/*` 或页面层再造一套去重。

### 去重生效边界

- 自动去重：`POST` / `PUT` / `DELETE` / `PATCH`。
- 不去重：`GET` / `HEAD` / `OPTIONS`（避免误伤分页 / 多 widget 并发查询）；请求体为 `FormData` / `Blob`（上传场景）。
- 单接口逃生口：`request({ ..., dedupe: false })`——极少用，仅当业务真允许短时间内连发完全相同的写请求。
- 兜底超时 30s：保险丝，防止 Promise 永不 settle 时内存泄漏。

### 指纹算法

`method 大写 | URL + 排序后的 params 序列化 | 稳定序列化的 body`。body 内对象按 key 排序、数组保序——保证调用顺序不同但参数等价的两次请求拿到同一指纹。

### 何时回到本节查

- 新建写操作页面 → 视觉层用对组件、不裸 `ElButton` 调接口
- 新建写接口 → 不用管，默认兜底；只有明确"允许短时间连发"才传 `dedupe: false`
- 新建上传 / 下载流程 → FormData / Blob 天然不在去重范围，按原方式写即可
- 用户报"双击双发"复现不出来 → 检查目标按钮是否走了 `business-form-dialog`；若用的是 `ElMessageBox.confirm`，靠第二层就该挡住

---

## 20. 我生成文档的输出格式（强约束）

- **superpowers 工作流（`docs/superpowers/plans/`、`docs/superpowers/specs/`）下输出的文档继续用 `.md`**——工作流以 markdown 为前提。
- **其他**我生成的文档（设计方案、复盘、规约、技术经验沉淀等）**默认用 `.html`**，沿用 `docs/debt/` 现有 HTML 文档（参考 `token-刷新机制对齐分析.html`、`技术负债台账.html`）的样式骨架：
  - 单文件、内联 CSS
  - `max-width: 980px` 居中容器、`padding: 32px 28px 80px`
  - 14px / `line-height: 1.7`、`PingFang SC` / `Microsoft YaHei` 中文字体优先
  - 模块化区块：`section` + 编号 h2、`card`、`table.cmp`、`pre`、`tag-ok/warn/bad/crit`
  - 配色用 `--bg / --panel / --border / --text / --primary` 一套 CSS 变量
- **`README.md`** 是目录索引约定文件，**保持 `.md`**（不强行 `.html`）。
- **已有 `.md` 文档不主动改写**，等用户明确要求再转。
