25 KiB
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 |
已经形成闭环的五条主线,后续改动顺着做,不平行起新的:
- 路由来源统一:页面文件 + 自定义路由 →
elegant-router生成 →build/plugins/router.ts集中补meta。 - 权限入口统一:常量路由 / 权限路由分流;
route store负责初始化、菜单生成、缓存路由、面包屑。 - 请求入口统一:所有业务请求走
src/service/request/index.ts。 - 页面套路统一:列表页 = 搜索区 + 表格区 + 操作弹层/抽屉 +
modules/*子组件。 - 衍生资产统一:页面资源白名单从路由结构生成,不手工维护第二份。
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.tssrc/router/elegant/routes.tssrc/router/elegant/transform.tssrc/typings/elegant-router.d.tssrc/typings/components.d.tsdocs/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 散落。
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"。
<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 / 组件 /
Mapkey / 路由参数)只接收 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. 常用命令速查
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文档不主动改写,等用户明确要求再转。