# CRUD 功能模块开发模板 ## 📋 概述 本模板提供了一套完整的 CRUD 功能模块开发规范,适用于当前项目框架(基于 Vue3 + TypeScript + Element Plus + VxeTable)。通过统一的目录结构、命名规范和代码模板,确保项目代码的一致性和可维护性。 ## 📁 目录结构规范 ### 标准结构(基于当前项目框架) ``` src/apps/{模块名}/ ├── api/ # 模块API接口 ├── assets/ # 模块静态资源 ├── components/ # 模块私有组件 ├── lang/ # 模块国际化文件 ├── mock/ # 模块模拟数据 ├── routes/ # 模块路由配置 ├── schemas/ # 模块CRUD配置 └── views/ # 模块页面视图 ├── {实体名}/ │ ├── index.vue # 页面入口文件 │ ├── schemas/ # Schema配置 │ └── components/ # 私有组件(可选) ``` ### 核心文件位置 - **页面入口**: `src/apps/{模块名}/views/{实体名}/index.vue` - **Schema配置**: `src/apps/{模块名}/views/{实体名}/schemas/index.tsx` - **API服务**: `src/apps/{模块名}/api/{实体名}/index.ts` - **组件库**: `src/components/crud/`(核心CRUD组件) ### ⚠️ 重要注意事项 - **禁止增加不必要的子目录**,避免目录结构混乱 - 保持目录结构简洁统一 - 模块名使用小写字母,多个单词用连字符分隔 - 实体名使用小写字母,多个单词用连字符分隔 - 文件名统一使用小写字母和连字符 - 目录结构必须严格遵循此规范 ## 🔤 命名规范 ### 命名规则 - **数据库表名**:使用下划线命名法,如 `system_user`、`system_user_role` - **模块名**:使用表名中下划线前的部分,如 `system` - **实体名**:使用表名中下划线后的部分,如 `user`,多个部分用中横线连接,如 `user-role` - **文件名**:使用中横线命名法,如 `user-role.ts` ### 命名示例 | 表名 | 模块名 | 实体名 | 实体小驼峰名 | API 服务文件 | API 服务类名 | | --- | --- | --- | --- | --- | --- | | `system_user` | `system` | `user` | `user` | `./src/app/api/system/user.ts` | `UserService` | | `system_user_role` | `system` | `user-role` | `userRole` | `./src/app/api/system/user-role.ts` | `UserRoleService` | ## 📏 约束规则 1. **ID字段**:不在列表上显示 2. **列宽度**:默认自适应,无需手动设置 3. **备注字段**:字段过多时不在列表显示 4. **表单布局**: - 字段较少时:`colSpan` 默认为 24(全宽) - 字段较多时:可设置 `colSpan` 为 12(半宽) ## 🔧 API 服务模板 ### 基础服务模板 **文件路径**: `src/apps/{模块名}/api/{实体名}/index.ts` ```typescript import { BaseService } from '@/utils/request' import { {EntityName}Row } from '@/apps/{模块名}/api/{实体名}/types' /** * {实体名} API 服务 */ export const {EntityName}Service = new BaseService<{EntityName}Row>('/{模块名}/{实体名}') export default {EntityName}Service ``` ### BaseService 配置说明 - **baseUrl**: 接口基础路径 - **allowedMethods**: 允许的方法列表(空数组表示允许所有) - **forbiddenMethods**: 禁止的方法列表(如 `['export']` 表示禁用导出功能) ### 默认包含的接口方法 - `page()`: 分页查询 - `save()`: 新增 - `update()`: 修改 - `remove()`: 删除 - `detail()`: 详情 - `export()`: 导出(可禁用) ## 📋 Schema 配置模板 ### 基础配置模板 **文件路径**: `src/apps/{模块名}/views/{实体名}/schemas/index.tsx` ```typescript import { CrudSchema } from '@/components/crud' import { {EntityName}Service } from '@/apps/{模块名}/api/{实体名}' import { $t } from '@/locales' /** * {实体名} CRUD Schema 配置 */ export const crudSchema = (): CrudSchema => { return { // 接口配置 api: {EntityName}Service.page, // CRUD API 映射 crudApi: { add: {EntityName}Service.save, edit: {EntityName}Service.update, remove: {EntityName}Service.remove, view: {EntityName}Service.detail }, // 功能开关 useCrud: true, hasAdd: true, hasRemove: true, hasEdit: true, hasView: true, // 权限控制 addAuth: '{模块名}:{实体名}:create', removeAuth: '{模块名}:{实体名}:delete', editAuth: '{模块名}:{实体名}:update', viewAuth: '{模块名}:{实体名}:read', // 表格列定义 columns: [ // 选择列 { type: 'selection' }, // 示例列定义 { prop: 'name', label: $t('{模块名}.{实体名}.list.table.columns.name'), minWidth: 120 }, // 更多列定义... ], // 搜索表单配置 searchFormSchema: { schema: [ { prop: 'LIKE_name', label: $t('{模块名}.{实体名}.list.query.name'), component: 'Input', componentProps: { clearable: true, placeholder: $t('{模块名}.{实体名}.list.query.placeholder.name') }, colSpan: 6 } // 更多搜索字段... ] }, // 弹窗配置 dialogProps: { dialogType: 'drawer', width: '50%' }, // 表单配置 formSchema: { showDefaultActions: false, labelWidth: '100px', schema: [ // 表单字段定义... ] } } } ``` ### 配置项说明 | 配置项 | 说明 | 默认值 | | ------------------- | ------------------ | ------- | | `useCrud` | 是否启用 CRUD 功能 | `true` | | `hasAdd` | 是否显示新增按钮 | `true` | | `hasRemove` | 是否显示删除按钮 | `true` | | `hasEdit` | 是否显示编辑按钮 | `true` | | `hasView` | 是否显示详情按钮 | `true` | | `labelWidth` | 表单标签宽度 | `100px` | | `dialogProps.width` | 弹窗宽度 | `50%` | ## 🖥️ 页面入口模板 ### 基础页面模板 **文件路径**: `src/apps/{模块名}/views/{实体名}/index.vue` ```vue ``` ### 操作按钮配置说明 | 配置项 | 说明 | 示例 | | ------------------------ | ------------------ | ------------------ | | `toolbarActions` | 表格上方工具栏按钮 | 导出、批量操作等 | | `dropDownToolbarActions` | 工具栏下拉菜单 | 更多操作 | | `tableActions` | 表格行操作按钮 | 编辑、删除、详情等 | | `dropDownActions` | 行操作下拉菜单 | 扩展操作 | ## ⚙️ 关键配置说明 ### 表格列配置 (columns) ```typescript { prop: '字段名', // 字段名称 label: '列标题', // 列显示标题 component: '组件名', // 渲染组件,如 'ApiDict', 'ApiSelect' 等 componentProps: { // 组件属性 code: '字典编码', // 字典组件特有 renderType: 'Select' // 渲染类型 }, width: 120, // 列宽度(可选) fixed: 'left' // 固定列(可选) } ``` **常用列属性:** - `type: 'selection'`: 选择列 - `type: 'index'`: 序号列 - `prop`: 数据字段名 - `label`: 列标题 - `component`: 渲染组件 - `width`: 列宽度 - `minWidth`: 最小宽度 - `fixed`: 固定列位置 ### 表单字段配置 (formSchema.schema) ```typescript { label: '字段标签', prop: '字段名', component: '组件名', // 如 'Input', 'Select', 'ApiDict' 等 colSpan: 12, // 栅格占比 (1-24) rules: [ // 校验规则 { required: true, message: '校验提示信息' } ], componentProps: { // 组件属性 placeholder: '占位符', clearable: true }, ifShow(values) { // 动态显示条件 return !!values.条件字段 } } ``` **常用表单属性:** - `colSpan`: 栅格宽度(1-24) - `rules`: 校验规则数组 - `componentProps`: 组件特有属性 - `ifShow`: 条件显示函数 - `show`: 静态显示控制 ### 搜索表单配置 (searchFormSchema.schema) ```typescript { prop: '操作符_字段名', // 如 'LIKE_name', 'EQ_status' label: '搜索字段标签', component: 'Input', // 搜索组件 componentProps: { clearable: true, placeholder: '请输入搜索条件' }, colSpan: 6 // 栅格占比 } ``` **常用搜索操作符:** - `EQ_`: 等于 - `LIKE_`: 模糊匹配 - `GT_`: 大于 - `LT_`: 小于 - `IN_`: 包含 ### 树形结构配置 (tree) 对于具有树形结构的数据(如菜单、组织架构等),需要配置 tree 属性: ```typescript export const crudSchema: CrudSchema = { // 使用 tree 接口而不是 page 接口 api: {EntityName}Service.tree, // 树型是全量数据,不需要搜索 useSearchForm: false, // 树形结构配置 tree: { id: 'id', // 树形节点唯一标识字段名 pid: 'parentId', // 父节点字段名 children: 'children' // 子节点字段名 }, // 其他配置... } ``` **树形配置注意事项:** - 必须使用 `tree` 接口而非 `page` 接口 - 禁用搜索表单(`useSearchForm: false`) - 确保 API 返回树形结构数据 ## 🧩 常用组件 ### 基础表单组件 | 组件 | 说明 | 常用属性 | | --------------- | ---------- | -------------------------- | | `Input` | 普通输入框 | `placeholder`, `clearable` | | `InputNumber` | 数字输入框 | `min`, `max`, `precision` | | `Select` | 下拉选择框 | `options`, `clearable` | | `RadioGroup` | 单选框组 | `options` | | `CheckboxGroup` | 多选框组 | `options` | | `DatePicker` | 日期选择器 | `type`, `format` | | `Divider` | 分割线 | `content-position` | ### 异步数据组件 | 组件 | 说明 | 常用属性 | | ------------------ | ------------ | --------------------------------- | | `ApiDict` | 字典组件 | `code`, `renderType` | | `ApiSelect` | 异步下拉选择 | `api`, `labelField`, `valueField` | | `ApiCascader` | 异步级联选择 | `api`, `checkStrictly` | | `ApiTreeSelect` | 异步树形选择 | `api`, `checkStrictly` | | `ApiRadioGroup` | 异步单选框组 | `api` | | `ApiCheckboxGroup` | 异步多选框组 | `api` | ### 特殊功能组件 | 组件 | 说明 | 常用属性 | | ------------ | ------------ | ----------------- | | `IconPicker` | 图标选择器 | - | | `Editor` | 富文本编辑器 | - | | `Upload` | 文件上传组件 | `accept`, `limit` | | `Avatar` | 头像上传组件 | `shape`, `size` | ### ApiDict 组件配置示例 ```typescript { label: '状态', prop: 'status', component: 'ApiDict', componentProps: { code: 'yes_no', // 字典编码 renderType: 'RadioGroup', // 渲染类型:Select/RadioGroup/Checkbox isBtn: true // 按钮样式(RadioGroup特有) } } ``` ### ApiSelect 组件配置示例 ```typescript { label: '用户', prop: 'userId', component: 'ApiSelect', componentProps: { api: '/system/user/select', // 接口地址 labelField: 'name', // 显示字段 valueField: 'id', // 值字段 placeholder: '请选择用户' } } ``` ## 🔒 权限控制 ### 权限码配置 通过以下属性控制功能权限,权限码格式为 `{模块名}:{实体名}:{操作}`: | 权限属性 | 说明 | 示例 | | ------------ | ---------- | -------------------- | | `addAuth` | 新增权限码 | `system:user:create` | | `editAuth` | 编辑权限码 | `system:user:update` | | `removeAuth` | 删除权限码 | `system:user:delete` | | `viewAuth` | 查看权限码 | `system:user:read` | ### 权限配置示例 ```typescript export const crudSchema: CrudSchema = { // ... 其他配置 addAuth: 'system:user:create', removeAuth: 'system:user:delete', editAuth: 'system:user:update', viewAuth: 'system:user:read' // ... } ``` ## 🔄 CRUD 操作方法 ### useCrud 返回方法 通过 `useCrud` 返回的方法可以控制 CRUD 行为: | 方法 | 说明 | 使用场景 | | ----------------- | ------------- | -------------- | | `reload()` | 刷新表格数据 | 手动刷新 | | `setState()` | 设置组件状态 | 动态控制 | | `removeByApi()` | 通过 API 删除 | 自定义删除逻辑 | | `refreshData()` | 全量刷新数据 | 数据同步 | | `refreshCreate()` | 新增后刷新 | 新增操作后 | | `refreshUpdate()` | 更新后刷新 | 编辑操作后 | | `refreshRemove()` | 删除后刷新 | 删除操作后 | ### 方法使用示例 ```typescript const [BasicCrud, { reload, refreshCreate }] = useCrud({ // ... 配置 }) // 手动刷新表格 const handleRefresh = () => { reload() } // 新增后刷新 const handleCreateSuccess = () => { refreshCreate() } ``` ## 📚 完整示例 ### 示例一:普通表 - 岗位管理 (sys_post) #### 1. 接口文件 **文件路径:** `src/apps/system/api/post/index.ts` ```typescript import { BaseService } from '@/utils/request' import { PostRow } from '@/apps/system/api/post/types' /** * 岗位管理 API 服务 */ export const PostService = new BaseService('/system/post') export default PostService ``` #### 2. Schema 配置文件 **文件路径:** `src/apps/system/views/post/schemas/index.tsx` ```typescript import { CrudSchema } from '@/components/crud' import { PostService } from '@/apps/system/api/post' import { $t } from '@/locales' /** 岗位管理 CRUD 配置 */ export const crudSchema = (): CrudSchema => { return { // 基础配置 api: PostService.page, dialogTitle: '岗位', // API 映射 crudApi: { add: PostService.save, edit: PostService.update, remove: PostService.remove, view: PostService.detail }, // 功能开关 useCrud: true, hasAdd: true, hasRemove: true, hasEdit: true, hasView: true, // 权限控制 addAuth: 'system:post:create', removeAuth: 'system:post:delete', editAuth: 'system:post:update', viewAuth: 'system:post:read', // 表格列定义 columns: [ { type: 'selection' }, { prop: 'name', label: $t('system.post.list.table.columns.name'), minWidth: 120 }, { prop: 'code', label: $t('system.post.list.table.columns.code'), minWidth: 120 }, { prop: 'sort', label: $t('system.post.list.table.columns.sort'), minWidth: 80 }, { prop: 'enabled', label: $t('system.post.list.table.columns.enabled'), component: 'ApiDict', componentProps: { code: 'yes_no' }, minWidth: 100 }, { prop: 'remark', label: $t('system.post.list.table.columns.remark'), minWidth: 150 } ], // 搜索表单 searchFormSchema: { schema: [ { prop: 'LIKE_name', label: $t('system.post.list.query.name'), component: 'Input', componentProps: { clearable: true, placeholder: $t('system.post.list.query.placeholder.name') }, colSpan: 6 }, { prop: 'EQ_enabled', label: $t('system.post.list.query.enabled'), component: 'ApiDict', componentProps: { code: 'yes_no', placeholder: $t('system.post.list.query.placeholder.enabled') }, colSpan: 6 } ] }, // 弹窗配置 dialogProps: { dialogType: 'drawer', width: '50%' }, // 表单配置 formSchema: { showDefaultActions: false, labelWidth: '100px', schema: [ { label: $t('system.post.form.fields.id'), prop: 'id', component: 'Input', colSpan: 24, show: false, ifDetail: false }, { label: $t('system.post.form.fields.name'), prop: 'name', component: 'Input', rules: [{ required: true, message: $t('system.post.form.rules.name.required') }], componentProps: { placeholder: $t('system.post.form.placeholder.name') }, colSpan: 24 }, { label: $t('system.post.form.fields.code'), prop: 'code', component: 'Input', rules: [{ required: true, message: $t('system.post.form.rules.code.required') }], componentProps: { placeholder: $t('system.post.form.placeholder.code') }, colSpan: 24 }, { label: $t('system.post.form.fields.sort'), prop: 'sort', component: 'InputNumber', componentProps: { min: 0, placeholder: $t('system.post.form.placeholder.sort') }, colSpan: 24 }, { label: $t('system.post.form.fields.enabled'), prop: 'enabled', component: 'ApiDict', componentProps: { code: 'yes_no', isBtn: true, renderType: 'RadioGroup' }, rules: [{ required: true, message: $t('system.post.form.rules.enabled.required') }], colSpan: 24 }, { label: $t('system.post.form.fields.remark'), prop: 'remark', component: 'Input', componentProps: { type: 'textarea', placeholder: $t('system.post.form.placeholder.remark') }, colSpan: 24 } ] } } } ``` #### 3. 页面入口文件 **文件路径:** `src/apps/system/views/post/index.vue` ```vue ``` ### 示例二:树型表 - 部门管理 (sys_dept) #### 1. 接口文件 **文件路径:** `src/apps/system/api/dept/index.ts` ```typescript import { BaseService } from '@/utils/request' import { DeptRow } from '@/apps/system/api/dept/types' /** * 部门管理 API 服务 */ export const DeptService = new BaseService('/system/dept') // 扩展树形接口 export const getDeptTree = (args?: Record) => { const params = { ...args, format: 'tree' } return DeptService.list(params) } export default DeptService ``` #### 2. Schema 配置文件 **文件路径:** `src/apps/system/views/dept/schemas/index.tsx` ```tsx import { CrudSchema } from '@/components/crud' import { DeptService, getDeptTree } from '@/apps/system/api/dept' import { $t } from '@/locales' /** 部门管理 CRUD 配置 */ export const crudSchema = (): CrudSchema => { return { // 接口配置:使用 tree 接口以支持树形结构 api: getDeptTree, // CRUD API 映射 crudApi: { add: DeptService.save, edit: DeptService.update, remove: DeptService.remove, view: DeptService.detail }, // 功能开关 useCrud: true, hasAdd: true, hasRemove: true, hasEdit: true, hasView: true, // 权限控制 addAuth: 'system:dept:create', removeAuth: 'system:dept:delete', editAuth: 'system:dept:update', viewAuth: 'system:dept:read', // 禁用搜索表单,因为树形结构是全量数据 useSearchForm: false, // 树形结构配置 tree: { id: 'id', pid: 'pid', children: 'children' }, columns: [ { type: 'selection' }, { prop: 'name', label: $t('system.dept.list.table.columns.name'), component: 'Input', minWidth: 150 }, { prop: 'code', label: $t('system.dept.list.table.columns.code'), component: 'Input', minWidth: 120 }, { prop: 'sort', label: $t('system.dept.list.table.columns.sort'), component: 'InputNumber', minWidth: 80 }, { prop: 'enabled', label: $t('system.dept.list.table.columns.enabled'), component: 'ApiDict', componentProps: { code: 'yes_no', renderType: 'RadioGroup' }, minWidth: 100 } ], searchFormSchema: { schema: [] }, // 弹窗配置 dialogProps: { dialogType: 'drawer', width: '50%' }, // 表单配置 formSchema: { showDefaultActions: false, labelWidth: '100px', schema: [ { label: $t('system.dept.form.fields.id'), prop: 'id', component: 'Input', colSpan: 24, show: false, ifDetail: false }, { label: $t('system.dept.form.fields.pid'), prop: 'pid', component: 'ApiTreeSelect', componentProps: { api: '/system/dept/tree', labelField: 'name', valueField: 'id', // 任意节点可选 checkStrictly: true, clearable: true, placeholder: '请选择父级部门' }, colSpan: 24, rules: [ { required: true, message: '请选择父级部门' } ] }, { label: '部门名称', prop: 'name', component: 'Input', componentProps: { placeholder: '请输入部门名称' }, colSpan: 24, rules: [ { required: true, message: '请输入部门名称' } ] }, { label: '唯一编码', prop: 'code', component: 'Input', componentProps: { placeholder: '请输入唯一编码' }, colSpan: 24, rules: [ { required: true, message: '请输入唯一编码' } ] }, { label: '排序', prop: 'sort', component: 'InputNumber', componentProps: { min: 0, max: 9999 }, colSpan: 24, rules: [ { required: true, message: '请输入排序值' } ] }, { label: '是否启用', prop: 'enabled', component: 'ApiDict', componentProps: { code: 'yes_no', isBtn: true, renderType: 'RadioGroup' }, colSpan: 24, rules: [ { required: true, message: '请选择启用状态' } ] }, { label: '部门负责人', prop: 'leaderIdList', component: 'ApiSelect', componentProps: { api: '/system/user/select', placeholder: '请选择负责人', multiple: true }, colSpan: 24 }, { label: '分管领导', prop: 'main_leader_id', component: 'ApiSelect', componentProps: { api: '/system/user/select', placeholder: '请选择分管领导' }, colSpan: 24 }, { label: '备注', prop: 'remark', component: 'Input', componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' }, colSpan: 24 } ] } } } ``` #### 3. 页面入口文件 **文件路径:** `src/apps/system/views/dept/index.vue` ```vue ```