# Vben2 (Vue3) CRUD 页面生成技能 ## 概述 本技能用于在 Vben Admin v2 (Vue3 + Vite + TypeScript) 项目中快速生成标准的 CRUD 管理页面。基于对项目的深度分析,提炼出了一套完整的开发规范和代码模板。 > **公共规范**(格式、组件命名、API命名、API层组织、权限模式、CRUD架构、Lint流程)已在公司统一配置 §六 中定义,本文件不再重复。 --- ## 一、项目技术栈 | 技术 | 版本/说明 | |------|-----------| | Vue | 3.x (Composition API + Options API 混用,项目使用 `defineComponent` + `setup()`) | | Vite | 构建工具 | | TypeScript | 主要开发语言 | | Ant Design Vue | UI 组件库 | | Vben Admin v2 | 后台管理框架 | | Windi CSS | 原子化 CSS 框架 | | Pinia / Vuex | 状态管理 | | axios | HTTP 请求 | | i18n | 国际化 (vue-i18n) | | ESLint + Prettier | 代码规范 | ### 路径别名 ``` /@/* → src/* /#/* → types/* ``` --- ## 二、目录结构规范 ### 2.1 功能模块目录结构(标准三件套) 每个业务模块页面放在 `src/views/{module}/{feature}/` 下,通常包含三个核心文件: ``` src/views/{module}/{feature}/ ├── index.vue # 主页面(列表页) ├── {Feature}Drawer.vue # 侧边抽屉表单(新增/编辑) └── {feature}.data.ts # 数据配置(列定义、表单Schema、搜索Schema) ``` **命名规则:** - 目录名:kebab-case(如 `alarm/contacts/`) - 主页面:固定为 `index.vue` - Drawer 组件:PascalCase + `Drawer` 后缀(如 `ContactDrawer.vue`、`DictDrawer.vue`) - 数据配置:kebab-case + `.data.ts` 后缀(如 `config.data.ts`、`dict.data.ts`) - Modal 组件(替代 Drawer 时):PascalCase + `Modal` 后缀(如 `AccountModal.vue`) ### 2.2 API 层目录结构 > 公共原则见公司统一配置 §6.4(一功能一文件、禁止拆散、禁止混合、公共接口单独文件、接口只写一次)。以下仅列出 Vben2 专属结构。 ``` src/api/{module}/ ├── {feature}.ts # 单个功能的全部 API 方法(增删改查等) ├── {feature2}.ts # 另一个功能的全部 API 方法 └── model/ └── {feature}Model.ts # 对应功能的 TypeScript 类型/接口定义 ``` **Vben2 专属规则:** - 不再使用 `index.ts` 统一导出,各页面直接按功能路径导入 - API 文件与 Model 文件一一对应 - API 文件:camelCase(如 `alarmContact.ts`、`dataBoard.ts`) - Model 文件:camelCase + `Model` 后缀(如 `alarmContactModel.ts`、`menuModel.ts`) **导入示例:** ```typescript // 直接按功能路径导入,不经过 index.ts 中转 import { getContactList, addContact, deleteContact } from '/@/api/alarm/alarmContact'; ``` ### 2.3 枚举目录 ``` src/enums/ └── {feature}Enum.ts # camelCase + Enum 后缀 ``` --- ## 三、Vben2 专属命名规范 > 通用命名规范见公司统一配置 §6.2(组件PascalCase、目录kebab-case、页面index.vue)和 §6.3(API方法命名)。以下仅列出 Vben2 专属差异。 ### 3.1 文件命名(Vben2 专属类型) | 类型 | 规则 | 示例 | |------|------|------| | Drawer/Modal 组件 | PascalCase + 后缀 | `ContactDrawer.vue`, `AccountModal.vue` | | 数据配置 | kebab-case + `.data.ts` | `config.data.ts`, `role.data.ts` | | Model 文件 | camelCase + `Model.ts` | `menuModel.ts` | | 枚举文件 | camelCase + `Enum.ts` | `roleEnum.ts` | | Hook 文件 | camelCase, `use` 前缀 | `useBatchDelete.ts` | | 工具文件 | camelCase | `menuUtil.ts` | ### 3.2 Vben2 变量/函数命名 | 类型 | 规则 | 示例 | |------|------|------| | 导出常量(列/表单配置) | camelCase | `columns`, `searchFormSchema`, `formSchema` | | 导出接口/类型 | PascalCase | `ContactModal`, `MenuOperationParams` | | 导出枚举 | PascalCase | `RoleEnum`, `KeysTypeEnum` | | setup 内函数 | camelCase | `handleCreate`, `handleEdit`, `handleDelete`, `handleSuccess` | | setup 内 ref | camelCase | `isUpdate`, `treeData`, `roleMenus` | | 事件处理函数 | `handle` 前缀 | `handleSubmit`, `handleCreate`, `handleEdit` | | 计算属性 | `get` 前缀 | `getTitle`, `getCanBatchDelete` | ### 3.4 API 枚举命名 API 路径使用 PascalCase 枚举: ```typescript enum Api { AccountList = '/user/page', BaseUserUrl = '/user', } ``` 或 ```typescript enum API { alarmContact = '/alarm_contact', } ``` --- ## 四、代码模板 ### 4.1 `{feature}.data.ts` — 数据配置文件模板 ```typescript import { BasicColumn, FormSchema } from '/@/components/Table'; // 按需导入: // import { h } from 'vue'; // import { Tag } from 'ant-design-vue'; // import { Icon } from '/@/components/Icon'; // import { useI18n } from '/@/hooks/web/useI18n'; // import { emailRule, phoneRule } from '/@/utils/rules'; // const { t } = useI18n(); // ==================== 表格列配置 ==================== export const columns: BasicColumn[] = [ { title: '字段名称', // 或 t('i18n.key') dataIndex: 'fieldName', width: 120, // align: 'left', // 默认居中,需要时指定 // slots: { customRender: 'slotName' }, // 自定义渲染插槽 // customRender: ({ record }) => { // 行内自定义渲染 // return h(Tag, { color: 'green' }, () => '文本'); // }, }, { title: '状态', dataIndex: 'status', width: 100, customRender: ({ record }) => { const enable = record.status === 1; const color = enable ? 'green' : 'red'; const text = enable ? '启用' : '停用'; return h(Tag, { color }, () => text); }, }, { title: '创建时间', dataIndex: 'createTime', width: 180, }, ]; // ==================== 搜索表单配置 ==================== export const searchFormSchema: FormSchema[] = [ { field: 'keyword', label: '关键字', component: 'Input', colProps: { span: 8 }, // 搜索表单常用 span: 6 或 8 componentProps: { maxLength: 255, placeholder: '请输入关键字', }, }, { field: 'status', label: '状态', component: 'Select', componentProps: { options: [ { label: '启用', value: 1 }, { label: '停用', value: 0 }, ], }, colProps: { span: 8 }, }, ]; // ==================== 新增/编辑表单配置 ==================== export const formSchema: FormSchema[] = [ { field: 'name', label: '名称', required: true, component: 'Input', componentProps: { maxLength: 255, placeholder: '请输入名称', }, }, { field: 'status', label: '状态', component: 'RadioButtonGroup', defaultValue: 1, componentProps: { options: [ { label: '启用', value: 1 }, { label: '停用', value: 0 }, ], }, }, { field: 'remark', label: '备注', component: 'InputTextArea', componentProps: { maxLength: 255, placeholder: '请输入备注', }, dynamicRules: () => { return [ { required: false, validator: (_, value) => { if (String(value).length > 255) { return Promise.reject('字数不超过255个字'); } return Promise.resolve(); }, }, ]; }, }, { field: 'id', label: '', component: 'Input', show: false, // 隐藏字段,用于传递id }, ]; ``` **FormSchema 常用 component 类型:** - `Input` / `InputTextArea` — 文本输入 - `InputNumber` — 数字输入 - `Select` — 下拉选择 - `RadioButtonGroup` — 单选按钮组 - `Switch` — 开关 - `DatePicker` — 日期选择 - `TreeSelect` / `ApiTreeSelect` — 树形选择 - `IconPicker` — 图标选择 - `Checkbox` — 复选框 **FormSchema 常用属性:** - `ifShow: ({ values }) => boolean` — 条件显示(根据表单其他值) - `dynamicRules` — 动态校验规则 - `rules` — 静态校验规则(如 `phoneRule`, `emailRule`) - `slot` — 自定义插槽 - `show: false` — 隐藏字段(用于传递隐藏值如 id) - `colProps: { lg: 12, md: 24 }` — 栅格布局 - `defaultValue` — 默认值 ### 4.2 `index.vue` — 主列表页模板(标准 CRUD) ```vue 新增{业务名} 批量删除 <{Feature}Drawer @register="registerDrawer" @success="handleSuccess" /> ``` ### 4.3 `{Feature}Drawer.vue` — Drawer 表单模板(标准新增/编辑) ```vue ``` ### 4.4 `{Feature}Modal.vue` — Modal 表单模板(弹窗模式,替代 Drawer) ```vue ``` ### 4.5 API 文件模板 ```typescript import { defHttp } from '/@/utils/http/axios'; import type { {Feature}PageParams, {Feature}Modal, {Feature}Params, {Feature}Info, } from './model/{feature}Model'; import { getPageData } from '../../base'; // 分页查询使用 getPageData enum Api { {FEATURE}_URL = '/{feature_path}', UPDATE_URL = '/{feature_path}/update', } /** * 分页查询{业务名}列表 * @param params 查询参数(含分页) * @returns 分页数据 */ export const get{Feature}List = (params: {Feature}PageParams) => { return getPageData<{Feature}Modal>(params, Api.{FEATURE}_URL); }; /** * 新增{业务名} * @param params 新增参数 */ export const add{Feature} = (params: {Feature}Params) => { return defHttp.post({ url: Api.{FEATURE}_URL, data: params, }); }; /** * 更新{业务名} * @param params 更新参数 */ export const update{Feature} = (params: {Feature}Params) => { return defHttp.post({ url: Api.UPDATE_URL, data: params, }); }; /** * 保存或编辑{业务名} * @param params 表单数据 * @param isUpdate 是否为编辑模式 */ export const saveOrEdit{Feature} = (params: {Feature}Info, isUpdate: boolean) => { if (isUpdate) return update{Feature}(params); return add{Feature}(params); }; /** * 删除{业务名} * @param ids 待删除的ID数组 */ export const delete{Feature} = (ids: string[]) => { return defHttp.delete({ url: Api.{FEATURE}_URL, data: ids, // 方式一:直接传数组 // data: { ids }, // 方式二:包装为对象 }); }; /** * 更新{业务名}状态 * @param id 记录ID * @param status 目标状态值 */ export const set{Feature}Status = (id: string, status: number) => { return defHttp.put({ url: Api.{FEATURE}_URL + '/update_status/' + id + '/' + status }); }; ``` **API 设计模式说明:** - 分页查询:统一使用 `getPageData` 工具函数(基于 `defHttp.get`) - 新增:`defHttp.post` - 更新:`defHttp.put` 或 `defHttp.post`(取决于后端设计) - 删除:`defHttp.delete`,参数为 `string[]`(ID数组) - 保存/编辑合并方法:`saveOrEdit` 模式,通过 `isUpdate` 参数区分 ### 4.6 Model 文件模板 ```typescript import { BasicPageParams } from '/@/api/model/baseModel'; // 分页查询参数 export type {Feature}PageParams = BasicPageParams & { keyword?: string; status?: number; }; // 列表项数据模型 export interface {Feature}Modal { id: string; name: string; status: number; createTime: string; updateTime: string; remark?: string; } // 新增/编辑参数 export interface {Feature}Params { name: string; status: number; remark?: string; } export type {Feature}Info = {Feature}Params; // 通用分页返回模型(如使用 getPageData) // 返回类型为 PageData<{Feature}Modal>,已包含 items + total ``` **基础模型说明(src/api/model/baseModel.ts):** ```typescript export interface BasicPageParams { page: number; pageSize: number; } export interface BasicFetchResult { items: T[]; total: number; } ``` **分页基础请求类(src/api/base.ts):** ```typescript export class BaseQueryRequest implements BaseQueryParams { constructor(page = 1, pageSize = 10) { ... } } export function getPageData(params: BaseQueryParams, url: string): Promise> { ... } ``` --- ## 五、核心组件和 Hooks 速查 ### 5.1 表格 — BasicTable + useTable ```typescript const [registerTable, { reload, setProps, getRowSelection, getSelectRowKeys, setSelectedRowKeys }] = useTable({ title: '列表标题', api: getListApi, // 必须是返回 Promise 的函数 columns, // 从 .data.ts 导入 formConfig: { // 搜索表单配置(与 useSearchForm 配合) labelWidth: 120, schemas: searchFormSchema, resetFunc: resetFn, // 重置回调 }, useSearchForm: true, // 是否显示搜索表单 showTableSetting: true, // 表格设置按钮 bordered: true, // 边框 showIndexColumn: false, // 序号列 rowKey: 'id', // 行唯一标识 canResize: false, // 是否可以自适应高度 pagination: true, // 分页(树表设为 false) isTreeTable: false, // 是否树形表格 striped: false, // 斑马纹 clickToRowSelect: false, // 点击行是否触发选择 actionColumn: { // 操作列 width: 200, title: '操作', dataIndex: 'action', slots: { customRender: 'action' }, fixed: 'right', }, rowSelection: { // 行选择(也可通过 useBatchDelete 注入) type: 'checkbox', }, }); ``` **常用表格方法:** - `reload()` — 重新加载数据 - `setProps(props)` — 动态设置表格属性 - `getRowSelection()` — 获取当前选中行 - `getSelectRowKeys()` — 获取选中行 key - `setSelectedRowKeys(keys)` — 设置选中行 - `collapseAll()` — 折叠所有(树表用) ### 5.2 抽屉 — BasicDrawer + useDrawer / useDrawerInner **父组件:** ```typescript const [registerDrawer, { openDrawer, closeDrawer }] = useDrawer(); // 打开抽屉 openDrawer(true, { isUpdate: false, // 新增模式 // isUpdate: true, // 编辑模式 record: data, // 编辑时传入行数据 }); ``` **子组件(Drawer 内部):** ```typescript const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { await resetFields(); setDrawerProps({ confirmLoading: false }); isUpdate.value = !!data?.isUpdate; if (unref(isUpdate)) { await setFieldsValue({ ...data.record }); } }); ``` ### 5.3 弹窗 — BasicModal + useModal / useModalInner **父组件:** ```typescript const [registerModal, { openModal, closeModal }] = useModal(); // 打开弹窗 openModal(true, { isUpdate: false, record: data, }); ``` **子组件(Modal 内部):** ```typescript const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => { await resetFields(); setModalProps({ confirmLoading: false }); isUpdate.value = !!data?.isUpdate; if (unref(isUpdate)) { await setFieldsValue({ ...data.record }); } }); ``` ### 5.4 表单 — BasicForm + useForm ```typescript const [registerForm, { resetFields, // 重置表单 setFieldsValue, // 设置表单值 validate, // 校验表单 getFieldsValue, // 获取表单值 updateSchema, // 动态更新 Schema(如禁用、隐藏字段) clearValidate, // 清除校验 }] = useForm({ labelWidth: 100, schemas: formSchema, showActionButtonGroup: false, baseColProps: { lg: 12, md: 24 }, // 默认栅格 }); ``` ### 5.5 权限控制 — Authority ```vue 新增 { label: '编辑', auth: 'api:yt:{module}:update', icon: 'clarity:note-edit-line', onClick: handleEdit.bind(null, record), } ``` ### 5.6 批量删除 — useBatchDelete ```typescript import { useBatchDelete } from '/@/hooks/web/useBatchDelete'; const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions, resetSelectedRowKeys } = useBatchDelete(deleteFn, handleSuccess, setProps); // 注入选择配置到表格 nextTick(() => { setProps(selectionOptions); }); // 自定义 checkbox 禁用 selectionOptions.rowSelection.getCheckboxProps = (record: Recordable) => { if (record.status === 1) { return { disabled: true }; } return { disabled: false }; }; ``` ### 5.7 消息提示 — useMessage ```typescript import { useMessage } from '/@/hooks/web/useMessage'; const { createMessage } = useMessage(); createMessage.success('操作成功'); createMessage.error('操作失败'); createMessage.warning('警告信息'); createMessage.info('提示信息'); ``` ### 5.8 确认弹框 — useSyncConfirm ```typescript import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm'; const { createSyncConfirm } = useSyncConfirm(); // 返回 Promise,支持 async/await await createSyncConfirm({ iconType: 'warning', content: '确认后数据将被删除', }); ``` ### 5.9 国际化 — useI18n ```typescript import { useI18n } from '/@/hooks/web/useI18n'; const { t } = useI18n(); // 在 .data.ts 中使用 title: t('routes.common.system.tableTitleSystemMenuName'), // 在组件中使用 const getI18nCreateMenu = computed(() => t('routes.common.system.pageSystemTitleCreateMenu')); ``` ### 5.10 树形组件 — BasicTree ```vue ``` ### 5.11 树形选择 — TreeSelect / ApiTreeSelect ```typescript // 静态树形选择 { field: 'parentId', label: '上级菜单', component: 'TreeSelect', componentProps: { replaceFields: { title: 'menuName', key: 'id', value: 'id' }, getPopupContainer: () => document.body, }, } // 动态 API 树形选择 { field: 'organizationId', label: '所属组织', required: true, component: 'ApiTreeSelect', componentProps: { api: async () => { const data = await getOrganizationList(); copyTransFun(data as any as any[]); return data; }, }, } ``` --- ## 六、编码规范 ### 6.1 组件风格 - **使用 Options API + `defineComponent` + `setup()` 函数**(项目统一风格) - **不使用** `