Skip to content

CURD 快速开发

CURD 组件是一套配置化的快速开发方案,通过声明式配置快速搭建"搜索 + 表格 + 弹窗表单"的增删改查页面。

🎯 在线演示https://vue.youlai.tech/#/demo/curd

快速导航

章节说明
介绍组件概述与源码位置
快速开始5 分钟上手示例
查询PageSearch 搜索表单配置
列表PageContent 表格配置
新增PageModal 新增表单配置
编辑PageModal 编辑表单配置
删除删除功能配置
导入导出Excel 导入导出配置
高级功能权限控制、数据回显等

介绍

CURD 组件由三个核心组件和一个组合函数组成:

  • PageSearch - 搜索表单组件,提供条件筛选功能
  • PageContent - 数据列表组件,展示表格数据
  • PageModal - 表单弹窗组件,处理新增和编辑
  • usePage - 页面逻辑组合函数,统一管理页面状态和事件

什么时候用

  • 你希望用一套统一模式快速落地列表页(系统管理、基础数据管理等)。
  • 你的接口已经具备分页查询/新增/修改/删除/导入/导出能力(按需配置)。

源码位置

  • src/components/CURD/PageSearch.vue
  • src/components/CURD/PageContent.vue
  • src/components/CURD/PageModal.vue
  • src/components/CURD/types.ts
  • src/components/CURD/usePage.ts
  • 示例页面:
    • src/views/demo/curd/index.vue
    • src/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)拉取列表数据(isRestarttrue 时重置分页)
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表单项类型stringinput / 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,
  },
];

十三、注意事项

  1. 必须设置主键pk 字段用于删除和编辑功能
  2. 响应式配置:配置对象需要使用 reactive 包裹
  3. 接口格式:后端接口需要返回符合 parseData 定义的格式
  4. 权限控制:按钮权限基于 permPrefix:操作名 格式
  5. 初始化时机:选项数据需要在页面挂载时异步加载

相关链接

基于 MIT 许可发布