UseCrud - 增删改查
组件介绍
UseCrud 是基于 UseElForm 和 UseElTable 二次封装的 CRUD 一体化业务组件,整合查询表单、操作栏、数据表格、新增 / 编辑弹窗四大核心模块,统一配置入口 configs,内置请求管理、字典加载、批量 / 单行删除、自动取消请求、列过滤排序、弹窗切换等通用能力,标准化扩展配置体系,大幅减少后台增删改查页面重复模板代码,开箱即用且高度可定制插槽。
核心能力
- 统一字段配置入口 configs,一套配置驱动表单 + 表格
仅需传入一份 configs 数组,组件内部自动拆分生成查询表单列、表格列;支持单独配置查询标签queryLabel、查询字段queryProp、仅搜索字段onlySearch、表格排序权重tableOrder;内置工具自动过滤有效表单项、自动按权重排序表格列,重复排序值自动打印警告,兼容逗号分割多字段拆分场景。
- 完整请求生命周期管控,自动防竞态、可中止请求
内置 AbortController 管理列表请求,每次查询自动中止上一次未完成请求,避免新旧接口数据覆盖;提供 autoCancelQueryStrategy 策略配置,支持页面卸载 / 路由失活 / 两者均触发时自动取消请求;提供查询前置钩子onQueryBefore可拦截请求,内置查询失败回调onQueryError,分页接口自动拼接页码、页大小参数,非分页接口直接返回完整数组。
- 内置单行 / 批量删除能力,统一 loading 状态管理
对外暴露 handleDelete 方法,同时支持单条 id、批量 ids 两种传参模式;自动清洗空请求参数,支持自定义成功回调、失败回调、成功提示文案;可绑定行 loading 或批量按钮 loading,删除完成默认自动刷新列表,缺失deleteApi时控制台给出明确错误提示。
- 双弹窗模式无缝切换(Drawer 抽屉 / Dialog 对话框)
通过 extConfig.modal.type 控制弹窗类型,默认抽屉;提供handleShowModal打开弹窗、handleCloseModal关闭弹窗;完整转发弹窗头部 / 底部 / 内容插槽,支持自定义弹窗标题、底部按钮;弹窗渲染基于nextTick保证 DOM 实例就绪,使用shallowRef缓存弹窗组件减少性能开销。
- 可定制操作栏,内置展开 / 收起查询表单按钮
操作栏支持自定义外边距、左右反转布局、底部分割线;内置「展开 / 收起查询」内置按钮,自动根据查询表单状态切换图标与文字;支持自定义actionBar插槽放置新增、批量删除等业务按钮;可控制内置按钮是否显示,布局采用 flex 自适应对齐。
- 全局 + 局部双层扩展配置合并体系
内置defaultExtConfig默认扩展配置,业务传入extConfig自动与默认配置、表格扩展配置合并(MergedExtConfig);扩展配置覆盖查询请求、弹窗、操作栏、分页等全场景参数,遵循「业务局部配置 > 内置默认配置」优先级,TS 完整类型约束。
- 字典自动收集派发,联动外部字典加载逻辑
监听configs配置变化,自动提取所有useDict字典标识,通过onDictKeys事件抛出字典 key 数组;业务侧可监听事件批量加载字典,注入组件dictMap实现下拉、单选等字典渲染联动。
- 丰富插槽透传,全场景自定义无限制
完整透传所有子组件插槽(查询表单、表格、操作栏、弹窗头部 / 内容 / 底部);支持自定义表格区域customArea插槽替代原生表格;所有插槽自动透传插槽作用域参数,子组件属性通过v-bind="$attrs"透传,兼容 Element Plus 原生全部属性。
- 自适应高度计算,适配页面 / 弹窗两种布局模式
通过extConfig.mode.type区分页面模式、弹窗内嵌模式,自动读取全局注入的表格高度配置;根容器采用 flex 弹性布局,表格区域自适应剩余高度,最小高度兜底避免滚动异常。
- 完整 TS 类型闭环,强类型约束
分层定义Configs/ExtConfig/MergedExtConfig/UseCrudState/UseCrudReturn全套类型;props、emits、返回实例、扩展配置均有完备类型推导;defineExpose对外暴露全部核心方法与状态,外部 TS 调用有完整类型提示,无any泛滥问题。
- 查询逻辑标准化,分页自动重置
点击查询自动重置为第一页,区分表单原始模型formModel与接口请求参数formParams;提供onQuery方法供查询按钮、重置按钮调用,支持回调函数等待请求完成;无查询接口时直接执行回调,不发起无效请求。
- 内存与生命周期安全处理
组件卸载、路由失活时自动中止进行中的列表请求,释放AbortController;watch 监听配置使用deep: true+immediate: true初始化解析列配置;所有 ref、shallowRef 合理区分使用场景,避免不必要的响应式开销,无事件内存泄漏风险。
Demo
Props
import type { PropType } from 'vue'
import type { Configs, MergedExtConfig } from './types'
/**
* 组件props
*/
export default {
/**
* 配置 必传
* 1、该configs是【formColumns】和【tableColumns】的统一配置入口
* 2、该configs在【formColumns】和【tableColumns】的配置基础上新增了【queryLabel】、【onlySearch】和【tableOrder】配置项用于兼容特殊应用场景
*/
configs: {
type: Array as PropType<Configs[]>,
required: true
},
/**
* 扩展配置 如【操作栏/弹窗】等
* 该extConfig是本组件extConfig和UseElTable组件extConfig合并后的扩展配置
*/
extConfig: {
type: Object as PropType<MergedExtConfig>,
default: () => ({})
}
}Types
import type {
Component,
ComputedRef,
ExtractPropTypes,
Ref,
ShallowRef
} from 'vue'
import componentProps from './props'
import type { FormColumn } from '../UseElForm/types'
import type {
TableColumn,
ExtConfig as TableExtConfig
} from '../UseElTable/types'
/**
* props类型
*/
export type Props = ExtractPropTypes<typeof componentProps>
/**
* emits类型
*/
export type Emits = {
/** dictKeys事件 */
(e: 'onDictKeys', value?: string[]): void
}
/**
* 基础配置类型
*/
export type BaseConfigs = FormColumn & TableColumn
/**
* 配置项接口
*/
export interface Configs extends BaseConfigs {
/** 查询表单项标签 当查询表单项标签和表格项标签不一致使使用 */
queryLabel?: string
/** 查询表单项属性 当查询表单项属性和表格项属性不一致使使用,可传以逗号分隔的字符串,组件内部自动拆解*/
queryProp?: string
/** 是否仅搜索 */
onlySearch?: boolean
/** 表格排序 */
tableOrder?: number
}
/**
* 分割线配置接口
*/
export interface Divider {
/** 是否显示分割线 */
show?: boolean
/** 分割线边框样式 */
borderStyle?: 'none' | 'solid' | 'hidden' | 'dashed'
}
/**
* 操作栏配置接口
*/
export interface ActionBar {
/** 操作栏外边距 */
margin?: string
/** 是否显示【收起/展开查询】按钮 */
showControlFormDisplayBtn?: boolean
/** 【收起/展开查询】按钮展开时图标 */
controlFormDisplayBtnWithOpenedIcon?: Component
/** 【收起/展开查询】按钮收起时图标 */
controlFormDisplayBtnWithClosedIcon?: Component
/** 是否反转操作栏按钮 仅showControlFormDisplayBtn为true时生效 */
reverse?: boolean
/** 操作栏底部分割线 */
divider?: Divider
}
/**
* extConfig接口
*/
export interface ExtConfig {
/** 增删改查请求配置项 */
crudFetch?: {
/** 查询api */
queryApi?: Function
/** 是否立即查询 */
queryImmediate?: boolean
/** 自动取消查询策略 配合signal实现接口取消请求 */
autoCancelQueryStrategy?: 'none' | 'unmount' | 'deactivate' | 'both'
/** 开始查询前钩子 用于注入字典数据等 */
onQueryBefore?: (done: () => void) => void
/** 查询错误钩子 */
onQueryError?: (err: any) => void
/** 删除api */
deleteApi?: Function
}
/** 操作栏配置项 */
actionBar?: ActionBar
/** 弹窗配置项 */
modal?: {
/** 弹窗类型 支持Drawer和Dialog */
type?: 'Drawer' | 'Dialog'
/** 其他配置项由于组件内部已进行v-bind透传,故不在此重复声明接口,详见UseElDrawer和UseElDialog组件types */
}
}
/**
* 合并后的extConfig类型 本组件ExtConfig和UseElTable组件ExtConfig合并
*/
export type MergedExtConfig = ExtConfig & TableExtConfig
/**
* Crud状态类型
*/
export interface UseCrudState {
/** 表单列配置 */
formColumns: FormColumn[]
/** 表格列配置 */
tableColumns: TableColumn[]
/** 表单模型 */
formModel: Record<string, any>
/** 表格loading */
tableLoading: boolean
/** 表格数据 */
tableData: any[]
/** 当前页码 */
currentPage: number
/** 分页大小 */
pageSize: number
/** 分页总数 */
total: number
/** 字典映射 */
dictMap: Record<string, any>
}
/**
* Crud返回类型
*/
export interface UseCrudReturn {
state: Ref<UseCrudState>
heightComputed: ComputedRef<string>
fetchTableData: (
options?: { params?: Record<string, any>; isPageApi?: boolean },
cb?: () => void
) => Promise<any>
onQuery: (e: Record<string, unknown>, done?: () => void) => void
onCancelQueryRequest: () => void
handleDelete: (
payload: { id?: string | number; ids?: string | Array<string | number> },
loadingConfig: { source?: Record<string, unknown>; key?: string },
successCb?: () => void,
errorCb?: (err: unknown) => void,
successMsg?: string
) => Promise<void>
margin: ComputedRef<string | undefined>
actionBar: any
handleDisplayQueryForm: () => void
showModal: Ref<boolean>
modalComponent: ShallowRef<Component | null>
handleShowModal: () => Promise<void>
handleCloseModal: ComputedRef<() => void>
}DefaultExtConfig
import type { MergedExtConfig } from './types'
/**
* 默认扩展配置
*/
export default {
crudFetch: { queryImmediate: true, autoCancelQueryStrategy: 'unmount' },
mode: { type: 'PAGE' },
actionBar: {
margin: '20px 0',
showControlFormDisplayBtn: true,
reverse: false,
divider: { show: false, borderStyle: 'solid' }
},
modal: { type: 'Drawer' }
} satisfies MergedExtConfigDefineExpose
defineExpose({
state,
onQuery,
onCancelQueryRequest,
handleDelete,
modalRef,
handleShowModal,
handleCloseModal
})