在企业管理后台开发中,CRUD 页面往往占据 80% 以上的开发量。搜索、表格、表单、详情------四个视图,四套配置,稍有改动就四处修改。本文介绍一个生产验证过的方案:用单一 Schema 同时驱动四个视图,将 CRUD 页面的开发效率提升一个数量级。
一、问题:CRUD 页面的四份配置
一个典型的管理后台页面长这样:
css
┌──────────────────────────────────────────────┐
│ 🔍 搜索区:用户名[___] 状态[___] [搜索] [重置] │
├──────────────────────────────────────────────┤
│ 📋 表格区:| 用户名 | 状态 | 创建时间 | 操作 │
│ | 张三 | 启用 | 01-01 | 编辑 删除│
├──────────────────────────────────────────────┤
│ ✏️ 表单弹窗:用户名[___] 状态[___] [确定] [取消] │
├──────────────────────────────────────────────┤
│ 📄 详情抽屉:用户名:张三 状态:启用 时间:... │
└──────────────────────────────────────────────┘
常规做法是分别维护四份配置:
ts
// ❌ 四份独立配置,字段信息重复四遍
const searchConfig = [{ field: 'username', label: '用户名', component: 'Input' }, ...]
const tableColumns = [{ field: 'username', label: '用户名', width: 120 }, ...]
const formSchema = [{ field: 'username', label: '用户名', component: 'Input', required: true }, ...]
const detailSchema = [{ field: 'username', label: '用户名' }, ...]
当字段名从 username 改成 userName,或者新增一个字段时,四个地方都要改。字段越多,维护成本指数级上升。
二、方案:单一 Schema,按需派生
useCrudSchemas 的核心思想很简单:
less
一份 CrudSchema[] → → → {
searchSchema // 搜索表单配置
tableColumns // 表格列配置
formSchema // 编辑表单配置
detailSchema // 详情描述配置
}
一份数据,四个视图。 字段的基本信息(field、label)只写一次,各视图的特化配置通过 search、table、form、detail 子对象覆盖。
Schema 定义
ts
export type CrudSchema = {
field: string // 字段名 --- 四个视图共用
label: string // 标签 --- 四个视图共用
dictType?: string // 字典类型 --- 自动注入选项 & 格式化器
dictClass?: 'string' | 'number' | 'boolean' // 字典值类型
isSearch?: boolean // 是否出现在搜索区
search?: CrudSearchParams // 搜索区特化配置
isTable?: boolean // 是否出现在表格
table?: CrudTableParams // 表格特化配置
isForm?: boolean // 是否出现在表单
form?: CrudFormParams // 表单特化配置
isDetail?: boolean // 是否出现在详情
detail?: CrudDescriptionsParams // 详情特化配置
children?: CrudSchema[] // 支持树形结构(多级表头)
}
每个视图的 show 属性控制可见性,其余配置覆盖默认值。没有 show: false 则默认显示。
使用示例
ts
const { allSchemas } = useCrudSchemas([
{
field: 'username',
label: '用户名',
search: { component: 'Input' },
table: { width: 120 },
form: { component: 'Input', required: true },
detail: { span: 12 },
},
{
field: 'status',
label: '状态',
dictType: 'common_status', // 🔥 字典类型 --- 自动处理一切
dictClass: 'number',
search: { component: 'Select' },
form: { component: 'Select', required: true },
},
{
field: 'createTime',
label: '创建时间',
isSearch: false, // 不在搜索区出现
form: { component: 'DatePicker', show: false }, // 表单中隐藏
detail: { dateFormat: 'YYYY-MM-DD HH:mm:ss' },
},
])
// allSchemas.searchSchema → 搜索表单配置(2个字段)
// allSchemas.tableColumns → 表格列配置(3列,status 自动带 DictTag 格式化)
// allSchemas.formSchema → 编辑表单配置(2个字段)
// allSchemas.detailSchema → 详情描述配置(3个字段)
三、核心设计要点
3.1 按视图过滤与合并
四个过滤函数的逻辑一致,以 filterSearchSchema 为例:
ts
const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
const searchSchema: FormSchema[] = []
eachTree(crudSchema, (schemaItem: CrudSchema) => {
// 1. 可见性判断:isSearch 或 search.show
if (schemaItem?.isSearch || schemaItem.search?.show) {
// 2. 默认组件:无指定时用 Input,有 dictType 时自动切 Select
let component = schemaItem?.search?.component || 'Input'
// 3. 字典自动注入选项
if (schemaItem.dictType) {
options.push({ label: '全部', value: '' }) // 搜索区加"全部"选项
getDictOptions(schemaItem.dictType).forEach(d => options.push(d))
if (!schemaItem.search?.component) component = 'Select'
}
// 4. 合并:默认值 → search 配置 → dict 派生配置
const searchSchemaItem = merge(
{ component, ...schemaItem.search, field: schemaItem.field, label: ... },
{ componentProps: comonentProps }
)
// 5. 异步 API 选项加载
if (searchSchemaItem.api) {
searchRequestTask.push(async () => {
const res = await searchSchemaItem.api()
// 找到已推入 schema 的对应项,更新其 options
allSchemas.searchSchema[index].componentProps.options = filterOptions(res)
})
}
delete searchSchemaItem.show
searchSchema.push(searchSchemaItem)
}
})
// 6. 批量执行异步任务
for (const task of searchRequestTask) { task() }
return searchSchema
}
设计亮点:
| 步骤 | 说明 |
|---|---|
eachTree 递归遍历 |
支持 children 嵌套,处理多级表头场景 |
| 可见性判断 | isSearch 为 false 即跳过,也可以 search.show: false 精确控制 |
| 字典自动注入 | 声明 dictType 后自动获取选项、自动切换组件类型 |
merge 合并策略 |
先放默认值,再放用户配置,保证用户配置优先级最高 |
| 延迟收集异步任务 | 先同步收集所有 schema item,再统一执行异步 API 调用,避免阻塞主流程 |
3.2 表格的字典自动格式化
表格视图的处理有一个巧妙的自动化:
ts
const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
const tableColumns = treeMap(crudSchema, {
conversion: (schema: CrudSchema) => {
if (schema?.isTable !== false && schema?.table?.show !== false) {
// 🔥 自动注入 DictTag 格式化器
if (!schema.formatter && schema.dictType) {
schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => {
return h(DictTag, { type: schema.dictType!, value: cellValue })
}
}
return { ...schema.table, ...schema }
}
}
})
// 二次过滤:去掉 treeMap 产生的 undefined 节点
return filter(tableColumns, (data) => !!data.field)
}
声明 dictType: 'common_status' 后,表格列自动使用 DictTag 组件渲染,将 0/1 显示为带颜色的"启用"/"停用"标签------零额外配置。
treeMap + filter 的两阶段处理也很巧妙:
treeMap递归转换树结构,不满足条件的节点返回undefinedfilter二次过滤去掉undefined,并清理空children
3.3 表单的字典类型区分
表单视图对字典的处理更精细,按 dictClass 区分三种类型:
ts
if (schemaItem.dictType) {
if (schemaItem.dictClass === 'number') {
getIntDictOptions(schemaItem.dictType) // value 转 int
} else if (schemaItem.dictClass === 'boolean') {
getBoolDictOptions(schemaItem.dictType) // value 转 boolean
} else {
getDictOptions(schemaItem.dictType) // value 保持 string
}
}
这样保证了表单提交时,status 字段的值类型与后端接口完全一致------string、number、boolean 严格区分避免类型转换 bug。
3.4 详情视图的日期格式化
ts
const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => {
eachTree(crudSchema, (schemaItem: CrudSchema) => {
if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) {
// 日期格式化优先级:detail.dateFormat > 自动推导
if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') {
descriptionsSchemaItem.dateFormat =
schemaItem?.detail?.dateFormat || 'YYYY-MM-DD HH:mm:ss'
}
// dictType 透传,由详情组件自行处理字典渲染
if (schemaItem.dictType) {
descriptionsSchemaItem.dictType = schemaItem.dictType
}
}
})
}
细节处理到位:用户可以通过 detail.dateFormat 自定义格式,也可以通过 formatter: 'formatDate' 标记自动获得默认格式。
四、架构图
css
CrudSchema[]
│
┌──────────────┼──────────────┐
│ │ │
eachTree 遍历 treeMap 递归 eachTree 遍历
│ │ │
┌─────────▼─────────┐ │ ┌──────────▼──────────┐
│ filterSearchSchema │ │ │ filterFormSchema │
│ │ │ │ │
│ • 可见性判断 │ │ │ • 可见性判断 │
│ • dictType → 选项 │ │ │ • dictClass 类型区分 │
│ • api 异步加载 │ │ │ • api 异步加载 │
│ • merge 合并配置 │ │ │ • 默认值设置 │
└────────┬───────────┘ │ └──────────┬───────────┘
│ │ │
▼ ▼ ▼
searchSchema tableColumns formSchema detailSchema
(FormSchema[]) (TableColumn[]) (FormSchema[]) (DescriptionsSchema[])
五、进阶技巧
5.1 异步选项加载
当选项数据来自后端接口而非字典时:
ts
{
field: 'deptId',
label: '所属部门',
isSearch: true,
search: {
component: 'Select',
api: () => getDeptList(), // 异步加载部门列表
},
}
api 返回的数组会被自动注入到对应字段的 componentProps.options,并经过 i18n 翻译。
5.2 多级表头
CrudSchema 支持 children,利用 treeMap 递归转换,天然支持多级表头:
ts
{
label: '基本信息',
children: [
{ field: 'username', label: '用户名' },
{ field: 'nickname', label: '昵称' },
]
}
5.3 列排序辅助
额外导出的 sortTableColumns 工具函数,用于将指定列移到表格最前面:
ts
sortTableColumns(allSchemas.tableColumns, 'selection')
配合 ag-grid 或 Element Plus Table 使用,灵活控制列顺序。
六、适用场景与局限
✅ 适合
- 标准 CRUD 页面:字段在搜索/表格/表单/详情四个视图都有出现
- 字典驱动型字段多:状态、类型、分类等枚举字段,一次声明自动处理
- 快速原型开发:从一份 Schema 直接生成完整功能页面
⚠️ 不适合
- 四个视图字段差异极大:若大部分字段只在某一个视图出现,单一 Schema 的优势不明显
- 高度定制化页面:如复杂的自定义搜索面板、非标表格渲染,Schema 声明式配置力不从心
七、总结
useCrudSchemas 的核心价值可以归结为三点:
- 消除重复 :
field、label、dictType写一次,四个视图自动消费 - 智能推导 :有
dictType自动选组件、自动注选项、自动格式化;有dateFormat自动处理日期显示 - 渐进增强 :默认行为覆盖 80% 场景,剩余 20% 通过
search/table/form/detail子配置精确覆盖
在一个有 50+ CRUD 页面的企业管理后台中,这个 Hook 将单个页面的配置代码从 ~200 行减少到 ~60 行,字段级别的改动从四处修改变成一处修改。它不是一个技术炫技的产物,而是在重复劳动中生长出来的自动化方案------这正是业务开发中最值得沉淀的东西。