Files
workspace/code/fms/开发规范.md
T
2026-09-22 17:29:39 +08:00

242 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发规范(AI 使用)
## 通用原则
1. **不搞 placeholder** — 代码中不得出现未实现的占位符、临时桩代码或待填充的 TODO 占位。
2. **后端接口优先通用** — 优先使用或设计通用接口,尽量不写特殊接口(为单一业务场景定制的接口)。如需编写特殊接口,必须提前询问用户并获得确认。
3. **先读现有代码再改** — 修改前先确认已有组件、store、接口和测试的职责边界,优先复用现有模式,不因局部需求引入新的架构分支。
4. **修改范围最小化** — 只修改完成当前需求所必需的文件;发现无关的脏改动、临时文件或历史问题时,不擅自回滚或顺手重构。
5. **当前系统按内部 ERP 处理** — 默认使用场景是已登录的内部用户和受控网络。当前需求阶段不额外引入复杂的安全防护层或前后端重复校验;除非用户明确提出,不因安全假设新增接口、中间层或配置系统。查询条件和查询 SQL 可以由调用方传入,后端重点完成查询、分页、数据格式转换以及只执行查询语句的边界控制。
6. **SQL 驱动优先** — 本系统是 SQL 驱动的内部 ERP。能通过 SQL 完成的查询、关联、过滤、排序、分页、聚合、权限过滤和数据范围过滤,优先在 SQL 层完成,不在业务代码中加载大量数据后再处理。
7. **优先使用配置驱动的动态 SQL** — 模块、字段、查询条件、排序、字段权限和数据范围等存在于表结构中的规则,应由通用查询逻辑读取配置并动态生成 SQL。对于需要快速适配复杂场景的查询,允许调用方或模块配置直接传入完整 SQL;该入口只允许执行 `SELECT` 查询,不限制表、字段、关联、函数或条件的具体写法。结构化条件和参数化值仍可按调用场景使用。
8. **原生 SQL 仅限查询** — `b_query_sql`、查询 SQL 扩展和调用方传入的 SQL 可以是任意合法的 `SELECT` 语句,包括子查询、CTE、关联、聚合、排序和组合条件。禁止执行 `INSERT`、`UPDATE`、`DELETE`、DDL、存储过程调用及其他非查询语句;保存和业务写入仍走模块保存逻辑或业务处理器。
9. **固定 SQL 只用于稳定且明确的特殊逻辑** — 当 SQL 由固定业务规则组成,或动态生成会明显降低可读性、性能和可维护性时,可以使用固定 SQL。固定 SQL 应保持通用,不为单个页面或单个用户复制一套接口和查询。
10. **权限过滤以后端 SQL 为准** — 前端可以根据权限隐藏菜单、按钮和字段,但最终的模块操作权限、字段可见/可查询/可导出权限以及数据范围,必须在后端查询或写入 SQL 中落实。用户个性化配置只能调整布局,不能放宽权限。
SQL 驱动的实现应优先复用通用查询、保存和权限计算逻辑。模块之间的差异通过 `s_module`、`s_field`、`s_module_schema`、权限表和数据范围表配置;新增模块或字段时,优先增加配置数据,不复制一套模块专用 SQL。查询既可以由模块元数据动态组合,也可以使用模块配置或调用方传入的完整 `SELECT` 语句;不得把非查询 SQL 交给执行器。
## 前端代码风格
1. **少用语法糖** — 优先使用直白、易读的写法,避免为了炫技而使用语法糖,可读性优先。
2. **避免无意义的封装** — 没有必要的抽取就不抽取,不为一层简单调用再包一层方法。例如 `emit('xxx')` 这类直接调用即可,无需再封装成自定义方法。
3. **按目录归属组织公共代码** — 只服务于某个业务目录的函数,放在该目录下的 `utils.js`;只服务于布局、页面或组件的函数,放在对应目录,不放入全局 `src/utils`。
4. **公共函数抽取条件** — 满足以下任一条件才抽取:
- 至少有两个独立调用点;
- 逻辑本身较复杂,内联会明显影响可读性;
- 需要保证多个调用点使用同一规则(例如缓存名称、字段 key 生成规则)。
只有一个调用点且逻辑简单的代码直接写在使用处;没有调用点的代码必须删除。
5. **公共工具文件数量控制** — 一个业务目录原则上只保留一个公共 `utils.js`。只有职责明显独立、体量较大或确实需要单独测试的特殊工具,才允许单独成文件。禁止为了一个函数创建一个文件。
6. **避免重复实现同一规则** — 即使函数很短,只要多个位置必须保持完全一致,就集中维护一个实现,不在各处复制字符串拼接、编码或判断逻辑。
7. **命名反映职责** — 目录已经表达业务上下文时,文件名不再重复目录名;优先使用 `utils.js`、`constants.js` 等稳定名称,避免创建 `xxxHelper.js`、`xxxCommon.js` 等含义模糊的文件。
8. **为真实出现的第二个使用方而抽象** — 组件、组合式函数或页面级结构同理:只有出现第二个真实、形态接近的调用方(或确有确定性的同类页面即将落地)才抽取;不为"将来可能复用"预先抽一层,避免基于猜测设计接口。形似但数据与行为各异的页面(如模块树与菜单树),先各自平铺实现,等共性真正清晰后再合并。
### 命名约定
1. **按代码层次区分命名风格** — 前端 JavaScript 逻辑标识符使用 `camelCase`;后端返回字段、数据库字段和与表结构一一对应的业务数据使用 `snake_case`。不要为了追求全项目单一风格而把两类名称混在一起。
```js
// 页面状态、局部变量和方法:camelCase
const moduleTreeLoading = ref(false)
const selectedModuleId = ref(null)
async function loadModuleConfig() {}
// 后端业务数据和原始基线:snake_case
const maindata = ref(null)
const maindata_org = ref(null)
const view_schema = ref(null)
const view_schema_org = ref(null)
```
2. **方法名统一使用 camelCase** — 采用职责明确的动词开头,例如 `loadModuleConfig()`、`handleSave()`、`buildSaveData()`、`clearModuleData()`。不使用 `loadMod()`、`dealData()`、`doIt()` 等含义不明确的缩写或泛化名称。
3. **禁止单词直接拼接** — 不使用 `maintableorg`、`subtablereceiveorg` 这类无法直观看出单词边界的名称。数据与基线必须使用 `_org` 分隔,例如 `maindata / maindata_org`、`fields / fields_org`。
4. **状态名称要能表达类型和职责** — 布尔值优先使用 `is`、`has`、`can`、`should` 前缀(如 `isLoading`、`hasChanges`、`canDelete`);ID 统一使用 `xxxId`;数组、集合使用复数名(如 `fields`、`unsavedModuleIds`)。避免使用 `data`、`info`、`flag`、`temp` 等无法说明职责的名称。
5. **避免含义不明的缩写** — 除领域内已经稳定且广泛使用的缩写(如 `id`、`api`、`i18n`)外,优先使用完整单词。例如使用 `containerData`,不使用 `zxData`;使用 `loadEdit()`,不使用 `loadMod()`。已有外部字段名或历史接口字段不因本规则擅自改名。
6. **API 层做命名边界转换** — API 封装函数的参数和前端局部对象使用 `camelCase`,请求体字段必须按后端契约保留原名(通常为 `snake_case`)。不得因为前端命名调整而修改接口字段、数据库字段、路由 meta、组件 props 或第三方库 API。
7. **Vue 公开接口统一模板命名** — 组件文件名和组件名使用 PascalCase;模板中的 props、事件和具名 `v-model` 使用 kebab-case,例如 `:module-data`、`@save-success`、`v-model:main-data`。组件内部变量和方法仍遵守前述 `camelCase` / `snake_case` 分层规则。
8. **新增代码遵守,旧代码按需迁移** — 不为了统一命名进行无需求的大范围重命名。修改旧代码时只在当前需求涉及的标识符上采用本规则,并同步更新全部引用和测试;除非用户明确要求,不保留仅用于兼容旧名称的转发别名。
### Store 设计
1. **统一使用 Setup Store** — 新增或改造 Pinia store 统一使用 `defineStore('name', () => {}, options)`,状态用 `ref`,派生状态用 `computed`,操作用普通函数。
2. **一个 store 一个领域** — store 只管理一个明确领域。`app.js` 只保存应用壳层状态(主题、侧栏、页签、路由刷新等);权限、会话、i18n、业务配置分别由各自 store 管理。
3. **避免重复状态源** — 同一状态只能有一个真实来源,禁止同时在两个 store 或 store 与工具模块中各维护一份。跨 store 使用时直接调用所属领域的 store,不通过兼容门面转发。
4. **持久化显式声明** — 只有确实需要跨刷新保留的字段才配置 `persist`,并明确 `pick` 范围;临时请求状态、页签状态和加载状态不持久化。
5. **异步职责收敛** — 请求、加载状态、错误状态和刷新方法放在对应领域 store 中;页面只负责触发加载并消费结果,不在多个页面复制同一套请求缓存逻辑。
6. **不为旧接口保留高兼容层** — 当调用方已可控且用户未要求兼容时,直接迁移调用方并删除旧文件、旧导出和重复状态,不保留只转发一层的兼容文件。
### 查询列表页面设计
1. **查询条件由页面和模块配置共同决定** — 页面需要传入的业务固定条件(例如当前客户、当前单据 ID、费用类型)由业务代码组装;客户在列表或选择器中可以填写的条件,使用模块已有的查询配置(`s_module_schema.b_schema_type = 'query'`)。复杂查询可以直接使用模块的 `b_query_sql` 或调用方传入的完整 `SELECT`。
2. **不为每个业务动作编写特殊查询接口** — 优先使用现有通用列表查询接口,或新增一个通用的记录查询接口。接口接收模块、查询条件、分页和排序参数,不按“历史费用”“历史发货人”等场景分别新增接口。
3. **查询条件保持结构化** — 即使当前阶段信任前端传入的条件,也优先使用字段、操作符和值组成的对象,而不是在页面中散落拼接字符串。这样做是为了让条件容易阅读、复用和排查,不是为了增加安全限制。
```js
{
fieldId: 'mx_dw_id',
operator: 'eq',
value: currentData.b_customer_id,
}
```
4. **不提前做完整低代码查询配置** — 业务规则明确且只在一个页面使用时,直接写在页面代码中。只有同一规则被多个页面重复使用,并且确实需要由配置调整时,才抽取为公共配置。
5. **记录选择器只负责选择** — `FmsRecordPickerModal` 负责查询、筛选、分页和返回选中记录;字段回填由页面或已有映射函数处理;选择器不直接保存数据库。
### 详细表单页面设计
页面加载流程也统一使用以下命名,方便从入口快速定位新建和编辑逻辑:
1. `load()` — 页面加载入口,判断当前是新建还是编辑模式
2. `loadNew()` — 新建模式初始化:加载默认值、生成临时 ID、准备初始配置
3. `loadEdit()` — 编辑模式加载:按主键加载已有主表和子表数据
如果页面不区分新建和编辑,直接使用语义明确的 `loadXxx()`,例如 `loadModuleConfig()`、`loadMenuTree()`。不使用 `loadMod()` 这类含义不明确的缩写。
`initNew()` 只在“清空当前草稿后重新初始化”确实是独立流程时保留;否则直接合并到 `loadNew()`,避免同一套新建逻辑分散在多个方法中。
### 表单数据与变更检测
1. **数据与基线成对命名 `xxx` / `xxx_org`** — 编辑页面加载完成后,每份数据保留"可编辑副本 `xxx`"与"原始基线 `xxx_org`"两个 ref:`xxx` 是页面修改作用的对象,`xxx_org` 是从数据库加载、不可直接编辑的基线(保存 diff、放弃修改还原用)。主对象用 `maindata / maindata_org`,关联数据用 `fields / fields_org`、`view_schema / view_schema_org` 等。**禁止用 `drafts` / `originals` 两个聚合对象,即使页面只有一个主对象也不例外。** 不要为每个字段单独维护一份旧值。
2. **保存使用 diff** — 保存前将当前值与 `_org` 基线做差异对比,只提交新增、修改和删除的数据;没有变化时不调用保存接口。
3. **基线不提前覆盖** — 保存请求成功前不得把当前值覆盖到 `_org` 基线。保存成功后优先重新从服务端加载,拿到后端补充的主键、默认值和规范化数据,再更新 `_org`。
4. **新建数据单独处理** — 新建模式没有真实基线,`_org` 可以用 `null` 或空数组表示;临时 ID 只存在于当前值,提交成功后用后端返回的真实 ID 更新或重新加载。
5. **多表单统一基线** — 主表、子表和配置表分别保留原始数据,但由同一个保存入口统一计算 diff 和提交,避免每个面板自行维护一套保存状态。
标准表单页面统一采用以下保存流程和方法名称,便于快速定位代码。方法可以包含页面自身的业务逻辑,但不要随意改名或调换主流程顺序:
1. `handleSave()` — 保存入口
2. `beforeSave()` — 保存前校验、字段同步和数据准备;返回 `false` 时终止保存
3. `buildSaveData()` — 基于当前草稿构建保存数据;可以返回单个请求、请求数组或 `null`
4. 调用通用保存接口
5. `afterSave()` — 保存成功后的重新加载、状态更新、草稿清理和页面刷新
推荐主流程保持如下结构:
```js
async function handleSave() {
saving.value = true
try {
if (!(await beforeSave())) return false
const saveData = buildSaveData()
if (!saveData) return true
await saveObjectApi(saveData)
await afterSave()
return true
} finally {
saving.value = false
}
}
```
说明:
- `handleSave()` 是页面或表单组件的统一保存入口,不按业务对象改成多个不同名称。
- `beforeSave()` 只做保存前工作,不直接调用保存接口。
- `buildSaveData()` 只负责组装请求,不修改页面流程状态。
- `afterSave()` 必须在保存请求成功后执行;如果包含异步刷新,应使用 `await`。
- 页面上的内联切换、批量操作等独立动作,可以使用 `saveToggle()`、`saveBatch()` 等专用名称,不套用主表单保存流程。
### 面板式配置页(左树右面板)设计
适用:左侧树切换对象、右侧按页签分多个面板编辑同一对象的多份数据(如模块管理的「基础/字段/列表/表单/查询/自动编码/多语言」)。核心分工是**编排层只持有数据,各面板通过 `v-model` 各管各的**:
1. **编排层不写中转回调** — 页面组件维护每份数据,各面板用 `defineModel` 直接读写属于自己的那份;不在父级写 `onModuleUpdate(section, data)` 这类"子面板 emit → 父级按 section 写回"的中转函数,也不在各面板各自重复加载或维护同一份状态。
2. **数据成对命名,不用两个聚合对象** — 每份数据一对 ref:`xxx` 为当前值(面板绑定),`xxx_org` 为该表从数据库加载的原始基线(保存 diff、放弃修改还原用),小写下划线命名。禁止用 `drafts` / `originals` 两个聚合对象包住全部子数据——**即使页面只有一个主对象,也一律 `maindata / maindata_org` 成对命名**。主表行(如 `maindata`)是单对象;字段使用 `fields`,模块界面配置按 `view`、`edit`、`query` 三类维护,分别对应 `s_module_schema` 的配置行;自动编码和多语言资源按各自模块数据维护。
3. **加载就绪以主数据为判据** — 以主表行(如 `maindata`)是否为空决定配置区是否进入「加载中 / 暂无」占位,模板统一 `v-if="maindata"`。整批清空收敛到一个 `clearModuleData()`(或 `clearXxx()`)辅助函数,供切换对象、取消选择、加载失败复用。
4. **跨面板副作用由编排层 watch 承担** — 一个面板的改动要波及另一份数据时(如自动编码字段指定后在表单配置中自动置为只读、取消后还原),由编排层写**幂等** `watch` 联动;面板不直接改不属于自己的数据。
5. **表格编辑就地 patch + 换引用** — 表格组件对同一引用数组的 push 不敏感,行编辑采用"就地改行对象、提交时换新数组引用"触发渲染,不逐行深拷贝后再整表 emit。
6. **模块配置不使用雪花临时 ID** — 模块管理中的 `s_module` 使用业务编码,`s_field`、`s_module_schema`、`s_autocode`、`s_relation` 等配置表使用业务键或联合主键;新增配置行直接使用业务字段组成的临时唯一键,不调用 `nextIdApi`,保存时也不做“临时 ID → 雪花 ID”转换。其他确实使用 `bigint` 雪花主键的业务子表,才适用本条之外的通用临时 ID 方案。
7. **`_` / `v_` 前缀是内部派生字段** — 行内这类字段(参与勾选、受管标记等)不参与 diff、脏判断,也不随保存提交;提交前统一剥离。对于使用临时 ID 的业务表,先完成真实主键和跨表外键重映射,再剥离;模块配置表没有这一步。
8. **`buildSaveData` 可为 async** — diff 出请求后,直接在函数内完成必要的主键和跨表外键处理再返回,无需再单独包一层 `prepareXxx()`;模块配置按业务键和联合主键直接组装,不做雪花 ID 换号。没有任何变化时返回 `null`。其余流程仍按「表单数据与变更检测」的 `handleSave → beforeSave → buildSaveData → 保存 → afterSave` 主流程。
9. **受管多语言不做实时响应** — 模块名/字段名/分组标题等自动生成的多语言行(受管键)只做两件事:加载模块后 reconcile 一次、保存前 reconcile 一次再 diff;不写 watch + 防抖去实时补齐。reconcile 只动默认语言列,非默认语言的人工翻译一律不被覆盖;默认语言列分两种口径:模块名/字段名/分组标题的默认语言值就是名称本身(每次 reconcile 跟随当前名称、界面只读,改文案要改名称),共用动作动词(`action.*`)的默认语言值是可维护的译文(仅补空、不覆盖人工值)。受管行按清单顺序排列(模块 → 字段定义顺序 → 共用动词 → 分组标题),不沿用 `s_i18n` 的 `b_key` 字典序。
### 布局与页签缓存
1. **缓存规则归布局目录** — keep-alive、页签名称、路由包装组件等规则属于布局层,相关函数放在 `src/layouts` 下,不放入全局工具目录。
2. **区分组件 key 与组件 name** — `:key` 用于区分组件实例;`keep-alive :include` 按组件 name 匹配。需要按路由参数独立缓存时,必须为每个 `fullPath` 生成稳定且唯一的 name,并在记录页签和渲染包装组件时使用同一规则。
3. **不为了抽象而抽象** — 如果缓存逻辑只在一个组件中使用,直接放在该组件;只有页签和布局都需要同一缓存名称规则时,才抽成布局目录公共函数。
### 样式与 CSS 组织
1. **全局样式只放语义组合类,不放原子工具类** — 全局样式层(`src/styles/common.scss`)只收跨页面复用的语义骨架,例如 `.fms-page`(页面根)、`.fms-page__body`(左右分栏容器)、`.fms-page__main`(右栏主区)。不在全局定义 `.fms-fill`、`.fms-clip`、`.fms-column` 这类单一职责的原子类:它们把组合责任推给调用方,模板里必然出现 `fms-fill fms-clip`、`fms-fill fms-clip fms-column` 这样的拼串,而且漏拼一个不会报错,只会坏掉一个页面。每种组合形态必须由一个显式命名的类承担。
2. **全局类必须有第二个真实调用点** — 进入全局样式层的类至少要有两个真实调用点;只有一个调用点的样式留在使用页面的 scoped 块中,没有调用点的必须删除。
3. **命名表达"这是什么",不表达"它长什么样"** — 优先使用页面结构、面板、部件名(`fms-page`、`fms-tree-panel`、`fms-toolbar`),不使用视觉结果名(`fms-fill`、`fms-clip`、`fms-flex`)。一个类要靠注释解释"它刻意不含 `overflow: hidden`"才能被理解,说明抽象没找准,应重新设计。
4. **模板里连续出现三个以上 `fms-*` 就是缺语义类** — 页面模板中的 `fms-*` 通常不超过两个(页面根一个、右栏一个)。出现三个及以上连续拼串时,先在全局补出对应的语义组合类,再改调用方。
5. **样式跟着组件走,不跟着页面走** — 组件自身的布局样式写在组件 `<style scoped>` 内,不进全局。一段结构相同的标记在多个页面重复出现时,先抽组件(标记与样式一并收进组件),再删除全局里对应的类;只有真正的跨页面页面骨架才留在全局。像"搜索框 + 动作区 + 可滚动区 + 树"这类左树面板结构,属于面板组件的职责,不拆散到各页面模板和全局样式里。
6. **不靠选择器权重压组件库** — 需要提升选择器权重才能覆盖组件库内部样式时(例如用 `.fms-tree-panel__actions .fms-tool-btn` 去压 `.button-ghost` 的 `padding` / `border` / `hover`),先在组件库补一个变体(如 `Button` 的 `size="icon"`),不要在业务侧堆嵌套选择器和权重注释。
7. **高度撑满走 flex 链,不写百分比高度** — 页面根依靠 `.fms-content` → `.fms-page-wrap` → 页面根的 `flex: 1` + `min-height: 0` 获得确定高度;不写 `height: 100%`、`100vh` 或 `calc(100vh - xx)` 魔数。
存量页面中的原子类调用按需迁移:修改涉及到的页面时顺带改成语义类并同步更新样式定义,不为统一命名单独发起大范围重构(同「命名约定」第 8 条)。
## 多语言(i18n)约定
1. **区分管理界面与业务资源** — module 目录下的模块管理、菜单管理、多语言类型等页面自身面向 IT 人员,界面文案固定使用中文,不把这些管理界面文案接入业务多语言体系;但模块名称、字段名称和 JSON 布局节点等面向客户的业务资源仍可维护 `s_i18n` 资源键和翻译。
2. 面向客户使用的业务页面需要做多语言翻译;模块管理页可以维护这些业务资源的 key 和翻译数据,但不要求管理页自身多语言化。
3. **区分界面语言、语言清单和资源键** — UI 当前语言、UI 翻译目录以及 `s_i18n_type` 语言区域清单统一由 `stores/i18n.js` 管理;`s_i18n.b_locale` 对应语言区域,资源来源通过 `b_key` 的命名空间表达(如 `module.`、`menu.`、`field.`、`group.`),不再依赖 `b_source` / `b_source_id` 字段,页面不得自行维护第二份语言状态。
4. **多语言 key 规则就近维护** — 只服务于模块管理的 key 生成和同步逻辑放在 `module-management/utils.js`;不得放入全局 `src/utils`。单页面使用一次的校验、透视合并或字段转换逻辑直接写在页面中。
5. **默认语言必须按显式配置判断** — 使用 `s_i18n_type.b_default` 确定默认语言,不得依赖语言数组第一项;有效默认语言只能有一个,语言列表为空时不得静默伪造默认语言掩盖接口或配置错误。
### 变更验收
1. **先做引用清理** — 删除或移动文件后,用全局搜索确认没有旧路径、旧导出和旧方法名残留。
2. **开发阶段不运行测试** — 功能开发或修改完成后,以功能可用为完成标准,不在开发过程中运行单元测试、全量测试或构建检查,也不每改一个功能就补一次测试。测试由用户统一安排,等用户明确提出「测试」时再一次性执行。
3. **改到测试文件也要同步** — 不运行测试不等于放任测试失效:若本次改动改了类名、导出名、方法名或 DOM 结构,被波及的既有测试文件仍需同步更新到与新实现一致,保证代码库自洽;只是不在这一阶段运行它们。测试断言、夹具的补充同样留到统一测试时进行。
4. **区分新问题与历史问题** — 构建、lint 或全量测试若被改动前已存在的缺失文件、旧测试或环境问题阻断,应在结果中明确记录,不为了通过检查修改无关代码。