Skip to content

CRUD组件开发文档

1. 概述

梵医云前端项目提供了一套完整的CRUD(增删改查)组件系统,包括表格、表单、搜索等组件,以及相应的Hooks封装,帮助开发者快速构建标准的CRUD页面。

1.1 CRUD组件特点

  • 开箱即用:提供完整的CRUD功能,无需重复开发
  • 高度可配置:通过Schema配置实现灵活的功能定制
  • 类型安全:完整的TypeScript类型定义
  • 统一规范:统一的开发规范和代码风格
  • 易于扩展:支持自定义组件和插槽

1.2 核心组件

组件说明路径
Table表格组件@/components/Table
Form表单组件@/components/Form
Search搜索组件@/components/Search
ContentWrap内容包裹组件@/components/ContentWrap

1.3 核心Hooks

Hook说明路径
useTable表格逻辑封装@/hooks/web/useTable
useForm表单逻辑封装@/hooks/web/useForm
useCrudSchemasCRUD Schema处理@/hooks/web/useCrudSchemas

2. Table组件

2.1 基础用法

vue
<template>
  <ContentWrap>
    <Table
      :columns="tableColumns"
      :data="tableList"
      :loading="loading"
      :pagination="{
        total: total
      }"
      v-model:pageSize="pageSize"
      v-model:currentPage="currentPage"
    >
      <template #action="{ row }">
        <el-button link type="primary" @click="handleEdit(row)">
          编辑
        </el-button>
        <el-button link type="danger" @click="handleDelete(row.id)">
          删除
        </el-button>
      </template>
    </Table>
  </ContentWrap>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { TableColumn } from '@/types/table'

const tableColumns: TableColumn[] = [
  { field: 'id', label: '编号', width: 80 },
  { field: 'name', label: '名称' },
  { field: 'status', label: '状态' },
  { field: 'createTime', label: '创建时间' }
]

const tableList = ref([])
const loading = ref(false)
const total = ref(0)
const pageSize = ref(10)
const currentPage = ref(1)

const handleEdit = (row: any) => {
  console.log('编辑', row)
}

const handleDelete = (id: number) => {
  console.log('删除', id)
}
</script>

2.2 Table组件Props

属性说明类型默认值
pageSize每页显示条数number10
currentPage当前页码number1
selection是否多选booleanfalse
showOverflowTooltip是否显示超出提示booleantrue
columns表头配置TableColumn[][]
expand是否展开行booleanfalse
pagination分页配置Pagination | undefinedundefined
reserveSelection是否保留选中状态booleanfalse
loading加载状态booleanfalse
reserveIndex是否叠加索引booleanfalse
align对齐方式'left' | 'center' | 'right''center'
headerAlign表头对齐方式'left' | 'center' | 'right''center'
data表格数据Recordable[][]

2.3 TableColumn配置

typescript
interface TableColumn {
  field: string          // 字段名
  label?: string         // 列标题
  width?: number | string // 列宽
  fixed?: 'left' | 'right' // 固定列
  children?: TableColumn[] // 子列
  type?: 'index'       // 序号列
  formatter?: Function  // 格式化函数
}

2.4 自定义列渲染

使用插槽自定义列内容:

vue
<template>
  <Table :columns="columns" :data="tableList">
    <template #name="{ row }">
      <el-tag>{{ row.name }}</el-tag>
    </template>
    <template #status="{ row }">
      <el-tag :type="row.status === 1 ? 'success' : 'danger'">
        {{ row.status === 1 ? '启用' : '禁用' }}
      </el-tag>
    </template>
  </Table>
</template>

<script setup lang="ts">
const columns = [
  { field: 'id', label: '编号' },
  { field: 'name', label: '名称' },
  { field: 'status', label: '状态' }
]
</script>

2.5 使用formatter格式化

typescript
const columns = [
  {
    field: 'status',
    label: '状态',
    formatter: (row: any, column: any, cellValue: any, index: number) => {
      return cellValue === 1 ? '启用' : '禁用'
    }
  },
  {
    field: 'createTime',
    label: '创建时间',
    formatter: (row: any, column: any, cellValue: any, index: number) => {
      return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss')
    }
  }
]

2.6 多选表格

vue
<template>
  <Table
    :columns="columns"
    :data="tableList"
    selection
    :reserve-selection="true"
    row-key="id"
  >
    <template #action="{ row }">
      <el-button link type="primary" @click="handleDelete(row.id)">
        删除
      </el-button>
    </template>
  </Table>
</template>

2.7 展开行

vue
<template>
  <Table
    :columns="columns"
    :data="tableList"
    expand
  >
    <template #expand="{ row }">
      <div>展开内容:{{ row.description }}</div>
    </template>
  </Table>
</template>

3. Form组件

3.1 基础用法

vue
<template>
  <Form
    :schema="formSchema"
    :model="formData"
    :is-col="true"
    label-width="120px"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { FormSchema } from '@/types/form'

const formData = ref({})

const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    componentProps: {
      placeholder: '请输入名称'
    },
    formItemProps: {
      rules: [{ required: true, message: '请输入名称', trigger: 'blur' }]
    }
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select',
    componentProps: {
      options: [
        { label: '启用', value: 1 },
        { label: '禁用', value: 0 }
      ]
    }
  }
]
</script>

3.2 Form组件Props

属性说明类型默认值
schema表单配置FormSchema[][]
isCol是否使用栅格布局booleanfalse
model表单数据对象Recordable{}
autoSetPlaceholder是否自动设置placeholderbooleantrue
isCustom是否自定义内容booleanfalse
labelWidth表单label宽度string | number'auto'
vLoading加载状态booleanfalse

3.3 FormSchema配置

typescript
interface FormSchema {
  field: string                    // 字段名
  label?: string                   // 标题
  labelMessage?: string            // 提示信息
  colProps?: ColProps             // col组件属性
  componentProps?: {              // 表单组件属性
    slots?: Recordable
  } & ComponentProps
  formItemProps?: FormItemProps   // formItem组件属性
  component?: ComponentName        // 渲染的组件
  value?: FormValueType           // 初始值
  hidden?: boolean               // 是否隐藏
  api?: <T = any>() => AxiosPromise<T> // 远程加载下拉项
}

3.4 支持的组件

组件名说明示例
Input输入框{ component: 'Input' }
InputNumber数字输入框{ component: 'InputNumber' }
Select下拉选择{ component: 'Select' }
SelectV2远程搜索下拉{ component: 'SelectV2' }
Radio单选框{ component: 'Radio' }
RadioButton单选按钮组{ component: 'RadioButton' }
Checkbox复选框{ component: 'Checkbox' }
CheckboxButton复选按钮组{ component: 'CheckboxButton' }
DatePicker日期选择器{ component: 'DatePicker' }
TimePicker时间选择器{ component: 'TimePicker' }
Switch开关{ component: 'Switch' }
Slider滑块{ component: 'Slider' }
Upload上传{ component: 'Upload' }
Editor富文本编辑器{ component: 'Editor' }
Divider分割线{ component: 'Divider' }

3.5 表单验证

typescript
const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    formItemProps: {
      rules: [
        { required: true, message: '请输入名称', trigger: 'blur' },
        { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
      ]
    }
  },
  {
    field: 'email',
    label: '邮箱',
    component: 'Input',
    formItemProps: {
      rules: [
        { required: true, message: '请输入邮箱', trigger: 'blur' },
        { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
      ]
    }
  }
]

3.6 自定义插槽

vue
<template>
  <Form :schema="formSchema" :model="formData">
    <template #name="{ model }">
      <el-input v-model="model.name" placeholder="自定义输入框" />
    </template>
    <template #name-label>
      <span>自定义标签</span>
    </template>
  </Form>
</template>

3.7 栅格布局

typescript
const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    colProps: {
      span: 12
    }
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select',
    colProps: {
      span: 12
    }
  }
]

4. Search组件

4.1 基础用法

vue
<template>
  <ContentWrap>
    <Search
      :schema="searchSchema"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #actionMore>
        <el-button type="primary" plain @click="handleAdd">
          新增
        </el-button>
      </template>
    </Search>
  </ContentWrap>
</template>

<script setup lang="ts">
import type { FormSchema } from '@/types/form'

const searchSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    componentProps: {
      placeholder: '请输入名称'
    }
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select',
    componentProps: {
      options: [
        { label: '全部', value: '' },
        { label: '启用', value: 1 },
        { label: '禁用', value: 0 }
      ]
    }
  }
]

const handleSearch = (params: any) => {
  console.log('搜索参数', params)
}

const handleReset = (params: any) => {
  console.log('重置参数', params)
}

const handleAdd = () => {
  console.log('新增')
}
</script>

4.2 Search组件Props

属性说明类型默认值
schema搜索表单配置FormSchema[][]
isCol是否使用栅格布局booleanfalse
labelWidth表单label宽度string | number'auto'
layout操作按钮风格位置'inline' | 'bottom''inline'
buttomPosition底部按钮的对齐方式'left' | 'center' | 'right''center'
showSearch是否显示搜索按钮booleantrue
showReset是否显示重置按钮booleantrue
expand是否显示伸缩booleanfalse
expandField伸缩的界限字段string''
inline是否行内表单booleantrue
model表单数据对象Recordable{}

4.3 伸缩搜索

vue
<template>
  <Search
    :schema="searchSchema"
    expand
    expand-field="createTime"
    @search="handleSearch"
    @reset="handleReset"
  />
</template>

<script setup lang="ts">
const searchSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input'
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select'
  },
  {
    field: 'createTime',
    label: '创建时间',
    component: 'DatePicker'
  }
]
</script>

5. useTable Hook

5.1 基础用法

typescript
import { useTable } from '@/hooks/web/useTable'
import * as UserApi from '@/api/system/user'

const { tableObject, tableMethods } = useTable({
  getListApi: UserApi.getUserPage,
  delListApi: UserApi.deleteUser
})

const { getList, setSearchParams } = tableMethods

// 获取列表数据
getList()

// 搜索
setSearchParams({ name: '张三' })

5.2 useTable参数

typescript
interface UseTableConfig<T = any> {
  getListApi: (option: any) => Promise<T>    // 分页接口
  delListApi?: (option: any) => Promise<T>   // 删除接口
  exportListApi?: (option: any) => Promise<T> // 导出接口
  response?: ResponseType                      // 返回数据格式配置
  defaultParams?: Recordable                    // 默认传递的参数
  props?: TableProps                           // Table组件属性
}

5.3 tableObject

typescript
interface TableObject<T = any> {
  pageSize: number          // 页数
  currentPage: number      // 当前页
  total: number           // 总条数
  tableList: T[]         // 表格数据
  params: any            // AxiosConfig 配置
  loading: boolean       // 加载中
  exportLoading: boolean // 导出加载中
  currentRow: Nullable<T> // 当前行的数据
}

5.4 tableMethods

方法说明参数
getList获取列表数据-
setProps设置Table组件属性props: TableProps
setColumn设置列属性columnProps: TableSetPropsType[]
getSelections获取选中的数据-
setSearchParams设置搜索参数data: Recordable
delList删除数据ids: string | number | string[] | number[], multiple: boolean, message?: boolean
exportList导出列表fileName: string

5.5 完整示例

vue
<template>
  <ContentWrap>
    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
      <template #actionMore>
        <el-button type="primary" plain @click="openForm('create')">
          新增
        </el-button>
      </template>
    </Search>
  </ContentWrap>

  <ContentWrap>
    <Table
      :columns="allSchemas.tableColumns"
      :data="tableObject.tableList"
      :loading="tableObject.loading"
      :pagination="{ total: tableObject.total }"
      v-model:pageSize="tableObject.pageSize"
      v-model:currentPage="tableObject.currentPage"
    >
      <template #action="{ row }">
        <el-button link type="primary" @click="openForm('update', row.id)">
          编辑
        </el-button>
        <el-button link type="danger" @click="handleDelete(row.id)">
          删除
        </el-button>
      </template>
    </Table>
  </ContentWrap>
</template>

<script setup lang="ts">
import { allSchemas } from './account.data'
import * as MailAccountApi from '@/api/system/mail/account'

const { tableObject, tableMethods } = useTable({
  getListApi: MailAccountApi.getMailAccountPage,
  delListApi: MailAccountApi.deleteMailAccount
})

const { getList, setSearchParams } = tableMethods

const openForm = (type: string, id?: number) => {
  console.log(type, id)
}

const handleDelete = (id: number) => {
  tableMethods.delList(id, false)
}

getList()
</script>

6. useForm Hook

6.1 基础用法

typescript
import { useForm } from '@/hooks/web/useForm'

const { register, elFormRef, methods } = useForm({
  schema: formSchema,
  model: formData
})

const { setValues, getFormData, setSchema } = methods

// 设置表单值
setValues({ name: '张三' })

// 获取表单值
const formData = await getFormData()

// 设置Schema
setSchema([{ field: 'name', path: 'formItemProps', value: { required: false } }])

6.2 useForm参数

typescript
interface FormProps {
  schema?: FormSchema[]
  isCol?: boolean
  model?: Recordable
  autoSetPlaceholder?: boolean
  isCustom?: boolean
  labelWidth?: string | number
}

6.3 methods

方法说明参数
setProps设置Form组件属性props: FormProps
setValues设置表单值data: Recordable
getFormData获取表单值-
setSchema设置Schema属性schemaProps: FormSetPropsType[]
addSchema添加SchemaformSchema: FormSchema, index?: number
delSchema删除Schemafield: string

6.4 完整示例

vue
<template>
  <el-dialog v-model="dialogVisible" title="编辑" width="600px">
    <Form
      :schema="formSchema"
      :model="formData"
      label-width="120px"
      @register="register"
    />
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit">确定</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@/hooks/web/useForm'
import type { FormSchema } from '@/types/form'

const dialogVisible = ref(false)
const formData = ref({})

const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    formItemProps: {
      rules: [{ required: true, message: '请输入名称', trigger: 'blur' }]
    }
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select',
    componentProps: {
      options: [
        { label: '启用', value: 1 },
        { label: '禁用', value: 0 }
      ]
    }
  }
]

const { register, elFormRef, methods } = useForm({
  schema: formSchema,
  model: formData
})

const handleSubmit = async () => {
  const valid = await elFormRef.value?.validate()
  if (valid) {
    const { getFormData } = methods
    const data = await getFormData()
    console.log('提交数据', data)
    dialogVisible.value = false
  }
}
</script>

7. useCrudSchemas Hook

7.1 基础用法

typescript
import { useCrudSchemas } from '@/hooks/web/useCrudSchemas'

const crudSchema = [
  {
    field: 'name',
    label: '名称',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Input'
  },
  {
    field: 'status',
    label: '状态',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Select',
    dictType: 'common_status'
  }
]

const { allSchemas } = useCrudSchemas(crudSchema)

7.2 CrudSchema配置

typescript
interface CrudSchema {
  field: string                    // 字段名
  label?: string                   // 标题
  labelMessage?: string            // 提示信息
  colProps?: ColProps             // col组件属性
  componentProps?: {              // 表单组件属性
    slots?: Recordable
  } & ComponentProps
  formItemProps?: FormItemProps   // formItem组件属性
  component?: ComponentName        // 渲染的组件
  value?: FormValueType           // 初始值
  hidden?: boolean               // 是否隐藏
  api?: <T = any>() => AxiosPromise<T> // 远程加载下拉项
  
  // CRUD特有配置
  isSearch?: boolean             // 是否在查询显示
  search?: CrudSearchParams     // 查询的详细配置
  isTable?: boolean             // 是否在列表显示
  table?: CrudTableParams       // 列表的详细配置
  isForm?: boolean             // 是否在表单显示
  form?: CrudFormParams        // 表单的详细配置
  isDetail?: boolean           // 是否在详情显示
  detail?: CrudDescriptionsParams // 详情的详细配置
  children?: CrudSchema[]     // 子字段
  dictType?: string          // 字典类型
  dictClass?: 'string' | 'number' | 'boolean' // 字典数据类型
}

7.3 allSchemas

typescript
interface AllSchemas {
  searchSchema: FormSchema[]   // 搜索Schema
  tableColumns: TableColumn[]  // 表格列配置
  formSchema: FormSchema[]     // 表单Schema
  detailSchema: DescriptionsSchema[] // 详情Schema
}

7.4 完整示例

typescript
import { useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { getIntDictOptions } from '@/utils/dict'

const crudSchema = [
  {
    field: 'name',
    label: '名称',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Input',
    formItemProps: {
      rules: [{ required: true, message: '请输入名称', trigger: 'blur' }]
    }
  },
  {
    field: 'status',
    label: '状态',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Select',
    dictType: 'common_status',
    dictClass: 'number',
    table: {
      width: 100
    }
  },
  {
    field: 'createTime',
    label: '创建时间',
    isTable: true,
    detail: {
      dateFormat: 'YYYY-MM-DD HH:mm:ss'
    }
  }
]

const { allSchemas } = useCrudSchemas(crudSchema)

7.5 字典类型支持

typescript
const crudSchema = [
  {
    field: 'status',
    label: '状态',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Select',
    dictType: 'common_status',
    dictClass: 'number' // 'string' | 'number' | 'boolean'
  }
]

7.6 远程API支持

typescript
const crudSchema = [
  {
    field: 'userId',
    label: '用户',
    isForm: true,
    component: 'SelectV2',
    api: async () => {
      const res = await UserApi.getUserList()
      return res
    },
    componentProps: {
      optionsAlias: {
        labelField: 'nickname',
        valueField: 'id'
      }
    }
  }
]

8. 完整CRUD页面示例

8.1 页面结构

vue
<template>
  <!-- 搜索工作栏 -->
  <ContentWrap>
    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
      <template #actionMore>
        <el-button type="primary" plain @click="openForm('create')">
          新增
        </el-button>
      </template>
    </Search>
  </ContentWrap>

  <!-- 列表 -->
  <ContentWrap>
    <Table
      :columns="allSchemas.tableColumns"
      :data="tableObject.tableList"
      :loading="tableObject.loading"
      :pagination="{ total: tableObject.total }"
      v-model:pageSize="tableObject.pageSize"
      v-model:currentPage="tableObject.currentPage"
    >
      <template #action="{ row }">
        <el-button link type="primary" @click="openForm('update', row.id)">
          编辑
        </el-button>
        <el-button link type="primary" @click="openDetail(row.id)">
          详情
        </el-button>
        <el-button link type="danger" @click="handleDelete(row.id)">
          删除
        </el-button>
      </template>
    </Table>
  </ContentWrap>

  <!-- 表单弹窗 -->
  <UserForm ref="formRef" @success="getList" />
  <!-- 详情弹窗 -->
  <UserDetail ref="detailRef" />
</template>

<script setup lang="ts">
import { allSchemas } from './user.data'
import * as UserApi from '@/api/system/user'
import UserForm from './UserForm.vue'
import UserDetail from './UserDetail.vue'

defineOptions({ name: 'SystemUser' })

const { tableObject, tableMethods } = useTable({
  getListApi: UserApi.getUserPage,
  delListApi: UserApi.deleteUser
})

const { getList, setSearchParams } = tableMethods

const formRef = ref()
const detailRef = ref()

const openForm = (type: string, id?: number) => {
  formRef.value.open(type, id)
}

const openDetail = (id: number) => {
  detailRef.value.open(id)
}

const handleDelete = (id: number) => {
  tableMethods.delList(id, false)
}

getList()
</script>

8.2 Schema配置文件

typescript
import { CrudSchema } from '@/hooks/web/useCrudSchemas'

export const crudSchema: CrudSchema[] = [
  {
    field: 'username',
    label: '用户账号',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Input',
    formItemProps: {
      rules: [{ required: true, message: '请输入用户账号', trigger: 'blur' }]
    }
  },
  {
    field: 'nickname',
    label: '用户昵称',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Input'
  },
  {
    field: 'status',
    label: '状态',
    isSearch: true,
    isTable: true,
    isForm: true,
    component: 'Select',
    dictType: 'common_status',
    dictClass: 'number'
  },
  {
    field: 'createTime',
    label: '创建时间',
    isTable: true,
    detail: {
      dateFormat: 'YYYY-MM-DD HH:mm:ss'
    }
  }
]

export const { allSchemas } = useCrudSchemas(crudSchema)

9. 最佳实践

9.1 Schema配置规范

  • 统一使用 crudSchema 配置所有字段
  • 使用 useCrudSchemas 统一生成各场景Schema
  • 字段命名遵循驼峰命名规范

9.2 组件选择

场景推荐组件
简单文本输入Input
数字输入InputNumber
单选Radio / RadioButton
多选Checkbox / CheckboxButton
下拉选择Select
远程搜索SelectV2
日期选择DatePicker
富文本Editor
文件上传Upload

9.3 性能优化

  • 使用 reserveSelection 保留选中状态
  • 合理设置 pageSize 避免一次性加载过多数据
  • 使用 formatter 格式化数据而非插槽

9.4 用户体验

  • 搜索条件合理分组,使用 expand 控制显示
  • 表格列宽合理设置,避免横向滚动
  • 操作按钮使用 link 类型,避免视觉干扰

10. 常见问题

10.1 表格数据不更新

问题原因

  • 没有正确调用 getList 方法
  • 分页参数没有正确更新

解决方案

typescript
// 正确的做法
const { tableMethods } = useTable({ ... })
const { getList } = tableMethods

// 搜索后重新获取数据
setSearchParams({ name: '张三' })

10.2 表单验证不生效

问题原因

  • formItemProps.rules 配置不正确
  • 没有正确调用验证方法

解决方案

typescript
const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    component: 'Input',
    formItemProps: {
      rules: [
        { required: true, message: '请输入名称', trigger: 'blur' }
      ]
    }
  }
]

// 提交时验证
const valid = await elFormRef.value?.validate()
if (valid) {
  // 提交数据
}

10.3 字典数据不显示

问题原因

  • dictType 配置不正确
  • 字典数据未加载

解决方案

typescript
const crudSchema = [
  {
    field: 'status',
    label: '状态',
    dictType: 'common_status', // 确保字典类型正确
    dictClass: 'number', // 根据实际数据类型设置
    component: 'Select'
  }
]

注意:本文档持续更新中,如有问题请及时反馈。