# IOTLCM CRUD 新风格模板 本目录用于把默认生成或旧风格的 IOTLCM CRUD 页面迁移为当前统一风格。模板是可复制使用的示例,迁移时先复制文件,再按真实模块替换类型名、API 路径、字段、主键、权限码和表格标题。 ## 目录说明 ```text 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 })`。 ## 迁移前检查 1. 确认视图目录和 API 目录是一一对应的,例如: - 视图:`apps/web-antd/src/views/iotlcm/tLifeCycle/tExample` - API:`apps/web-antd/src/api/iotlcm/tLifeCycle/tExample` 2. 先打开真实模块的 `type.ts`,确认主键字段。不要默认所有模块都用 `id`,也可能是 `appKey`、`deptId`、`regionId` 等。 3. 记录原代码里的业务差异:自定义按钮、特殊 API、权限码、查询参数转换、插槽渲染、弹窗传参、提交前后处理。 4. 检查本地 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 类型化迁移”。 必须包含这些类型: ```text XxxModel XxxListParam XxxListRes XxxExportParam XxxEditParam XxxViewParam XxxViewRes XxxDeleteParam ``` 规则: - `XxxListRes` 使用 `GlobalListRes`。 - `XxxExportParam` 通常复用 `XxxListParam`。 - `XxxViewParam` 写真实主键字段。 - `XxxDeleteParam` 按后端约定写单值或数组。 - 字段注释保留或补齐,便于后续判断主键和业务字段。 ### 2. 替换 API index.ts 用 `template/api/index.ts` 的结构改造真实 API 文件。 必须满足: - 从 `./type` 导入请求参数和响应类型。 - `List(params: XxxListParam)` 返回 `requestClient.get(...)`。 - `View(params: XxxViewParam)` 返回 `requestClient.get(...)`。 - `Edit/Delete/Export` 使用明确参数类型。 - 保留函数名 `List/Delete/Edit/View/Export`。 - 不再使用 `params: any`、`requestClient.get` 这类旧写法。 API 验收时必须同时满足: ```text 不存在:params: any 不存在:requestClient.get 存在:List(params: XxxListParam) 存在:requestClient.get 存在:View(params: XxxViewParam) 存在:requestClient.get ``` ### 3. 改造 model.tsx 把旧导出名统一为新导出名: | 旧风格 | 新风格 | 用途 | | ------------- | ------------------ | ------------ | | `querySchema` | `IndexGridSchema` | 表格查询表单 | | `columns` | `IndexGridColumns` | 表格列 | | `editSchema` | `EditFormSchema` | 编辑表单 | | `viewSchema` | `ViewDescSchema` | 查看描述 | 必须清理旧残留: ```text 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` 禁止残留: ```text 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`。 - 保留新增、批量删除、导出、查看、编辑、删除的标准入口。 - 工具栏按钮必须放在 `
` 中,按钮自身使用 `class="flex items-center"`。 - 操作列按钮必须放在 `
` 中,链接按钮自身使用 `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` -> 真实 `XxxModel` - `View({ 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(...)`,模板中必须使用 `
`。 - `useVbenForm` 必须使用 `...BetterVben.form.default` 作为基础配置,除非已有业务明确需要保留特殊布局。 `edit.vue` 禁止残留这些旧写法: ```text 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` 事件,父组件使用 ``。 - 如果旧页面手写表单校验和 loading,迁移后必须改为 `BetterVben.function.formValidate`、`formSubmit`、`modalAutoLoadAsync`。 ### 6. 改造 view.vue 复制 `template/view/view.vue` 的结构,再替换真实模块信息。 必须替换: - `TemplateCrudModel` -> 真实 `XxxModel` - `View({ id: row.id })` -> 真实详情主键参数 - `ViewDescSchema` 保持新命名 结构要求: - 使用 `Description + useDescription`。 - 打开弹窗时从 `modalApi.getData()` 取 `{ row }`。 - `schema` 使用 `ViewDescSchema`。 - 查看弹窗不提交,模板中保留 `:footer="false"`。 ### 7. 扫描旧风格残留 迁移后在目标模块执行: ```bash 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. 只替换面板骨架 不要直接删除旧文件后重写。推荐顺序: 1. 先把旧文件中所有特殊逻辑标记出来。 2. 复制新模板骨架,不要从旧 Drawer 文件上局部改。 3. 把旧特殊逻辑按区域放回,只保留业务逻辑,不保留旧面板写法。 4. 对比原文件确认功能入口没有少。 区域放置规则: | 内容 | 放置区域 | | ----------------------------------------------------------------- | -------- | | `defineEmits`、静态标题、权限码、常量枚举 | `常量` | | `ref`、`computed`、`reactive`、`inject` | `数据` | | CRUD 方法、自定义业务方法、参数转换 | `方法` | | `useVbenVxeGrid`、`useVbenModal`、`useVbenForm`、`useDescription` | `组件` | | `watch`、`onMounted` 等生命周期 | `周期` | ### 3. 保持业务行为不变 以下内容不能因套模板而擅自改变: - API URL。 - 后端参数名。 - 主键字段。 - 权限码。 - 表格字段顺序和必要插槽。 - 新增、编辑、查看的传参结构。 - 提交前后的数据转换。 - 用户已经新增的按钮和业务方法。 如果新模板和旧业务逻辑冲突,以旧业务逻辑为准,只把代码组织方式调成新风格。 这里的“以旧业务逻辑为准”只指业务行为,不包括旧代码结构。下面这些不是业务逻辑,必须按新模板替换: - `BasicDrawer/useVbenDrawer` - `BasicForm` - `DrawerProps` - `reload` 事件 - 手写 loading - 手写 `formApi.validate/getValues` - 编辑弹窗里的查看态只读逻辑 - 旧生成器默认 `fieldMappingTime/submitOnChange/submitOnEnter` - 旧生成器默认 `checkboxConfig/rowConfig` - 按钮自身使用 `mr-2` 维护按钮组间距 如果旧代码里有真正的时间范围查询、行主键或复选框业务要求,可以在新模板里重新加回,但必须替换成真实字段并验证效果。 ### 4. 逐项回归 完成后人工核对: - 搜索条件是否都还在。 - 表格列和操作列是否都还在。 - 新增、编辑、查看、删除、批量删除、导出是否可触发。 - 自定义按钮是否仍可触发原方法。 - 编辑回显参数是否还是原主键。 - 提交 payload 是否没有少字段。 ## 命名和代码风格要求 导入顺序: 1. Vue 相关 2. UI 组件 3. API 接口 4. 自定义组件 5. 常量配置 6. 工具函数 7. 相对导入 代码区域顺序: ```ts /** ==================== 常量 ==================== */ /** ==================== 数据 ==================== */ /** ==================== 方法 ==================== */ /** ==================== 组件 ==================== */ /** ==================== 周期 ==================== */ ``` 如果存在类型声明,可放在 `常量` 前的 `类型` 区域: ```ts /** ==================== 类型 ==================== */ ``` ## 验证命令 在 `vue` 目录下执行: ```bash 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 ``` 迁移真实模块后,额外执行目标模块扫描: ```bash 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 也必须扫描: ```bash rg -n "params:\s*any|requestClient\.(get|post)" apps/web-antd/src/api/iotlcm/目标模块路径 ``` 如果目标 API 目录不存在 `type.ts`,最终回复必须包含: ```text 未完成 API 类型化:目标 API 目录缺少 type.ts,请先补全类型文件。 ```