Skip to content

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表单项类型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">
    <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
  }
]

十三、注意事项

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

相关链接

基于 MIT 许可发布