管理页面的增删改查复用,核心是把”列表加载、分页、搜索、新增、编辑、删除、表单校验”这套通用流程从业务代码里抽出来,封装成组合式函数与通用组件,业务页只用声明式配置就能组装出一个完整的管理模块。我们在企业级 AI 网关项目中落地了 useCrud 组合式函数 + 通用表格/表单/弹窗组件的方案,模型管理、应用管理、渠道配置三个模块共用同一套骨架,每个新模块从零开发到可用控制在半天以内,重复代码减少七成以上。下面给出可直接复用的封装思路。
一、先拆解哪些东西值得复用
1.1 增删改查的共性到底在哪
任何管理页都逃不开同一套流程:进页面拉列表、分页跳转、按条件搜索、打开弹窗新增、回填数据编辑、二次确认删除。业务之间的差异只在字段、接口和校验规则,流程骨架完全一致。把骨架抽出来,业务就只剩”配置”。
| 能力 | 通用实现 | 业务差异点 |
|---|---|---|
| 列表加载 | useCrud 统一拉取与分页状态 | 列表接口路径与字段映射 |
| 搜索 | 搜索表单组件统一处理 | 搜索项声明配置 |
| 新增/编辑 | 弹窗 + 表单组件统一管理 | 表单字段与校验规则 |
| 删除 | 统一二次确认与回调 | 删除接口与提示字段 |
| 刷新/重置 | 统一暴露给页面 | 基本无差异 |
1.2 组件与 Hook 各管一层
组件管展示,Hook 管状态。表格、表单、弹窗这些 UI 用通用组件承接;数据状态、加载状态、请求方法用 Hook 承接。两者解耦后,同样的 Hook 可以配不同 UI 库,同一个组件也可以对接不同业务字段。
二、useCrud 组合式函数的设计
2.1 核心 API 契约
useCrud 接收一个配置对象,把接口封装、分页字段、主键字段、表单字段一次性声明清楚,返回列表数据与全套操作方法。
// composables/useCrud.ts
export function useCrud(options: CrudOptions) {
const {
api, // { list, add, update, remove, detail }
idField = 'id',
rowsField = 'list',
totalField = 'total',
queryParams = {},
formModel = {},
} = options
const list = ref<Record<string, any>[]>([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
async function load() {
loading.value = true
try {
const res = await api.list({ ...queryParams, page, size })
list.value = res[rowsField] ?? []
total.value = res[totalField] ?? 0
} finally {
loading.value = false
}
}
function openAdd() {
editingId.value = null
Object.keys(formModel).forEach((k) => (formModel[k] = ''))
dialogVisible.value = true
}
function openEdit(row: Record<string, any>) {
editingId.value = row[idField]
Object.assign(formModel, row)
dialogVisible.value = true
}
async function submit() {
if (editingId.value) {
await api.update(editingId.value, formModel)
} else {
await api.add(formModel)
}
dialogVisible.value = false
load()
}
async function remove(row: Record<string, any>) {
await api.remove(row[idField])
load()
}
return { list, total, loading, dialogVisible, editingId, load, openAdd, openEdit, submit, remove }
}
2.2 通用表格组件的列定制
表格组件负责渲染列、分页和操作按钮,但列的展示逻辑各不相同。用”具名插槽自动接管”模式:业务页给某列提供了插槽就用插槽,没提供就走默认渲染,通用性与灵活性同时保住。
<!-- components/CrudTable.vue 关键片段 -->
<template>
<el-table :data="rows" v-loading="loading">
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
>
<template v-if="col.slot" #default="{ row }">
<slot :name="col.slot" :row="row" />
</template>
</el-table-column>
<el-table-column v-if="operations" label="操作" fixed="right">
<template #default="{ row }">
<slot name="actions" :row="row" />
</template>
</el-table-column>
</el-table>
</template>
三、业务模块的组装方式
3.1 一个模块一页代码
模型管理页只需要声明列、接口和表单字段,其余交给通用层:
- 声明
apiService,指向后端标准化的/model/page、/model、/model/{id}接口; - 声明
columns列配置,特殊列(如状态标签、模型类型)指定 slot 名称; - 声明
formFields,配置表单项类型、校验规则; - 调用
useCrud拿到数据与操作方法,模板里直接绑定。
3.2 表单的动态渲染
表单组件按 formFields 配置动态渲染表单项,字段类型支持输入框、下拉、开关、数字等,校验规则跟着字段走。新增一个模块不需要重写表单组件,只加字段配置。
3.3 事件回调与跨模块联动
业务模块在通用流程上还要挂自定义行为,比如新增模型后自动刷新渠道列表、删除应用后清掉关联缓存。useCrud 提供一个事件订阅位,业务页按模块名注册回调,通用层在增删改成功后统一派发,避免业务代码散落在各个组件里。
// 应用模块里订阅渠道模块的事件
crud.on('app:deleted', (id) => {
channelStore.refresh(id)
})
权限按钮也走配置化:新增、编辑、删除三个操作绑定权限标识,组件渲染前统一校验,没有权限的按钮直接不渲染,权限规则收敛在一个配置表里维护。
3.4 接口层约定统一
后端按统一模式暴露接口:分页查询 GET /{module}/page、新增 POST /{module}、修改 PUT /{module}/{id}、删除 DELETE /{module}/{id}、详情 GET /{module}/{id}。接口统一之后,前端 apiService 用工厂函数批量生成,一个模块的请求封装不超过 20 行。
四、复用过程中的三个取舍
复杂业务的列不硬塞进通用组件。订单这类状态流转型模块,操作按钮语义与普通删除完全不同,直接用插槽接管,通用组件退化成纯布局容器,避免为了复用强行抽象。搜索条件的联动依赖建议在业务页里维护,不要在通用组件里做全局联动逻辑,否则通用层会越变越重。删除的二次确认文案按模块定制,用配置项传入而不是写死在组件里。
常见问题(FAQ)
Q1:通用 CRUD 组件会不会限制复杂业务?
不会。通用层只处理标准流程,复杂场景通过插槽接管,组件退化为布局容器。
Q2:新增和编辑怎么复用同一个弹窗?
同一弹窗组件,编辑时先回填数据再提交,提交前按是否有主键分流。
Q3:不同模块的分页字段不一致怎么办?
通过 useCrud 配置的 pageField、sizeField、totalField 字段名映射解决。