1. 命名规范在 OMS 项目里为什么值得单独立一份文档
OMS(订单管理系统)这类后台项目有个很典型的特点:业务域多、字段多、状态机多、参与迭代的人也多。订单、履约、库存、售后、结算、物流,每个域都有自己的一套实体和状态流转,前端页面往往是一个列表页配一个详情页,再加一堆弹窗、抽屉、状态标签、操作按钮组。这种项目跑上半年,代码库里会积累几百个组件、上千个接口函数、几十个 Pinia store。此时命名就不只是"好不好看"的问题了,它直接决定了三件事:新人接手时要花多久才能定位到某段逻辑、keep-alive缓存会不会莫名失效、以及改一个字段时会不会漏改另外三个地方。
我在几个 Vue3 的中后台项目里反复踩过这类坑。最典型的一次是订单列表页需要做页签缓存,keep-alive的include里写了OrderList,但组件实际导出的 name 是文件名推导出来的Index,结果缓存静默失效,用户切页签回来筛选条件全丢,排查了大半天才定位到是命名不一致。从那以后,我就把命名规范当成一份要落地的技术资产来做,而不是 README 里一段没人看的文字。
这份 OMS Vue3 命名规范,核心解决四个问题:组件身份可预测(文件名叫什么,组件 name 就是什么)、业务语义可检索(搜Aftersale能捞全售后域的东西)、类型与字段不歧义(金额单位、时间格式写在名字里)、机械可校验(能交给 ESLint 拦住的,绝不靠人肉 review)。适合正在做 Vue3 中后台重构的团队、刚接手历史项目的同学,以及想给自己项目定一套轻量规约的独立开发者。下面从设计思路一路讲到工具配置和踩坑记录,能直接抄的部分我都标出来了。
2. 规范整体设计思路与分层拆解
2.1 先定"命名服务于谁",再谈规则细节
很多团队的命名规范之所以推行不下去,是因为一上来就列了几十条"必须 PascalCase""禁止缩写",但没回答一个问题:这条规则在防什么事故?我的做法是先盘点这个项目里命名会影响到哪些实际机制,规则只围绕这些机制来定,其余一律放开。
在 Vue3 + TypeScript 的 OMS 里,命名真正会引发运行时问题的机制大概有六个:keep-alive的include/exclude匹配组件 name、<component :is>动态组件解析、router.push({ name })的路由跳转、unplugin-vue-components的按需自动导入、Pinia 的 store id 与 devtools 调试、后端字段映射(驼峰与下划线的转换)。除此之外的命名,原则上只做"建议",不做"强制",避免规范变成负担。
按这个思路,我把规范分成三层,越往下约束越硬:
| 层级 | 覆盖范围 | 约束强度 | 违反后果 |
|---|---|---|---|
| 机制层 | 组件 name、路由 name、store id、自动导入目录 | 强制,ESLint + Review 双卡 | 缓存失效、跳转 404、导入失败 |
| 语义层 | 文件名、目录名、类型名、API 函数名 | 强制,Review 卡 | 检索困难、职责混淆 |
| 风格层 | 局部变量、循环变量、临时对象 | 建议,交给 Prettier 和习惯 | 基本无后果 |
这个分层的实际意义在于:Code Review 时你不用再纠结const data = ...该不该改名,但组件 name 和文件名不一致必须打回。把有限的评审精力放在真正会出事的地方。
2.2 业务域前缀是整个规范的骨架
OMS 的组件和文件如果只按技术类型分(Table.vue、Dialog.vue、Form.vue),跑到五十个组件之后就没法看了。我的做法是先切业务域,再切技术类型。域词表控制在十个以内,写进文档,命名时只允许从词表里选:
order订单主域(下单、订单列表、订单详情、订单状态)fulfill履约发货(拣货、打包、出库)inventory库存(锁定、扣减、回补)aftersale售后(退款、退货、换货、工单)settle结算对账(账单、流水、发票)logistics物流(运单、轨迹、承运商)promotion促销优惠(优惠券、满减、活动)customer客户与收货信息common跨域复用的业务组件
前缀的作用不只是分类,它同时解决了自动导入的命名冲突。假设订单域和售后域都有一个"状态标签"组件,叫StatusTag.vue就会出现同名覆盖,而OrderStatusTag.vue和AftersaleStatusTag.vue天然不冲突,unplugin-vue-components扫描时也不会打架。这条经验是我在被自动导入的静默覆盖坑过一次之后才总结出来的——它不报错,只是渲染出了另一个组件,非常难查。
2.3 目录结构与命名的对应关系
目录名用小写 kebab-case(多词时)或单个小写单词,页面级目录用业务域,页面内部再按 list / detail / edit 分。这套结构决定了文件名的可预测性:
src/ ├── api/ │ ├── order.ts │ ├── aftersale.ts │ └── types/ │ ├── order.ts │ └── aftersale.ts ├── components/ │ ├── base/ # 无业务语义的原子组件 │ │ ├── BaseTable.vue │ │ └── BaseDialog.vue │ └── business/ │ ├── order/ │ │ ├── OrderStatusTag.vue │ │ └── OrderAmountText.vue │ └── aftersale/ │ └── AftersaleReasonSelect.vue ├── composables/ │ ├── useOrderListQuery.ts │ └── useTableSelection.ts ├── constants/ │ ├── order.ts │ └── aftersale.ts ├── router/ │ └── modules/order.ts ├── stores/ │ ├── order.ts │ └── user.ts ├── types/ │ ├── order.ts │ └── global.d.ts └── views/ └── order/ ├── list/ │ ├── OrderList.vue │ └── components/ │ └── OrderListFilter.vue └── detail/ ├── OrderDetail.vue └── components/ └── OrderDetailBasicInfo.vue这里有个明确的取舍:页面主视图不叫index.vue。原因很实际——项目里几十个index.vue,在编辑器标签页里全是index.vue,devtools 组件树里全是Index,出问题时想定位是哪个页面得靠路径反推。改成OrderList.vue、OrderDetail.vue之后,不管是搜索、断点、还是看报错堆栈,一眼就能锁定。代价是路由里的import()路径要写全,这点成本完全可以接受。
3. 文件与组件命名的落地细则
3.1 单文件组件的文件名与组件 name 必须一致
规则只有一条,但必须硬性执行:.vue文件名用 PascalCase,且组件通过defineOptions显式声明同名 name。
<!-- src/components/business/order/OrderStatusTag.vue --> <script setup lang="ts"> import { computed } from 'vue' import { ORDER_STATUS_TEXT, type OrderStatus } from '@/constants/order' defineOptions({ name: 'OrderStatusTag' }) const props = defineProps<{ status: OrderStatus showDot?: boolean }>() const text = computed(() => ORDER_STATUS_TEXT[props.status] ?? '未知状态') </script> <template> <span class="order-status-tag" :class="`order-status-tag--${status.toLowerCase()}`"> <i v-if="showDot" class="order-status-tag__dot" /> {{ text }} </span> </template>为什么要显式写defineOptions({ name })?因为<script setup>的组件名默认由文件名推导,这个推导在开发环境靠编译插件,生产构建后会变,而keep-alive的include匹配的是运行时name选项。不写 name 就意味着缓存行为依赖构建细节,属于给自己埋雷。显式声明之后,<keep-alive :include="['OrderList']">的匹配就完全可控了。
配套的 ESLint 规则要打开vue/multi-word-component-names(禁止单词组件名,避免和原生标签Table、Menu冲突)和vue/component-definition-name-casing: ['error', 'PascalCase']。自动导入插件的配置里也要把directoryAsNamespace关掉,否则components/business/order/OrderStatusTag.vue会被注册成BusinessOrderOrderStatusTag,没人愿意用这种名字。
3.2 组件命名遵循"域 + 对象 + 形态"三段式
OMS 里组件形态翻来覆去就那么几种:列表、表格、表单、弹窗、抽屉、标签、选择器、时间线。命名按"业务域 → 业务对象 → 组件形态"拼接,中间不加分隔符,全部 PascalCase:
| 场景 | 推荐命名 | 不推荐 | 原因 |
|---|---|---|---|
| 订单列表表格 | OrderListTable.vue | Table.vue、OrderTableList.vue | 域在前,检索时按域聚合 |
| 订单详情基础信息卡 | OrderDetailBasicInfo.vue | Info.vue、BaseInfoCard.vue | 表达层级归属 |
| 售后原因下拉 | AftersaleReasonSelect.vue | Select.vue、ReasonSelect.vue | 选择器类以 Select 结尾 |
| 履约时间线 | FulfillTimeline.vue | Timeline.vue | 无形组件必须带域 |
| 订单金额展示 | OrderAmountText.vue | Money.vue、Amount.vue | 展示类以 Text 结尾 |
| 订单操作按钮组 | OrderActionBar.vue | Buttons.vue | 容器类以 Bar / Group 结尾 |
形态词表也建议固定下来:Table、Form、Dialog、Drawer、Select、Tag、Timeline、Card、Bar、Panel。好处是新人看到一个陌生的SettleBillDrawer,立刻知道它是个抽屉,也知道该去哪找它的打开逻辑。
base目录下的原子组件统一加Base前缀,不要加App(App前缀在 Vue 官方风格指南里是留给根组件和全局单例的)。Base组件的判定标准很严格:不能出现任何业务字段名,不能 importapi/或stores/里的任何东西。一旦它开始 import 订单状态常量,就该搬到business/下并改名。
注意:组件名不要用单个单词,也不要和 HTML 原生标签重名。
OrderTable安全,Table、Menu、Select、Switch、Option都危险,浏览器解析模板时可能出意想不到的问题,Vue 官方风格指南里明确要求组件名是多单词。
3.3 Props、Emits、Slots 的命名约定
Props 在defineProps里用 camelCase,模板里传值用 kebab-case,这是 Vue 生态的既定习惯,照做就行:
<!-- 父组件 --> <OrderListTable :order-list="list" :loading="loading" :show-selection="true" @update:selection="onSelectionChange" @row-click="handleRowClick" />围绕这套写法,我补了几条团队约定:
- 布尔类 props 统一加
is/has/show/can前缀。showSelection、hasPagination、canExport。反面例子是selection、disabled这种——selection看不出是数组还是布尔,读模板时得回去翻定义。例外是与原生或组件库对齐的disabled、readonly、clearable,这些保留原样,避免二次包装时心智负担。 - 事件名在
defineEmits里用 camelCase,模板监听用 kebab-case。update:modelValue这种带冒号的双向绑定属于 Vue 保留写法,update:visible、update:status同理。业务事件用动词开头:submit、confirm、cancel、export、batchAssign。不要用onSubmit当事件名,on前缀是父组件监听时的语法糖,混在 emit 里会造成@on-submit这种别扭写法。 - Slot 名用 kebab-case 短词,
default、header、footer、actions、empty、prefix、suffix。带作用域的 slot 要传对象,别传裸值,方便后续加字段不破坏调用方。
事件处理函数的命名也要区分:模板里直接写逻辑用handleXxx(handleRowClick),用组合式函数封装的事件用onXxx(onVisibleChange)。这个区分不是为了好看,而是 review 时能一眼看出这个函数是模板直连还是被 composable 消费的。
4. JavaScript 与 TypeScript 层命名细则
4.1 变量、函数与常量的命名基准
这一层最容易失控,因为量大、随手写、review 时又懒得提。我给的最小可行规则是四条:
- 变量、函数用 camelCase,函数用动词开头:
getOrderDetail、formatAmount、resetFilter、fetchList。 - 模块级常量用 UPPER_SNAKE_CASE,导出时按域聚合成一个对象,而不是散着导出一堆。
- 布尔变量必须带判断语义前缀:
isPaid、hasAftersale、canCancel、shouldRefresh。禁止flag、status2、type1这类。 - 禁止拼音、禁止无意义缩写(
dt、tmp、val),但行业通用缩写保留:id、sku、url、api、dto。
常量的组织方式值得单独说。OMS 里一个订单状态会被用在列表、详情、筛选项、状态标签、操作按钮权限判断等至少七八个地方,如果每个页面自己写一份if (status === 'PAID'),迟早出现文案不一致。正确做法是常量与映射表放在constants/order.ts里集中维护:
// src/constants/order.ts export const ORDER_STATUS = { PENDING: 'PENDING', PAID: 'PAID', SHIPPED: 'SHIPPED', COMPLETED: 'COMPLETED', CANCELLED: 'CANCELLED', } as const export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS] export const ORDER_STATUS_TEXT: Record<OrderStatus, string> = { PENDING: '待支付', PAID: '已支付', SHIPPED: '已发货', COMPLETED: '已完成', CANCELLED: '已取消', } export const ORDER_STATUS_COLOR: Record<OrderStatus, string> = { PENDING: 'warning', PAID: 'processing', SHIPPED: 'processing', COMPLETED: 'success', CANCELLED: 'default', } export const ORDER_STATUS_OPTIONS = Object.entries(ORDER_STATUS_TEXT).map( ([value, label]) => ({ value: value as OrderStatus, label }), )这里用as const对象 + 联合类型,而不是 TS 的enum。原因有两个:const enum在isolatedModules和部分构建链里有坑,普通enum编译后会生成运行时代码,且和字符串字面量比较时类型收窄不如联合类型自然;而as const方案零运行时开销,Record<OrderStatus, string>还能强制你补齐所有状态的文案,漏一个直接编译报错。这个反例我在老项目里见过——新增一个"部分退款"状态后,三个页面的文案映射表漏了两个,线上显示了空白标签。
4.2 类型、接口与枚举的命名区分
类型命名最大的争议是到底加不加I前缀。我的立场很明确:在 Vue3 + TS 的前端项目里不加I前缀,改用用途后缀表达角色。理由是前端类型大多是 DTO 和视图模型,I前缀区分不出任何有价值的语义,反而让 IDE 的自动补全里一堆I开头的类型挤在I这个字母下。
后缀约定如下:
| 后缀 | 用途 | 示例 |
|---|---|---|
Params | 请求入参 | OrderListParams、AftersaleAuditParams |
Result | 接口返回结构 | OrderListResult、SettleBillResult |
DTO | 与后端字段一一对应的数据结构 | OrderDetailDTO |
VO/ 视图模型 | 前端组装后供页面使用的结构 | OrderDetailVO |
Props/Emits | 组件契约 | OrderListTableProps |
Config | 配置对象 | OrderExportConfig |
DTO与VO分离这件事,在 OMS 里价值极高。后端返回的订单详情常常是扁平大对象加几个嵌套数组,字段名是后端风格(gmt_create、order_amount);页面需要用到的却是带格式化金额、带枚举文案、带计算属性的结构。如果两者共用一个类型,格式化逻辑会到处散落。我的做法是api/types/order.ts里定义 DTO,views/order/detail/types.ts里定义 VO,中间由一个mapOrderDetail(dto): OrderDetailVO的适配函数转换。这条边界一旦划清,后端改字段时改动范围就锁定在适配函数一处。
类型名禁止用单复数混淆的形式。OrderList既可能是数组也可能是组件名,一律用OrderListItem[]或OrderListResult,不用裸OrderList当类型名。
4.3 组合式函数与 Pinia Store 的命名
组合式函数一律use前缀 + camelCase,文件名与函数名一致:useOrderListQuery.ts导出useOrderListQuery。这条是 Vue 官方约定,没有讨论空间,重点是按职责起名而不是按内容起名:
useOrderListQuery—— 列表查询(分页、筛选、请求、重置)useTableSelection—— 表格多选与跨页保留usePermission—— 权限码判断useOrderStatusAction—— 根据状态算出可用操作按钮
反面教材是useOrder、useData、useTable这种大而全的名字。它们最后都会长成几百行的怪物,谁都改不动。判定标准:如果一个 composable 超过 150 行,或者内部 state 超过 6 个,就该按职责拆开。
Pinia store 的命名规则更硬:文件名小写域名单词,store id 与域同名,导出函数useXxxStore。
// src/stores/order.ts import { defineStore } from 'pinia' import { ref } from 'vue' import { getOrderList } from '@/api/order' import type { OrderListItem, OrderListParams } from '@/api/types/order' export const useOrderStore = defineStore('order', () => { const list = ref<OrderListItem[]>([]) const total = ref(0) const queryParams = ref<OrderListParams>({ page: 1, pageSize: 20 }) async function fetchList() { const { data } = await getOrderList(queryParams.value) list.value = data.records total.value = data.total } function resetQuery() { queryParams.value = { page: 1, pageSize: 20 } fetchList() } return { list, total, queryParams, fetchList, resetQuery } })store id 用order,不要用useOrderStore或orderStore。id 会出现在 devtools 的 store 列表和持久化插件的 key 里,简短的域名最好用。state 里的变量直接用具名语义,别加order前缀——在useOrderStore内部叫list就够了,写orderList属于重复上下文。
提示:
useXxxStore的 Xxx 必须和文件、目录、业务域词表里的词完全一致。曾经有个项目分成orderInfo、orderData两个 store,实际都是订单数据,导致列表改了详情不刷新,排查了整整一个下午。
5. 样式、路由、接口与字段的命名映射
5.1 CSS 类名的裁剪版 BEM
<style scoped>已经能隔离大部分样式冲突,所以我不建议上完整的 BEM(block__element--modifier三层写全太长)。裁剪版规则是:块名与组件文件名 kebab-case 对应,元素用__单层,修饰符用--单层,最多两层。
OrderStatusTag.vue内部的类名就是order-status-tag、order-status-tag__dot、order-status-tag--paid。这样在浏览器里看到一个类名,能直接反推组件文件名,调试时非常省事。
几条具体禁忌:
- 禁止在组件根节点使用无前缀的通用类名,比如
container、header、content、title。这类名字在任何一个页面的 DOM 里都会出现几十次,选中和调试都是灾难。用order-detail__header这种带块名的。 - 状态修饰符用业务值而不是颜色。写
--paid而不是--green,主题切换或者设计稿改色时不用改模板。 - 穿透样式统一用
:deep(),::v-deep和>>>在 Vue3 里已经废弃,混用会导致编译警告甚至样式丢失。 - 不要为了"语义化"造长类名。
order-detail-basic-info__row-item-label-text这种超过四段的,说明组件的 DOM 层级该收一收了。
如果项目用了原子化 CSS 方案,规则改成"原子类做布局、自定义类做业务语义",不要在模板里堆mt-[13px]这种魔法值,改一次设计稿要全项目替换。
5.2 路由 name 与权限码的对齐
路由name是会被代码直接引用的(router.push({ name })、面包屑、页签缓存),所以它属于机制层,必须强约束。规则:路由 name 用 PascalCase,等于"域 + 页面"的组合,和主视图组件名保持一致。
// src/router/modules/order.ts import type { RouteRecordRaw } from 'vue-router' const orderRoutes: RouteRecordRaw[] = [ { path: '/order/list', name: 'OrderList', component: () => import('@/views/order/list/OrderList.vue'), meta: { title: '订单列表', perm: 'order:list:view', keepAlive: true }, }, { path: '/order/detail/:id', name: 'OrderDetail', component: () => import('@/views/order/detail/OrderDetail.vue'), meta: { title: '订单详情', perm: 'order:detail:view', keepAlive: false, hidden: true }, }, ] export default orderRoutesname与组件名一致带来的直接收益是:<keep-alive :include="['OrderList', 'OrderDetail']">可以直接复用路由 name,不用维护第二份名单;页签系统里缓存的 key 也能直接用路由 name。这一点在做过页签化后台的人都懂——两套名字对不上是最常见的一类 bug。
权限码用域:资源:动作三段式小写冒号分隔:order:list:view、order:detail:edit、aftersale:audit:approve。前端不要自己拼权限码字符串,用常量函数生成:
// src/constants/permission.ts export const PERM = { ORDER_LIST_VIEW: 'order:list:view', ORDER_DETAIL_EDIT: 'order:detail:edit', AFTERSALE_AUDIT_APPROVE: 'aftersale:audit:approve', } as const export type PermCode = (typeof PERM)[keyof typeof PERM]写成常量之后,改权限码只需要改一处,而且 grep 能穷举出所有引用点。直接写字符串字面量的话,后端改一个权限码,前端基本靠线上报错来找。
5.3 API 函数命名与字段映射的三条硬规矩
API 层命名按 HTTP 语义走动词:查询用get,创建用create,更新用update,删除用delete或remove,状态变更用具体的业务动词cancelOrder、confirmReceipt、auditAftersale。不要全部用query/save/submit这种含糊动词,一个saveOrder到底是新建还是更新,看名字永远不知道。
字段映射这块我想多说两句,因为它是 OMS 项目里最容易出隐性 bug 的地方。三条规矩:
第一,金额字段必须带单位后缀。后端返回分就用amountCent,返回元就用amountYuan,浮点金额一律不进前端计算。我见过最痛的一次是促销模块里两个接口一个返回分一个返回元,页面上 100 元的订单显示成 1 元,测试环境因为数据小没发现,上线后对账直接炸了。名字里带单位,这种错误在 review 阶段就会被拦住。
第二,时间字段区分形态。payTime表示格式化后的字符串,payTimestamp表示毫秒时间戳,payDate表示YYYY-MM-DD。三种形态混用是另一个高频事故点——时间戳直接渲染成1700000000000,或者字符串被new Date()解析成 Invalid Date。
第三,布尔字段不用否定式。用isDeleted、enabled,不用notDeleted、disabledFlag。否定式在做条件组合时极容易写错取反。
6. 工具链落地与问题排查实录
6.1 用 ESLint 把规范钉死在提交前
规范写在文档里没人看,写在 ESLint 里就会被强制执行。核心配置如下,naming-convention那部分需要@typescript-eslint插件:
// eslint.config.js 片段(扁平配置) export default [ { files: ['**/*.{ts,vue}'], rules: { 'vue/multi-word-component-names': 'error', 'vue/component-definition-name-casing': ['error', 'PascalCase'], 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 'vue/prop-name-casing': ['error', 'camelCase'], 'vue/custom-event-name-casing': ['error', 'camelCase'], '@typescript-eslint/naming-convention': [ 'error', { selector: 'interface', format: ['PascalCase'] }, { selector: 'typeAlias', format: ['PascalCase'] }, { selector: 'enum', format: ['PascalCase'] }, { selector: 'enumMember', format: ['UPPER_CASE'] }, { selector: 'variable', format: ['camelCase', 'UPPER_CASE', 'PascalCase'], leadingUnderscore: 'allow' }, { selector: 'function', format: ['camelCase'] }, { selector: 'variable', modifiers: ['const', 'exported'], format: ['UPPER_CASE', 'camelCase', 'PascalCase'] }, ], }, }, ]有两点需要特别注意:vue/component-name-in-template-casing配成PascalCase意味着模板里必须写<OrderStatusTag />而不是<order-status-tag />,这条会和部分团队的既有习惯冲突,上线前先全量跑一次--fix,别让人肉改。naming-convention的variable规则容易误伤从后端透传的字段(比如data.records解构出来的变量),建议对解构赋值单独放行,配置里加selector: 'variable', modifiers: ['destructured']的宽松规则,不然全是噪声告警,团队会直接关掉这条规则。
组件 name 与文件名的一致性,ESLint 官方规则覆盖不到,得自己写一个小脚本挂到lint-staged里:
#!/usr/bin/env bash # scripts/check-component-name.sh fail=0 for f in $(git diff --cached --name-only --diff-filter=ACM | grep '\.vue$'); do base=$(basename "$f" .vue) if ! grep -q "name: '$base'" "$f" && [[ "$base" != index ]]; then echo "[命名规范] $f 缺少 defineOptions({ name: '$base' })" fail=1 fi done exit $fail脚本很短,但它把这套规范里最容易偷懒的一条变成了硬门槛。团队跑了两周之后,基本就形成肌肉记忆了。
6.2 常见问题速查表
下面这些是我在 OMS 项目里实际踩过或被同事问过的问题,按出现频率排序:
| 现象 | 大概率原因 | 排查方式 | 修复 |
|---|---|---|---|
keep-alive缓存不生效 | 组件 name 与include不一致,或未显式声明 name | devtools 看组件名是否等于文件名 | 补defineOptions({ name }) |
| 模板里组件渲染成另一个 | 自动导入重名覆盖 | 查unplugin-vue-components生成的components.d.ts | 加业务域前缀 |
| 父传子 props 不生效 | 模板里写成 camelCase | 检查<Comp :orderList="x" /> | 改成:order-list="x" |
| 事件监听没触发 | emit 声明成 kebab-case,监听也写成 kebab-case | 看defineEmits定义 | 定义用 camelCase,监听用 kebab-case |
router.push后白屏或 404 | 路由 name 被改过,引用处没同步 | 全局搜name:和被引用的字符串 | name 提成常量或只引用一次 |
| Pinia 数据不一致 | 同一业务域建了多个 store | devtools 看 store 列表 | 合并为单一域 store |
| TS 类型报错提示联合类型不存在 | 用enum而非as const对象 | 检查常量定义方式 | 改用as const+ 联合类型 |
| 金额显示差 100 倍 | 分/元字段混用 | 检查字段名是否带单位后缀 | 统一加Cent/Yuan后缀 |
| 样式在某处失效 | ::v-deep与:deep()混用 | 搜全项目v-deep | 统一:deep() |
| 自动导入类型提示丢失 | 组件目录层级变化后没重新生成components.d.ts | 重启 dev server 或手动触发 | 提交前纳入构建产物 |
6.3 老项目迁移顺序:先改名,后改结构
如果手里的 OMS 已经跑了一两年,别指望一次性推平。我推荐的迁移顺序是四步,按风险从低到高排:
第一步,先建constants/和types/,把散落在各页面的状态字面量和字段映射收拢上来。这一步不改任何组件名,风险极低,收益立刻可见——后面所有改名都建立在这份常量之上。
第二步,给所有.vue补defineOptions({ name }),name 与当前文件名保持一致。这一步也不改文件名,但解决了缓存和 devtools 定位问题,属于纯收益。
第三步,按业务域重命名文件和目录。这一步必须配合 Git 的git mv保留历史,并且一次只改一个域,改完立刻回归该域的页面。切忌一次提交改两百个文件,出问题根本回滚不了。
第四步,才去重命名变量、函数这类风格层的东西,并且只在改动那个文件的顺手做,不做专项重构。
注意:重命名路由 name 和 store id 属于破坏性变更,必须前后端和权限配置一起对表。store id 还会影响本地持久化的 key,改名意味着用户本地的草稿、筛选条件全部丢失,选择低峰期发布。
最后分享一个我在实际操作中的体会:命名规范这件事,真正难的不是"制定",而是"新人和老代码同时存在的那段时间"。我的做法是在views/下建一个legacy目录,把还没重命名的旧页面整体挪进去,路由照常指向它。这样新目录里一眼看过去都是合规代码,新人照着写就不会跑偏;而旧代码有明确的边界,谁改谁负责顺手对齐。跑三个月之后,legacy目录自然就空了。