diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9dcd653..2699c67 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,13 @@ "WebFetch(domain:raw.githubusercontent.com)", "Bash(Remove-Item *)", "PowerShell(pnpm typecheck *)", - "WebFetch(domain:www.wangeditor.com)" + "WebFetch(domain:www.wangeditor.com)", + "Bash(node *)", + "Bash(dir \"rdms-project-boot-*\")", + "Bash(git stash *)", + "Bash(pnpm eslint *)", + "Bash(Select-String -Pattern \"business-rich-text-editor|task-operate-dialog\")", + "Bash(powershell *)" ] } } diff --git a/AGENTS.md b/AGENTS.md index 307b299..59bd2d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,8 @@ 默认回答保持精简,优先给结论、改动点、验证方式和必要风险;如果用户只要求分析、审阅或方案,就停留在分析层,不主动扩展到实现层。 +分析、解释、方案类回答优先用业务和逻辑语言把结构、差异与结论说清楚,不要大段贴源码、罗列 `file:line` 或把实现细节当解释;只有用户明确要求看代码、或某行确实是讨论焦点的关键佐证时,才贴最小必要的代码片段。 + ## 交互与执行原则 - 进入实施阶段前,先说明目标、涉及模块、预计改动点和验证方式。 @@ -173,6 +175,31 @@ - 涉及路由、菜单、权限的改动时,同时检查 `build/plugins/router.ts`、`src/router/routes/*`、`src/store/modules/route/*` 和相关文档。 - 对于可再生的路由产物,优先修改源配置并执行 `pnpm gen-route`,不要把手工修补生成文件当成常规方案。 +## 防重复提交(两层联防) + +用户快速双击、键盘连按 Enter、`ElMessageBox.confirm` 的"确定"按钮内置无 loading 等场景,都可能让同一写操作发出多次。仓库采用两层防御,新增写操作功能时按顺序检查: + +### 第一层:业务按钮的 loading 锁(视觉防御) + +- 新增、编辑入口优先使用 `src/components/custom/business-form-dialog.vue` 或 `src/components/custom/business-form-drawer.vue`,它们在 `submit` 流程内 await 接口期间会自动将"确认"按钮置为 `loading` + `disabled`。 +- 不要裸手写 `` 直接调接口;若必须使用裸 `ElButton`,需要自行绑定 `:loading` 并在 await 接口期间锁住按钮。 +- 删除二次确认使用 `ElMessageBox.confirm` 时,其内部"确定"按钮没有 loading 能力,必须依赖第二层兜底,不要尝试改造 confirm 的内部按钮。 + +### 第二层:请求层全局去重(逻辑兜底) + +- 入口:`src/service/request/dedupe.ts` 提供 `withDedupe`,已在 `src/service/request/index.ts` 包住统一的 `request` 实例;`demoRequest` 未启用。 +- 指纹:`method + 完整 URL + 排序后的 params + 稳定序列化的 body`;body 内对象按 key 排序,数组保序。 +- 行为:写操作(`POST` / `PUT` / `DELETE` / `PATCH`)在第一次请求 pending 期内,若再次发起指纹相同的请求,自动复用第一次的 Promise,不发出第二次实际请求;调用方两次拿到完全相同的返回对象。 +- 跳过条件(即不去重,按原逻辑发出):`GET` / `HEAD` / `OPTIONS`,请求体为 `FormData` 或 `Blob`(上传场景),调用方显式传 `{ dedupe: false }`。 +- 业务调用方零感知:新增接口默认即享受兜底,不需要在 `src/service/api/*` 或页面层做任何改动。 +- 极少数业务确实允许短时间内并发提交完全相同的写请求时,在调用处显式传 `request({ ..., dedupe: false })` 单接口关闭。 +- 兜底超时 30 秒:极端情况下若某次 Promise 未 settle,pending 条目过期后下一次相同请求视为新请求,避免内存泄漏。 + +### 设计责任划分 + +- 视觉层负责"按下立刻锁住按钮"的用户感知;逻辑层负责"即使锁失败也只发一次"的实际接口保护。 +- 不要因为有第二层兜底就省略第一层 loading 锁:用户没有视觉反馈会再次点击;也不要试图在业务页面再造一套请求去重逻辑。 + ## 运行时字典使用口径 - 运行时字典统一由 `src/store/modules/dict/index.ts` 管理,登录后通过 `/system/dict-data/frontend-cache` 初始化;不要在业务页面重复直调字典接口。 diff --git a/CLAUDE.md b/CLAUDE.md index 46e4803..da5109c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ - **描述现状以代码、配置、文档可直接验证的事实为准**;不引入历史实现/过渡方案/猜测。 - **默认精简回答**:先给结论 → 改动点 → 验证方式 → 必要风险。**除非用户主动要求详细,否则不要展开**——不复述清单、不列每条改动的小理由、不堆"汇总"段。用户只让分析就停在分析层,不主动跳到实现。 +- **分析/解释类回答不要堆代码层面描述**:默认用业务/逻辑语言说清楚结构、差异与结论;不要大段贴源码、不要罗列 `file:line`、不要把"实现细节"当解释。只有用户明确要求看代码、或非贴不可的关键佐证(如某行就是争议焦点),才贴最少代码片段。 - **进入实施阶段前,先说目标、涉及模块、预计改动点、验证方式**。 - **最小改动原则**:只改当前任务必需的范围,不顺手重构无关代码。 - **不主动执行 git 操作**(status/diff/add/commit/restore/reset/checkout 全部不主动跑),除非用户明确要求。识别用户改动优先用 Read 直接看文件。 @@ -368,3 +369,40 @@ pnpm preview # preview server (9725) - 业务模块写薄包装,例如 `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,不真正发出第二次请求 | + +### 业务侧关注点 + +- **不要裸手写** `` 调接口;用 `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`,靠第二层就该挡住 diff --git a/src/components/custom/business-attachment-uploader.vue b/src/components/custom/business-attachment-uploader.vue new file mode 100644 index 0000000..0cff3f3 --- /dev/null +++ b/src/components/custom/business-attachment-uploader.vue @@ -0,0 +1,718 @@ + + + + + + + diff --git a/src/components/custom/business-rich-text-editor.vue b/src/components/custom/business-rich-text-editor.vue index 0316d17..8bdf9d9 100644 --- a/src/components/custom/business-rich-text-editor.vue +++ b/src/components/custom/business-rich-text-editor.vue @@ -1,9 +1,10 @@ diff --git a/src/views/product/list/modules/product-create-team-step.vue b/src/views/product/list/modules/product-create-team-step.vue new file mode 100644 index 0000000..f734ffc --- /dev/null +++ b/src/views/product/list/modules/product-create-team-step.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/src/views/product/list/modules/product-operate-dialog.vue b/src/views/product/list/modules/product-operate-dialog.vue index 7bdbd4a..2ced26e 100644 --- a/src/views/product/list/modules/product-operate-dialog.vue +++ b/src/views/product/list/modules/product-operate-dialog.vue @@ -1,11 +1,14 @@