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

24 KiB
Raw Blame History

开发规范(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。不要为了追求全项目单一风格而把两类名称混在一起。

    // 页面状态、局部变量和方法: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. 查询条件保持结构化 — 即使当前阶段信任前端传入的条件,也优先使用字段、操作符和值组成的对象,而不是在页面中散落拼接字符串。这样做是为了让条件容易阅读、复用和排查,不是为了增加安全限制。

    {
      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() — 保存成功后的重新加载、状态更新、草稿清理和页面刷新

推荐主流程保持如下结构:

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 或全量测试若被改动前已存在的缺失文件、旧测试或环境问题阻断,应在结果中明确记录,不为了通过检查修改无关代码。