# 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
```