CURD 快速开发
CURD 组件是一套配置化的快速开发方案,通过声明式配置快速搭建"搜索 + 表格 + 弹窗表单"的增删改查页面。
快速导航
| 章节 | 说明 |
|---|---|
| 介绍 | 组件概述与源码位置 |
| 快速开始 | 5 分钟上手示例 |
| 查询 | PageSearch 搜索表单配置 |
| 列表 | PageContent 表格配置 |
| 新增 | PageModal 新增表单配置 |
| 编辑 | PageModal 编辑表单配置 |
| 删除 | 删除功能配置 |
| 导入导出 | Excel 导入导出配置 |
| 高级功能 | 权限控制、数据回显等 |
介绍
CURD 组件由三个核心组件和一个组合函数组成:
- PageSearch - 搜索表单组件,提供条件筛选功能
- PageContent - 数据列表组件,展示表格数据
- PageModal - 表单弹窗组件,处理新增和编辑
- usePage - 页面逻辑组合函数,统一管理页面状态和事件
什么时候用
- 你希望用一套统一模式快速落地列表页(系统管理、基础数据管理等)。
- 你的接口已经具备分页查询/新增/修改/删除/导入/导出能力(按需配置)。
源码位置
src/components/CURD/PageSearch.vuesrc/components/CURD/PageContent.vuesrc/components/CURD/PageModal.vuesrc/components/CURD/types.tssrc/components/CURD/usePage.ts- 示例页面:
src/views/demo/curd/index.vuesrc/views/demo/curd-single.vue
快速开始
基本结构
一个完整的 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/system/user";
import type {
IContentConfig,
IModalConfig,
IOperateData,
ISearchConfig,
} from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
// 使用 usePage 管理页面逻辑
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleViewClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 表格操作列点击事件(由 PageContent emit)
const handleOperateClick = (data: IOperateData) => {
switch (data.name) {
case "view":
handleViewClick(data.row);
break;
case "edit":
handleEditClick(data.row);
break;
default:
break;
}
};
// 配置项见下文详解
const searchConfig: ISearchConfig = {
/* ... */
};
const contentConfig: IContentConfig = {
/* ... */
};
const addModalConfig: IModalConfig = {
/* ... */
};
const editModalConfig: IModalConfig = {
/* ... */
};
</script>API(最常用)
只整理"页面开发最常用"的 API,完整能力以源码为准。
组件实例方法(defineExpose)
PageSearch
| 方法 | 说明 |
|---|---|
getQueryParams() | 获取当前查询表单数据 |
toggleVisible() | 显示/隐藏搜索区域 |
PageContent
| 方法 | 说明 |
|---|---|
fetchPageData(formData?: object, isRestart?: boolean) | 拉取列表数据(isRestart 为 true 时重置分页) |
exportPageData(formData?: object) | 导出(依赖 contentConfig.exportAction) |
getFilterParams() | 获取列筛选条件 |
getSelectionData() | 获取表格勾选数据 |
handleRefresh(isRestart?: boolean) | 刷新当前列表 |
PageModal
| 方法 | 说明 |
|---|---|
setFormData(data) | 设置表单数据(编辑/查看时常用) |
setModalVisible(visible?: boolean) | 打开/关闭弹窗 |
getFormData(key?: string) | 获取表单数据 |
setFormItemData(key, value) | 设置某一字段值 |
handleDisabled(disable: boolean) | 设置表单禁用(查看模式) |
核心配置类型(types.ts)
ISearchConfig
- 最常用:
formItems(表单项)、isExpandable/showNumber(展开收起) - 权限:
permPrefix(配置后会拼接按钮权限标识)
IContentConfig
- 必填:
indexAction(queryParams) - 常用:
cols(表格列定义)parseData(res)(把接口响应转换成{ total, list })deleteAction(ids)/exportAction(params)/importAction(file)(按需配 置)
IModalConfig
- 必填:
formItems - 常用:
component: "dialog" | "drawer"formAction(data)(提交时调用;不传则走@custom-submit)beforeSubmit(data)(提交前处理)
一、查询(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">
<DictTag 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:操作名格式 - 初始化时机:选项数据需要在页面挂载时异步加载
