CURD 快速开发
CURD 组件是一套配置化的快速开发解决方案,通过声明式配置实现增删改查功能,极大提高开发效率。
介绍
CURD 组件由三个核心组件和一个组合函数组成:
- PageSearch - 搜索表单组件,提供条件筛选功能
- PageContent - 数据列表组件,展示表格数据
- PageModal - 表单弹窗组件,处理新增和编辑
- usePage - 页面逻辑组合函数,统一管理页面状态和事件
快速开始
基本结构
一个完整的 CURD 页面包含以下四个部分:
vue
<template>
<div class="app-container">
<!-- 1. 搜索表单 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 2. 数据列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@operate-click="handleOperateClick"
>
<!-- 自定义列插槽 -->
<template #status="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</page-content>
<!-- 3. 新增弹窗 -->
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
/>
<!-- 4. 编辑弹窗 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
/>
</div>
</template>
<script setup lang="ts">
import UserAPI from '@/api/user-api'
import type { ISearchConfig, IContentConfig, IModalConfig } from '@/components/CURD/types'
import usePage from '@/components/CURD/usePage'
// 使用 usePage 管理页面逻辑
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleSubmitClick,
handleOperateClick
} = usePage()
// 配置项见下文详解
const searchConfig: ISearchConfig = { /* ... */ }
const contentConfig: IContentConfig = { /* ... */ }
const addModalConfig: IModalConfig = { /* ... */ }
const editModalConfig: IModalConfig = { /* ... */ }
</script>一、查询(PageSearch)
1.1 基础配置
SearchConfig 配置项:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| permPrefix | 权限前缀 | string | - |
| formItems | 表单项配置 | IFormItem[] | [] |
FormItem 配置项:
| 参数 | 说明 | 类型 | 可选值 |
|---|---|---|---|
| type | 表单项类型 | string | input / select / date-picker / tree-select |
| label | 标签文本 | string | - |
| prop | 绑定字段 | string | - |
| placeholder | 占位符 | string | - |
| attrs | 组件属性 | object | - |
| options | 选项数据(select使用) | OptionType[] | - |
| tips | 提示信息 | string | - |
1.2 完整示例
typescript
const searchConfig: ISearchConfig = reactive({
permPrefix: 'sys:user', // 权限前缀
formItems: [
// 输入框
{
type: 'input',
label: '关键字',
prop: 'keywords',
tips: '支持模糊搜索',
attrs: {
placeholder: '用户名/昵称/手机号',
clearable: true,
style: { width: '200px' }
}
},
// 下拉选择
{
type: 'select',
label: '状态',
prop: 'status',
attrs: {
placeholder: '全部',
clearable: true,
style: { width: '200px' }
},
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
},
// 树形选择
{
type: 'tree-select',
label: '部门',
prop: 'deptId',
attrs: {
placeholder: '请选择',
data: deptArr, // 树形数据
filterable: true,
'check-strictly': true,
'render-after-expand': false,
clearable: true,
style: { width: '200px' }
}
},
// 日期范围
{
type: 'date-picker',
label: '创建时间',
prop: 'createTime',
attrs: {
type: 'daterange',
'range-separator': '~',
'start-placeholder': '开始时间',
'end-placeholder': '截止时间',
'value-format': 'YYYY-MM-DD',
style: { width: '240px' }
}
}
]
})1.3 事件处理
typescript
const {
searchRef,
handleQueryClick, // 查询事件
handleResetClick // 重置事件
} = usePage()模板中使用:
vue
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>二、列表展示(PageContent)
2.1 基础配置
ContentConfig 配置项:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| permPrefix | 权限前缀 | string | - |
| pk | 主键字段 | string | 'id' |
| indexAction | 查询接口 | Function | - |
| deleteAction | 删除接口 | Function | - |
| exportAction | 导出接口 | Function | - |
| table | 表格配置 | object | - |
| pagination | 分页配置 | object | - |
| toolbar | 工具栏按钮 | Array | - |
| cols | 列配置 | IColumn[] | - |
| parseData | 数据解析函数 | Function | - |
2.2 表格列配置
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
permPrefix: 'sys:user',
pk: 'id', // 主键字段
// 表格配置
table: {
border: true,
highlightCurrentRow: true
},
// 分页配置
pagination: {
background: true,
layout: 'prev,pager,next,jumper,total,sizes',
pageSize: 20,
pageSizes: [10, 20, 30, 50]
},
// API 接口
indexAction: UserAPI.getPage, // 查询
deleteAction: UserAPI.deleteByIds, // 删除
exportAction: UserAPI.export, // 导出
// 数据解析
parseData(res: any) {
return {
total: res.total,
list: res.list
}
},
// 工具栏按钮
toolbar: [
'add', // 新增
'delete', // 批量删除
'import', // 导入
'export', // 导出
// 自定义按钮
{
name: 'custom1',
text: '自定义按钮',
perm: 'add',
attrs: { icon: 'plus', color: '#626AEF' }
}
],
// 默认工具栏
defaultToolbar: ['refresh', 'filter', 'search'],
// 列配置
cols: [
// 多选框
{ type: 'selection', width: 50, align: 'center' },
// 普通列
{ label: '编号', align: 'center', prop: 'id', width: 100 },
{ label: '用户名', align: 'center', prop: 'username' },
{ label: '昵称', align: 'center', prop: 'nickname', width: 120 },
// 图片列
{ label: '头像', align: 'center', prop: 'avatar', templet: 'image' },
// 自定义列(使用插槽)
{
label: '性别',
align: 'center',
prop: 'gender',
width: 100,
templet: 'custom',
slotName: 'gender'
},
// 状态列
{
label: '状态',
align: 'center',
prop: 'status',
templet: 'custom',
slotName: 'status'
},
// 筛选列
{
label: '角色',
align: 'center',
prop: 'roleNames',
width: 120,
columnKey: 'roleIds',
filters: [],
filterMultiple: true,
filterJoin: ',',
// 动态加载筛选项
async initFn(colItem: any) {
const roleOptions = await RoleAPI.getOptions()
colItem.filters = roleOptions.map(item => ({
text: item.label,
value: item.value
}))
}
},
// 操作列
{
label: '操作',
align: 'center',
fixed: 'right',
width: 220,
templet: 'tool',
operat: [
{
name: 'detail',
text: '详情',
attrs: { icon: 'Document', type: 'primary' }
},
'edit', // 内置编辑按钮
'delete' // 内置删除按钮
]
}
]
})2.3 自定义列插槽
在模板中使用自定义插槽:
vue
<page-content ref="contentRef" :content-config="contentConfig">
<!-- 性别列 -->
<template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template>
<!-- 状态列 -->
<template #status="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<!-- 手机号列(带复制按钮) -->
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
:style="{ marginLeft: '2px' }"
/>
</template>
</page-content>2.4 事件处理
typescript
const {
contentRef,
handleAddClick, // 新增按钮点击
handleExportClick, // 导出按钮点击
handleSearchClick, // 搜索按钮点击
handleFilterChange // 筛选变化
} = usePage()
// 自定义工具栏点击
function handleToolbarClick(name: string) {
if (name === 'custom1') {
ElMessage.success('点击了自定义按钮')
}
}
// 操作列按钮点击
const handleOperateClick = (data: IObject) => {
if (data.name === 'detail') {
// 查看详情
handleViewClick(data.row, async () => {
return await UserAPI.getFormData(data.row.id)
})
} else if (data.name === 'edit') {
// 编辑
handleEditClick(data.row, async () => {
return await UserAPI.getFormData(data.row.id)
})
}
}模板中使用:
vue
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@export-click="handleExportClick"
@toolbar-click="handleToolbarClick"
@operate-click="handleOperateClick"
@filter-change="handleFilterChange"
>
<!-- 插槽... -->
</page-content>三、新增(PageModal - 新增模式)
3.1 新增配置
ModalConfig 配置项:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| permPrefix | 权限前缀 | string | - |
| dialog | 弹窗配置 | object | - |
| form | 表单配置 | object | - |
| formAction | 提交接口 | Function | - |
| beforeSubmit | 提交前钩子 | Function | - |
| formItems | 表单项配置 | IFormItem[] | - |
3.2 完整示例
typescript
const addModalConfig: IModalConfig<UserForm> = reactive({
permPrefix: 'sys:user',
// 弹窗配置
dialog: {
title: '新增用户',
width: 800,
draggable: true // 可拖拽
},
// 表单配置
form: {
labelWidth: 100
},
// 提交接口
formAction: UserAPI.create,
// 提交前处理
beforeSubmit(data: any) {
console.log('提交之前处理', data)
// 可以在这里处理数据格式
},
// 表单项
formItems: [
// 输入框
{
label: '用户名',
prop: 'username',
type: 'input',
rules: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
attrs: {
placeholder: '请输入用户名'
},
col: {
xs: 24, // 移动端占满
sm: 12 // PC端占一半
}
},
// 树形选择
{
label: '所属部门',
prop: 'deptId',
type: 'tree-select',
rules: [
{ required: true, message: '所属部门不能为空', trigger: 'change' }
],
attrs: {
placeholder: '请选择所属部门',
data: deptArr,
filterable: true,
'check-strictly': true,
'render-after-expand': false
}
},
// 自定义组件(使用插槽)
{
type: 'custom',
label: '性别',
prop: 'gender',
initialValue: 1, // 默认值
attrs: { style: { width: '100%' } }
},
// 多选
{
label: '角色',
prop: 'roleIds',
type: 'select',
rules: [
{ required: true, message: '用户角色不能为空', trigger: 'change' }
],
attrs: {
placeholder: '请选择',
multiple: true
},
options: roleArr,
initialValue: [] // 多选默认为空数组
},
// 带正则校验的输入框
{
type: 'input',
label: '手机号码',
prop: 'mobile',
rules: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
],
attrs: {
placeholder: '请输入手机号码',
maxlength: 11
}
},
// 单选框
{
label: '状态',
prop: 'status',
type: 'radio',
options: [
{ label: '正常', value: 1 },
{ label: '禁用', value: 0 }
],
initialValue: 1
}
]
})3.3 自定义表单项插槽
vue
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
>
<!-- 性别字段使用字典选择器 -->
<template #gender="scope">
<DictSelect
v-model="scope.formData[scope.prop]"
code="gender"
v-bind="scope.attrs"
/>
</template>
</page-modal>3.4 事件处理
typescript
const {
addModalRef,
handleAddClick, // 打开新增弹窗
handleSubmitClick // 提交表单
} = usePage()四、编辑(PageModal - 编辑模式)
4.1 编辑配置
编辑配置与新增类似,但需要指定主键和数据获取方式:
typescript
const editModalConfig: IModalConfig<UserForm> = reactive({
permPrefix: 'sys:user',
// 使用抽屉而非弹窗
component: 'drawer',
drawer: {
title: '修改用户',
size: useAppStore().device === DeviceEnum.MOBILE ? '80%' : 500
},
// 主键字段
pk: 'id',
// 提交接口
formAction(data: any) {
return UserAPI.update(data.id as string, data)
},
// 提交前处理
beforeSubmit(data: any) {
console.log('beforeSubmit', data)
},
// 表单项
formItems: [
// 只读字段
{
label: '用户名',
prop: 'username',
type: 'input',
rules: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
attrs: {
placeholder: '请输入用户名',
readonly: true // 编辑时不可修改
}
},
// 其他字段与新增相同...
// 开关组件
{
label: '状态',
prop: 'status',
type: 'switch',
attrs: {
inlinePrompt: true,
activeText: '正常',
inactiveText: '禁用',
activeValue: 1,
inactiveValue: 0
}
}
]
})4.2 事件处理
typescript
const {
editModalRef,
handleEditClick, // 打开编辑弹窗
handleViewClick, // 打开查看弹窗
handleSubmitClick // 提交表单
} = usePage()
// 操作列按钮点击
const handleOperateClick = (data: IObject) => {
if (data.name === 'detail') {
// 查看详情(只读模式)
editModalConfig.drawer = { ...editModalConfig.drawer, title: '查看' }
handleViewClick(data.row, async () => {
return await UserAPI.getFormData(data.row.id)
})
} else if (data.name === 'edit') {
// 编辑
editModalConfig.drawer = { ...editModalConfig.drawer, title: '修改' }
handleEditClick(data.row, async () => {
return await UserAPI.getFormData(data.row.id)
})
}
}模板中使用:
vue
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<DictSelect
v-model="scope.formData[scope.prop]"
code="gender"
v-bind="scope.attrs"
/>
</template>
</page-modal>五、删除
5.1 单个删除
在列配置中添加删除按钮:
typescript
cols: [
{
label: '操作',
align: 'center',
fixed: 'right',
width: 220,
templet: 'tool',
operat: [
'edit',
'delete' // 内置删除按钮
]
}
]删除接口配置:
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
pk: 'id', // 主键字段
deleteAction: UserAPI.deleteByIds // 删除接口
})5.2 批量删除
工具栏添加批量删除按钮:
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
toolbar: [
'delete' // 批量删除按钮
],
cols: [
{ type: 'selection', width: 50, align: 'center' } // 必须有多选框
]
})删除时会自动获取选中行的主键值并调用 deleteAction 接口。
六、导入导出
6.1 导出功能
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
toolbar: ['export'], // 工具栏添加导出按钮
exportAction: UserAPI.export // 导出接口
})6.2 导入功能
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
toolbar: ['import'], // 工具栏添加导入按钮
importAction(file: File) {
return UserAPI.import('1', file)
},
importTemplate: UserAPI.downloadTemplate // 模板下载接口
})6.3 前端导入导出
不依赖后端接口,前端处理数据:
typescript
const contentConfig: IContentConfig<UserPageQuery> = reactive({
// 工具栏添加前端导入导出按钮
defaultToolbar: ['imports', 'exports'],
// 前端导入
importsAction(data: any) {
console.log('导入的数据', data)
// 处理导入数据
return Promise.resolve()
},
// 前端导出
async exportsAction(params: any) {
const res = await UserAPI.getPage(params)
console.log('导出的数据', res.list)
return res.list
}
})七、高级功能
7.1 响应式布局
表单项支持响应式布局:
typescript
{
label: '用户名',
prop: 'username',
type: 'input',
col: {
xs: 24, // 手机端占满整行
sm: 12, // 平板占半行
md: 8, // 桌面占三分之一
lg: 6 // 大屏占四分之一
}
}7.2 动态表单
根据条件动态显示表单项:
typescript
{
label: '指定用户',
prop: 'targetUserIds',
type: 'select',
// 根据其他字段值决定是否显示
hidden: (formData) => formData.targetType !== 2,
attrs: {
multiple: true
}
}7.3 自定义校验
typescript
{
label: '确认密码',
prop: 'confirmPassword',
type: 'input',
rules: [
{
validator: (rule, value, callback) => {
if (value !== formData.password) {
callback(new Error('两次密码输入不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}7.4 联动效果
typescript
{
label: '省份',
prop: 'province',
type: 'select',
options: provinceArr,
attrs: {
onChange: (value: any) => {
// 省份改变时清空城市
formData.city = ''
// 加载对应省份的城市列表
loadCities(value)
}
}
}八、完整示例
参考项目源码:
- 拆分版示例:
src/views/demo/curd-single.vue - 整合版示例:
src/views/demo/curd/index.vue - 实际应用:
src/views/system/user/index.vue
在线体验:CURD 演示
九、Props
PageSearch Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| search-config | 搜索配置 | ISearchConfig | - |
PageContent Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| content-config | 列表配置 | IContentConfig | - |
PageModal Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| modal-config | 弹窗配置 | IModalConfig | - |
十、Events
PageSearch Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| query-click | 查询按钮点击 | (params: any) |
| reset-click | 重置按钮点击 | () |
PageContent Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| add-click | 新增按钮点击 | () |
| export-click | 导出按钮点击 | () |
| toolbar-click | 工具栏按钮点击 | (name: string) |
| operate-click | 操作列按钮点击 | (data: IObject) |
| filter-change | 筛选变化 | (filters: any) |
PageModal Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| submit-click | 表单提交 | (formData: any) |
十一、功能特性
- ✅ 配置化开发 - 通过配置实现增删改查,减少重复代码
- ✅ 类型安全 - 完整的 TypeScript 类型定义
- ✅ 权限控制 - 基于权限前缀的按钮显示控制
- ✅ 响应式布局 - 支持多种设备的自适应布局
- ✅ 自定义插槽 - 灵活的插槽机制满足个性化需求
- ✅ 导入导出 - 内置导入导出功能
- ✅ 数据校验 - 强大的表单校验能力
- ✅ 弹窗/抽屉 - 支持弹窗和抽屉两种展示方式
- ✅ 批量操作 - 支持批量删除等批量操作
十二、使用技巧
技巧 1:共享选项数据
多个配置需要使用同样的选项数据时,提取为 ref 共享:
typescript
const deptArr = ref<OptionType[]>([])
const roleArr = ref<OptionType[]>([])
// 在搜索、新增、编辑中共享
searchConfig.formItems[0].attrs.data = deptArr
addModalConfig.formItems[0].attrs.data = deptArr
editModalConfig.formItems[0].attrs.data = deptArr技巧 2:异步加载选项
typescript
const initOptions = async () => {
const [dept, roles] = await Promise.all([
DeptAPI.getOptions(),
RoleAPI.getOptions()
])
deptArr.value = dept
roleArr.value = roles
}
onMounted(() => {
initOptions()
})技巧 3:条件显示按钮
根据权限或其他条件显示工具栏按钮:
typescript
const toolbar = computed(() => {
const btns = ['add', 'delete']
if (userHasImportPerm()) {
btns.push('import')
}
return btns
})技巧 4:自定义操作按钮
typescript
operat: [
{
name: 'reset_pwd',
text: '重置密码',
attrs: {
icon: 'refresh-left',
style: {
'--el-button-text-color': '#626AEF'
}
},
// 按钮显示条件
show: (row) => row.status === 1
}
]十三、注意事项
- 必须设置主键:
pk字段用于删除和编辑功能 - 响应式配置:配置对象需要使用
reactive包裹 - 接口格式:后端接口需要返回符合
parseData定义的格式 - 权限控制:按钮权限基于
permPrefix:操作名格式 - 初始化时机:选项数据需要在页面挂载时异步加载
