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 |
| useCrudSchemas | CRUD 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 | 每页显示条数 | number | 10 |
| currentPage | 当前页码 | number | 1 |
| selection | 是否多选 | boolean | false |
| showOverflowTooltip | 是否显示超出提示 | boolean | true |
| columns | 表头配置 | TableColumn[] | [] |
| expand | 是否展开行 | boolean | false |
| pagination | 分页配置 | Pagination | undefined | undefined |
| reserveSelection | 是否保留选中状态 | boolean | false |
| loading | 加载状态 | boolean | false |
| reserveIndex | 是否叠加索引 | boolean | false |
| 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 | 是否使用栅格布局 | boolean | false |
| model | 表单数据对象 | Recordable | {} |
| autoSetPlaceholder | 是否自动设置placeholder | boolean | true |
| isCustom | 是否自定义内容 | boolean | false |
| labelWidth | 表单label宽度 | string | number | 'auto' |
| vLoading | 加载状态 | boolean | false |
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 | 是否使用栅格布局 | boolean | false |
| labelWidth | 表单label宽度 | string | number | 'auto' |
| layout | 操作按钮风格位置 | 'inline' | 'bottom' | 'inline' |
| buttomPosition | 底部按钮的对齐方式 | 'left' | 'center' | 'right' | 'center' |
| showSearch | 是否显示搜索按钮 | boolean | true |
| showReset | 是否显示重置按钮 | boolean | true |
| expand | 是否显示伸缩 | boolean | false |
| expandField | 伸缩的界限字段 | string | '' |
| inline | 是否行内表单 | boolean | true |
| 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 | 添加Schema | formSchema: FormSchema, index?: number |
| delSchema | 删除Schema | field: 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'
}
]注意:本文档持续更新中,如有问题请及时反馈。
