MEMORY.md 已从开源仓移除,README 仍要求「必须同步阅读 MEMORY.md」并列出其内容。 改为直接用「常见踩坑」小节承载这些纠错要点,目录树中同步去掉 MEMORY.md 条目。
19 KiB
IOTLCM CRUD 新风格模板
本目录用于把默认生成或旧风格的 IOTLCM CRUD 页面迁移为当前统一风格。模板是可复制使用的示例,迁移时先复制文件,再按真实模块替换类型名、API 路径、字段、主键、权限码和表格标题。
目录说明
iotlcm-crud-style/
├── README.md
├── template/
│ ├── api/
│ │ ├── index.ts # 类型化 API 调用模板
│ │ └── type.ts # API 参数和响应类型模板
│ └── view/
│ ├── index.vue # 列表页模板
│ ├── edit.vue # 新增/编辑弹窗模板
│ ├── view.vue # 查看弹窗模板
│ └── model.tsx # 查询表单、表格列、编辑表单、查看描述配置
└── compare/
├── before/ # 修改前完整代码快照
└── after/ # 修改后完整代码快照
compare 是完整对照组,不是片段示例:
- 对照文件统一使用
.txt后缀保存完整源码,避免旧代码被 ESLint/TypeScript 当作真实业务代码扫描。 - 迁移时必须同时看
before/api -> after/api和before/view -> after/view,不要只改视图。
常见踩坑
模板迁移中的典型纠错,动手前先过一遍:
- 接口返回未知时不要凭前端字段伪造响应类型,保留
any并明确说明。 - 查看弹窗内新增 Tab/Grid 时,相关列和查询表单应放在
model.tsx的ViewDescSchema后面。 - 弹窗内 Grid 应使用
BetterVben.grid.gridOptions.modal。 - 弹窗内 Grid 的数据加载应使用
proxyConfig.ajax.query,打开弹窗后调用gridApi.query(),不要手动setGridOptions({ data })。
迁移前检查
- 确认视图目录和 API 目录是一一对应的,例如:
- 视图:
apps/web-antd/src/views/iotlcm/tLifeCycle/tExample - API:
apps/web-antd/src/api/iotlcm/tLifeCycle/tExample
- 视图:
- 先打开真实模块的
type.ts,确认主键字段。不要默认所有模块都用id,也可能是appKey、deptId、regionId等。 - 记录原代码里的业务差异:自定义按钮、特殊 API、权限码、查询参数转换、插槽渲染、弹窗传参、提交前后处理。
- 检查本地 Skill 和项目参考:优先参考
C:\Users\HuanTian\.claude\skills下的vben-page-guide、vben-grid-guide、vben-form-guide、vben-modal-guide、vben-form-grid。
硬性执行边界
视图模块和 API 模块是一个迁移单元。即使用户只点名 apps/web-antd/src/views/iotlcm/...,也必须同步检查并改造对应的 apps/web-antd/src/api/iotlcm/...:
| 用户给出的路径 | 必须同步处理的路径 |
|---|---|
apps/web-antd/src/views/iotlcm/tLifeCycle/tBatch |
apps/web-antd/src/api/iotlcm/tLifeCycle/tBatch |
apps/web-antd/src/views/iotlcm/tChannel/tSimChannels |
apps/web-antd/src/api/iotlcm/tChannel/tSimChannels |
跳过 API 文件属于迁移未完成。最终结果至少要覆盖:
- API:
index.ts、type.ts - 视图:
index.vue、edit.vue、view.vue、model.tsx
不要做“半旧半新”的局部改造。旧代码中的业务字段、接口路径、权限码、查询参数可以保留,但面板骨架必须收敛到本模板。尤其是从旧 Drawer 页面迁移时,不能只把 useVbenDrawer 改成 useVbenModal 后继续保留旧写法。
目标参考
新人迁移时可以把 template/view/、template/api/ 和 compare/after/ 当作最终形态参考。迁移后的结果不要求字段完全相同,但代码组织和基础逻辑应基本一致:
index.vue使用Page + Grid + EditModal + ViewModal。index.vue中表格标题、导出文件名、权限码直接替换为真实模块值。index.vue中formOptions.schema使用IndexGridSchema,gridOptions.columns使用IndexGridColumns。index.vue操作插槽使用#action="{ row }",查看、修改、删除方法都接收row。edit.vue使用ModalProps { data, isUpdate }、handleOpen、handleConfirm、handleCancel。edit.vue编辑回显调用View,提交调用Edit(value as XxxModel)或真实编辑参数类型。view.vue使用ViewDescSchema + Description + useDescription。- 基础 CRUD 的
model.tsx只导出IndexGridSchema、IndexGridColumns、EditFormSchema、ViewDescSchema这四类核心配置。 - 如果查看弹窗内新增业务 Tab/Grid,相关列配置和查询表单属于查看弹窗扩展配置,允许追加在
ViewDescSchema后面。 model.tsx内部顺序固定为:表格查询IndexGridSchema-> 表格列IndexGridColumns-> 新增/编辑表单EditFormSchema-> 查看描述ViewDescSchema。api/index.ts从同级type.ts导入参数和响应类型,不再使用any。
以 compare/after 中的 tSimChannels 完整对照为例,模板占位替换应类似这样:
| 模板内容 | 真实模块内容 |
|---|---|
TemplateCrudModel |
TSimChannelsModel |
TemplateCrudListParam |
TSimChannelsListParam |
TemplateCrudListRes |
TSimChannelsListRes |
TemplateCrudEditParam |
TSimChannelsEditParam |
TemplateCrudViewParam |
TSimChannelsViewParam |
TemplateCrudViewRes |
TSimChannelsViewRes |
TemplateCrudDeleteParam |
TSimChannelsDeleteParam |
TemplateCrudExportParam |
TSimChannelsExportParam |
iot/templateCrud |
iot/tSimChannels |
示例业务 |
通道管理 |
cpm:iot:templateCrud |
cpm:iot:tSimChannels |
路径一:基于默认生成代码应用新模板
这种情况适用于刚生成、还没有明显业务改动的页面。处理目标是把默认生成代码统一为新结构。
1. 检查 API 类型
先检查对应 API 目录下是否存在 type.ts。
- 如果
type.ts存在:继续按下面规则修正 API 类型和index.ts。 - 如果
type.ts不存在:跳过 API 类型化流程,不要临时编造类型文件;在最终输出中明确提示用户“对应 API 缺少 type.ts,需要先补全类型后才能完成 API 类型化迁移”。
必须包含这些类型:
XxxModel
XxxListParam
XxxListRes
XxxExportParam
XxxEditParam
XxxViewParam
XxxViewRes
XxxDeleteParam
规则:
XxxListRes使用GlobalListRes<XxxModel>。XxxExportParam通常复用XxxListParam。XxxViewParam写真实主键字段。XxxDeleteParam按后端约定写单值或数组。- 字段注释保留或补齐,便于后续判断主键和业务字段。
2. 替换 API index.ts
用 template/api/index.ts 的结构改造真实 API 文件。
必须满足:
- 从
./type导入请求参数和响应类型。 List(params: XxxListParam)返回requestClient.get<XxxListRes>(...)。View(params: XxxViewParam)返回requestClient.get<XxxViewRes>(...)。Edit/Delete/Export使用明确参数类型。- 保留函数名
List/Delete/Edit/View/Export。 - 不再使用
params: any、requestClient.get<any>这类旧写法。
API 验收时必须同时满足:
不存在:params: any
不存在:requestClient.get<any>
存在:List(params: XxxListParam)
存在:requestClient.get<XxxListRes>
存在:View(params: XxxViewParam)
存在:requestClient.get<XxxViewRes>
3. 改造 model.tsx
把旧导出名统一为新导出名:
| 旧风格 | 新风格 | 用途 |
|---|---|---|
querySchema |
IndexGridSchema |
表格查询表单 |
columns |
IndexGridColumns |
表格列 |
editSchema |
EditFormSchema |
编辑表单 |
viewSchema |
ViewDescSchema |
查看描述 |
必须清理旧残留:
querySchema
editSchema
viewSchema
State
newState
BasicDrawer
useVbenDrawer
rules: null
export const columns
import { columns
注意:columns 是 useVbenVxeGrid 的合法配置项,不要裸搜 columns 后全部清理。只清理旧导出 export const columns、旧导入 import { columns } 或把 columns 当表格列变量直接传入的旧写法。
字段配置不要照抄模板里的 name/code/status/remark 示例字段。真实模块要从旧 model.tsx、接口字段或 type.ts 迁移字段。可以参考 compare/after/view/model.tsx.txt 的最终形态:保留真实业务字段,同时清理旧生成器残留。
model.tsx 必须满足这些结构和样式规则:
- 导出顺序固定为
IndexGridSchema->IndexGridColumns->EditFormSchema->ViewDescSchema。 IndexGridColumns中删除旧生成器残留的align: 'left'。IndexGridColumns中删除width: -1,根据字段补合适宽度,普通字段优先用minWidth: 120。- 操作列使用固定宽度,例如
{ title: '操作', width: 120, slots: { default: 'action' }, fixed: 'right' }。 IndexGridSchema和EditFormSchema中删除旧生成器残留的formItemClass: 'col-span-1'。EditFormSchema只放新增/编辑字段,不要混入查看字段。ViewDescSchema只放查看字段,顺序应跟详情展示一致,不要插到EditFormSchema前面。
model.tsx 禁止残留:
align: 'left'
width: -1
formItemClass: 'col-span-1'
formItemClass: "col-span-1"
4. 改造 index.vue
复制 template/view/index.vue 的结构,再替换真实模块信息。
必须替换:
TemplateCrudModel-> 真实XxxModel../api/type-> 真实 API 类型路径../api-> 真实 API 路径示例业务-> 真实表格标题和导出文件名cpm:iot:templateCrud-> 真实权限前缀- 删除和批量删除里的主键字段
模板为了在 src/template 目录下自包含,使用了 ../api 相对导入。迁移到真实业务模块后,API 导入应改成 #/api/iotlcm/... 形式,并放回 API 接口导入分组。
结构要求:
- 使用
Page + useVbenVxeGrid + useVbenModal。 - 代码区域顺序固定为
常量 -> 数据 -> 方法 -> 组件 -> 周期。 formOptions.schema使用IndexGridSchema。gridOptions.columns使用IndexGridColumns。- 操作列插槽统一写
#action="{ row }"。 - 列表查询中清空
selectRows.value。 - 保留新增、批量删除、导出、查看、编辑、删除的标准入口。
- 工具栏按钮必须放在
<div class="flex items-center justify-center gap-2">中,按钮自身使用class="flex items-center"。 - 操作列按钮必须放在
<div class="flex items-center justify-center gap-2">中,链接按钮自身使用class="border-none p-0"。 - 按钮组间距统一由父容器
gap-2控制,不使用按钮自身的mr-2维护间距。
useVbenVxeGrid 配置必须以模板为准:
formOptions默认只保留...(BetterVben.grid.formOptions.default as any)和schema: IndexGridSchema。gridOptions默认只保留...(BetterVben.grid.gridOptions.default as any)、columns: IndexGridColumns和proxyConfig。- 引入
BetterVben默认配置后,不要继续叠加旧生成器默认项:fieldMappingTime: [['date', ['start', 'end']]]、submitOnChange、submitOnEnter、checkboxConfig、rowConfig都要默认删除。 - 如果业务确实依赖时间字段映射、行主键或复选框展示配置,必须按真实字段重新写,不能保留示例
date/start/end/id。最终汇报里要说明这是业务保留项。 - 判断标准:旧默认面板带来的配置删除;真实业务字段转换、交互或后端参数需要的配置保留并验证。
5. 改造 edit.vue
复制 template/view/edit.vue 的结构,再替换真实模块信息。
必须替换:
TemplateCrudModel-> 真实XxxModelView({ id: data.data.id })-> 真实详情主键参数Edit(value as TemplateCrudModel)-> 真实模型类型或编辑参数类型,默认生成页通常使用真实XxxModel
结构要求:
- 使用
ModalProps { data, isUpdate }。 - 打开编辑弹窗时调用
View获取最新详情。 - 提交时使用
BetterVben.function.formValidate和BetterVben.function.formSubmit。 onConfirm和onOpenChange使用BetterVben.function.modalAutoLoadAsync包裹。- 取消时关闭弹窗并
resetForm。 - 表单组件必须命名为
const [Form, formApi] = useVbenForm(...),模板中必须使用<Form />。 useVbenForm必须使用...BetterVben.form.default作为基础配置,除非已有业务明确需要保留特殊布局。
edit.vue 禁止残留这些旧写法:
DrawerProps
BasicDrawer
BasicForm
EditDrawer
editDrawerApi
reload
useVbenDrawer
formApi.validate()
formApi.getValues()
modalApi.setState({ confirmLoading
drawerApi
showConfirmButton
readonly
only-read
说明:
- 如果旧页面有查看态
view,新风格应交给独立view.vue处理,不要把查看态混进edit.vue。 - 如果旧页面有
id/update/view传参,迁移后统一改成ModalProps { data, isUpdate },由index.vue调用editModalApi.setData({ data: row, isUpdate: !!row })。 - 如果旧页面用
reload事件,迁移后统一改为confirm事件,父组件使用<EditModal @confirm="handleRefresh" />。 - 如果旧页面手写表单校验和 loading,迁移后必须改为
BetterVben.function.formValidate、formSubmit、modalAutoLoadAsync。
6. 改造 view.vue
复制 template/view/view.vue 的结构,再替换真实模块信息。
必须替换:
TemplateCrudModel-> 真实XxxModelView({ id: row.id })-> 真实详情主键参数ViewDescSchema保持新命名
结构要求:
- 使用
Description + useDescription。 - 打开弹窗时从
modalApi.getData()取{ row }。 schema使用ViewDescSchema。- 查看弹窗不提交,模板中保留
:footer="false"。
7. 扫描旧风格残留
迁移后在目标模块执行:
rg -n "querySchema|editSchema|viewSchema|State|newState|BasicDrawer|BasicForm|DrawerProps|useVbenDrawer|rules:\s*null|export const columns|import .*columns|align:\s*'left'|width:\s*-1|formItemClass:\s*['\"]col-span-1['\"]|fieldMappingTime|submitOnChange|submitOnEnter|checkboxConfig|rowConfig|formApi\.validate\(|formApi\.getValues\(|modalApi\.setState\(\{ confirmLoading|drawerApi|emit = defineEmits<\{ reload" apps/web-antd/src/views/iotlcm/目标模块路径
如果扫描结果命中真实旧变量或旧默认配置,继续清理;如果命中确实需要保留的业务配置,必须确认字段名和行为仍然正确,不能直接忽略。
路径二:不影响既有逻辑应用新面板
这种情况适用于旧代码已经被人改过,不能简单整体覆盖。处理目标是只统一外层风格,不改变现有功能。
1. 先做业务盘点
改动前先列出旧代码中的特殊逻辑:
- 工具栏额外按钮,例如导入、审核、同步、分配、启停。
- 操作列额外按钮,例如详情跳转、重置、启用、禁用。
- 特殊 API,例如
Enable、Disable、Sync、Import。 - 查询参数转换,例如时间范围拆分、枚举转数字、过滤空字符串。
- 编辑回显处理,例如 JSON 字符串转对象、数组合并、字段补默认值。
- 提交处理,例如提交前字段清洗、提交后刷新其他数据。
- 权限码和路由跳转。
- 插槽渲染,例如字典、成员摘要、可点击字段、自定义组件。
2. 只替换面板骨架
不要直接删除旧文件后重写。推荐顺序:
- 先把旧文件中所有特殊逻辑标记出来。
- 复制新模板骨架,不要从旧 Drawer 文件上局部改。
- 把旧特殊逻辑按区域放回,只保留业务逻辑,不保留旧面板写法。
- 对比原文件确认功能入口没有少。
区域放置规则:
| 内容 | 放置区域 |
|---|---|
defineEmits、静态标题、权限码、常量枚举 |
常量 |
ref、computed、reactive、inject |
数据 |
| CRUD 方法、自定义业务方法、参数转换 | 方法 |
useVbenVxeGrid、useVbenModal、useVbenForm、useDescription |
组件 |
watch、onMounted 等生命周期 |
周期 |
3. 保持业务行为不变
以下内容不能因套模板而擅自改变:
- API URL。
- 后端参数名。
- 主键字段。
- 权限码。
- 表格字段顺序和必要插槽。
- 新增、编辑、查看的传参结构。
- 提交前后的数据转换。
- 用户已经新增的按钮和业务方法。
如果新模板和旧业务逻辑冲突,以旧业务逻辑为准,只把代码组织方式调成新风格。
这里的“以旧业务逻辑为准”只指业务行为,不包括旧代码结构。下面这些不是业务逻辑,必须按新模板替换:
BasicDrawer/useVbenDrawerBasicFormDrawerPropsreload事件- 手写 loading
- 手写
formApi.validate/getValues - 编辑弹窗里的查看态只读逻辑
- 旧生成器默认
fieldMappingTime/submitOnChange/submitOnEnter - 旧生成器默认
checkboxConfig/rowConfig - 按钮自身使用
mr-2维护按钮组间距
如果旧代码里有真正的时间范围查询、行主键或复选框业务要求,可以在新模板里重新加回,但必须替换成真实字段并验证效果。
4. 逐项回归
完成后人工核对:
- 搜索条件是否都还在。
- 表格列和操作列是否都还在。
- 新增、编辑、查看、删除、批量删除、导出是否可触发。
- 自定义按钮是否仍可触发原方法。
- 编辑回显参数是否还是原主键。
- 提交 payload 是否没有少字段。
命名和代码风格要求
导入顺序:
- Vue 相关
- UI 组件
- API 接口
- 自定义组件
- 常量配置
- 工具函数
- 相对导入
代码区域顺序:
/** ==================== 常量 ==================== */
/** ==================== 数据 ==================== */
/** ==================== 方法 ==================== */
/** ==================== 组件 ==================== */
/** ==================== 周期 ==================== */
如果存在类型声明,可放在 常量 前的 类型 区域:
/** ==================== 类型 ==================== */
验证命令
在 vue 目录下执行:
pnpm exec prettier --check apps/web-antd/src/template/iotlcm-crud-style
pnpm exec eslint apps/web-antd/src/template/iotlcm-crud-style --ext .ts,.tsx,.vue
迁移真实模块后,额外执行目标模块扫描:
rg -n "querySchema|editSchema|viewSchema|State|newState|BasicDrawer|BasicForm|DrawerProps|useVbenDrawer|rules:\s*null|export const columns|import .*columns|align:\s*'left'|width:\s*-1|formItemClass:\s*['\"]col-span-1['\"]|fieldMappingTime|submitOnChange|submitOnEnter|checkboxConfig|rowConfig|formApi\.validate\(|formApi\.getValues\(|modalApi\.setState\(\{ confirmLoading|drawerApi|emit = defineEmits<\{ reload" apps/web-antd/src/views/iotlcm/目标模块路径
全量 typecheck 如果命中项目既有错误,需要再过滤目标模块路径,确认本次迁移目标没有新增类型错误。
API 也必须扫描:
rg -n "params:\s*any|requestClient\.(get|post)<any>" apps/web-antd/src/api/iotlcm/目标模块路径
如果目标 API 目录不存在 type.ts,最终回复必须包含:
未完成 API 类型化:目标 API 目录缺少 type.ts,请先补全类型文件。