Files
cn-rdms-web/CLAUDE.md

25 KiB
Raw Permalink Blame History

CLAUDE.md

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

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


0. 行为基线(最重要,先记住)

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

1. 项目骨架(认知地图)

维度 现状
应用 RDMS 系统的 Vue 3 后台前端
包管理 pnpm>=8.7.0Node >=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.tssrc/router/routes/custom-routes.ts
  • i18nKey 是兼容字段,不是新页必须补齐项。

4.1 对象上下文业务域(重要陷阱)

  • productproject 这类业务域,入口页是设计如此:先进业务域入口页 → 再选对象建上下文。不要把"入口页是可点击菜单"误判成 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.tsuseUIPaginatedTableuseTableOperatedefaultTransform
  • 表单 hook 优先复用:src/hooks/common/form.tsuseFormuseFormRules
  • 业务口径是"内网中文优先":新页不必强行国际化;但已有大量 $t(...) 的页面继续开发时,保持局部一致,不要中文/i18n 混用。

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

7.1 搜索区(强约束)

  • 必须用 src/components/custom/table-search-fields.vuefields 声明式配置,不得手写 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直出操作数 > 21 个直出主按钮 + 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字段布局也要落到单列:常规 ElColspan=24,除非已判定为复合内容特例。

8.3 复合内容特例

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

8.4 表单布局

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

8.5 其他

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

8.6 全局反馈Toast / Message

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

useDict 常用能力:dictOptionsgetItemgetLabelgetLabelshasValue

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

<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" />
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/layoutssrc/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.tssrc/router/routes/*

17. 常用命令速查

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 在此统一维护,不要在各页面散落硬编码
  • 已支持域:projectExecutionprojectTask;预留:projectproductrequirementworkOrder
  • helpergetStatusTagType(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.7PingFang SC / Microsoft YaHei 中文字体优先
    • 模块化区块:section + 编号 h2、cardtable.cmppretag-ok/warn/bad/crit
    • 配色用 --bg / --panel / --border / --text / --primary 一套 CSS 变量
  • README.md 是目录索引约定文件,保持 .md(不强行 .html)。
  • 已有 .md 文档不主动改写,等用户明确要求再转。