Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件
在 Vue 项目开发中,数据选择弹窗是高频出现的交互组件,比如用户选择、角色选择、部门选择等场景。如果每个选择场景都重复开发弹窗逻辑,不仅会导致代码冗余,还会增加维护成本。本文将深入解析一个基于 Vue 3 和 Arco Design 的选择弹窗工厂函数,带你理解其设计思想、实现细节与应用方式,助力提升组件复用效率。
完整源码展示
import type { Component } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { h, ref } from 'vue'
interface CreateSelectDialogParams {
title: string
component: Component
componentProps?: Record<string, any>
tip?: string
}
/**
* 选择弹窗配置选项接口
* @template T 选中数据的类型
*/
interface SelectDialogOptions<T> {
/** 弹窗标题(会覆盖创建参数中的title) */
title?: string
/** 是否允许多选 */
multiple?: boolean
/** 查询参数,通常用于初始化数据 */
queryParams?: Record<string, any>
/** 传递给组件的额外属性 */
componentProps?: Record<string, any>
/** 点击确定按钮后的回调函数 */
onOk?: (data: T) => void
/** 点击确定前的校验函数,返回Promise<boolean>决定是否允许确定 */
onBeforeOk?: (data: T) => Promise<boolean>
}
/**
* 创建一个选择类型的弹窗工厂函数
*
* 该函数返回一个创建特定类型选择弹窗的方法,适用于需要从列表中选择数据的场景。
* 内部使用Vue的createVNode动态渲染组件,并通过ref获取组件实例的方法。
*
* @template T 选中数据的类型
* @param {CreateSelectDialogParams} params 创建弹窗所需的基本参数
* @returns {(options: SelectDialogOptions<T>) => void} 可配置的弹窗创建函数
*
* @example
* // 创建一个用户选择弹窗
* const selectUserDialog = createSelectDialog({
* title: '选择用户',
* component: UserSelectComponent,
* tip: '请至少选择一个用户'
* })
*
* // 打开弹窗并处理选择结果
* selectUserDialog({
* multiple: true,
* queryParams: { status: 'active' },
* onOk: (selectedUsers) => {
* console.log('已选择用户:', selectedUsers)
* }
* })
*/
export const createSelectDialog = <T = any>(params: CreateSelectDialogParams) => {
return (options: SelectDialogOptions<T>) => {
const TableRef = ref<any>()
Modal.open({
// 优先使用options中的title,否则使用params中的title
title: options.title || params.title,
// 动态渲染传入的组件,设置ref引用并合并属性
content: () => h(params.component, {
ref: (e: any) => (TableRef.value = e),
multiple: options.multiple,
queryParams: options.queryParams,
...params.componentProps,
...options.componentProps
}),
// 设置弹窗宽度自适应
width: 'calc(100% - 20px)',
modalStyle: { maxWidth: '1000px' },
bodyStyle: { overflow: 'hidden', height: '500px', padding: 0 },
onBeforeOk: async () => {
// 检查组件是否暴露了必要的getSelectedData方法
if (!TableRef.value?.getSelectedData) {
Message.warning('组件必须暴露getSelectedData方法')
return false
}
// 获取选中的数据
const data = TableRef.value?.getSelectedData?.() || []
// 验证是否选择了数据
if (!data.length) {
Message.warning(params.tip || '请选择数据')
return false
}
// 如果提供了前置校验函数,则调用并根据结果决定是否继续
if (options?.onBeforeOk) {
return await options.onBeforeOk(data)
}
// 调用确定回调函数,传递选中的数据
options.onOk?.(data)
return true
}
})
}
}
一、函数设计背景与核心目标
在中后台系统中,数据选择弹窗通常具备以下共性需求:
- 统一的弹窗容器(标题、确认 / 取消按钮、尺寸控制);
- 动态嵌入不同的选择组件(如用户列表、角色表格);
- 支持单选 / 多选切换、初始化查询参数传递;
- 选中数据校验、前置拦截与结果回调;
- 组件间通信与方法调用(如获取选中数据)。
传统开发方式中,这些需求往往通过 “复制粘贴 + 修改” 实现,导致代码重复率高、
逻辑分散而本文解析的createSelectDialog工厂函数,正是为解决这些痛点而生,其核心目标是:封装共性逻辑,暴露个性化配置,实现 “一次定义,多场景复用” 。
二、核心架构与类型定义解析
在理解函数实现前,我们先梳理其类型接口与整体架构,这是保障代码健壮性和可维护性的基础。
1. 关键接口定义
函数通过 TypeScript 接口明确了参数与配置的结构,避免类型混乱,提升开发体验。
(1)创建弹窗的基础参数接口:CreateSelectDialogParams
该接口定义了创建特定类型选择弹窗的 “固定属性”,是工厂函数的 “原料”:
interface CreateSelectDialogParams {
title: string; // 弹窗默认标题
component: Component; // 嵌入弹窗的选择组件(如用户列表)
componentProps?: Record<string, any>; // 传递给选择组件的默认属性
tip?: string; // 未选择数据时的提示文本
}
。
- component:核心属性,指定弹窗内渲染的选择组件(需暴露getSelectedData方法,用于获取选中数据);
- componentProps:为选择组件设置默认属性,如表格的border、rowKey等通用配置。
(2)弹窗配置选项接口:SelectDialogOptions
该接口定义了每次打开弹窗时的 “动态配置”,支持个性化调整,泛型T用于指定选中数据的类型,提升类型安全性:
interface SelectDialogOptions<T> {
title?: string; // 覆盖默认标题
multiple?: boolean; // 单选/多选切换
queryParams?: Record<string, any>; // 初始化查询参数(如筛选“活跃用户”)
componentProps?: Record<string, any>; // 覆盖默认组件属性
onOk?: (data: T) => void; // 确定按钮回调(返回选中数据)
onBeforeOk?: (data: T) => Promise<boolean>; // 确定前校验(如“最多选择10个用户”)
}
- 泛型T:解决不同选择场景下数据类型不一致的问题(如用户类型User、角色类型Role);
- onBeforeOk:支持异步校验(如调用接口检查选中数据合法性),返回Promise决定是否允许关闭弹窗。
2. 工厂函数整体架构
createSelectDialog是一个高阶函数,其核心逻辑分为两步:
- 接收CreateSelectDialogParams参数,封装弹窗的 “固定逻辑”(如组件渲染、基础样式);
- 返回一个新函数,该函数接收SelectDialogOptions参数,处理弹窗的 “动态配置”(如单选 / 多选、回调函数),并打开弹窗。
这种设计的优势在于:将 “固定共性” 与 “动态个性” 分离,一次创建可多次调用,且每次调用可灵活配置。
三、核心功能实现细节
接下来,我们深入函数内部,解析关键功能的实现逻辑,理解其如何解决数据选择弹窗的核心痛点。
1. 动态组件渲染与 Ref 引用
弹窗内容通过 Vue 的h函数(创建虚拟 DOM)动态渲染传入的component,并通过ref获取组件实例,实现方法调用:
content: () => h(params.component, {
ref: (e: any) => (TableRef.value = e), // 绑定组件Ref
multiple: options.multiple, // 传递单选/多选配置
queryParams: options.queryParams, // 传递查询参数
...params.componentProps, // 合并默认组件属性
...options.componentProps // 合并动态组件属性(优先级更高)
})
- 属性合并规则:options.componentProps > params.componentProps,支持动态覆盖默认属性;
- Ref 引用核心作用:通过TableRef.value获取选择组件实例,调用其暴露的getSelectedData方法,这是 “获取选中数据” 的关键。
2. 选中数据校验与前置拦截
onBeforeOk是弹窗的 “核心校验逻辑”,负责确保选中数据合法,并支持自定义拦截,流程如下:
onBeforeOk: async () => {
// 1. 检查组件是否暴露getSelectedData方法
if (!TableRef.value?.getSelectedData) {
Message.warning('组件必须暴露getSelectedData方法');
return false;
}
// 2. 获取选中数据
const data = TableRef.value?.getSelectedData?.() || [];
// 3. 校验是否选择数据
if (!data.length) {
Message.warning(params.tip || '请选择数据');
return false;
}
// 4. 自定义前置校验(如异步接口校验)
if (options?.onBeforeOk) {
return await options.onBeforeOk(data);
}
// 5. 触发确定回调,返回选中数据
options.onOk?.(data);
return true;
}
- 强制接口约束:要求嵌入的选择组件必须暴露getSelectedData方法,否则弹窗无法正常工作,这是 “组件间通信” 的约定;
- 异步校验支持:onBeforeOk返回Promise,支持调用接口进行校验(如 “检查选中用户是否已被占用”);
- 友好提示:通过Message组件提供明确的错误提示,提升用户体验。
3. 弹窗样式自适应
为适配不同屏幕尺寸,函数对弹窗样式做了精细化控制:
width: 'calc(100% - 20px)', // 宽度自适应(左右各留10px边距)
modalStyle: { maxWidth: '1000px' }, // 最大宽度限制(避免大屏下过宽)
bodyStyle: { overflow: 'hidden', height: '500px', padding: 0 } // 固定高度+隐藏滚动
- 自适应宽度:在小屏设备(如平板)上占满屏幕,大屏设备上限制最大宽度;
- 固定 body 高度:避免选择组件(如长表格)导致弹窗过高,同时通过overflow: hidden配合组件内部滚动,保证弹窗整体美观。
四、使用示例与场景拓展
理解了函数设计后,我们通过实际示例,看如何在项目中应用该工厂函数。
1. 基础使用:创建用户选择弹窗
假设我们有一个UserSelectComponent(用户选择组件,已暴露getSelectedData方法),通过以下步骤创建用户选择弹窗:
// 1. 导入依赖与组件
import { createSelectDialog } from './createSelectDialog';
import UserSelectComponent from './UserSelectComponent.vue';
// 2. 创建用户选择弹窗函数(固定配置)
const selectUserDialog = createSelectDialog({
title: '选择用户', // 默认标题
component: UserSelectComponent, // 嵌入的用户选择组件
tip: '请至少选择一个用户', // 未选择时的提示
componentProps: { // 传递给用户组件的默认属性
border: false,
showSearch: true
}
});
// 3. 在业务组件中调用(动态配置)
const handleSelectUser = () => {
selectUserDialog({
multiple: true, // 允许多选
queryParams: { status: 'active' }, // 初始化查询“活跃用户”
onBeforeOk: async (selectedUsers) => {
// 自定义校验:最多选择5个用户
if (selectedUsers.length > 5) {
Message.warning('最多只能选择5个用户');
return false;
}
// 异步校验:检查选中用户是否已关联角色
const res = await checkUserRole(selectedUsers.map(u => u.id));
return res.data.isValid;
},
onOk: (selectedUsers) => {
// 确定后的逻辑:如渲染选中用户列表
console.log('已选择用户:', selectedUsers);
// 业务逻辑:更新页面状态、提交表单等
}
});
};
2. 场景拓展:支持不同类型的选择弹窗
除了用户选择,该工厂函数还可用于角色选择、部门选择等场景,只需替换component参数即可:
// 角色选择弹窗
const selectRoleDialog = createSelectDialog({
title: '选择角色',
component: RoleSelectComponent,
tip: '请选择角色'
});
// 部门选择弹窗
const selectDeptDialog = createSelectDialog({
title: '选择部门',
component: DeptSelectComponent,
tip: '请选择部门'
});
通过这种方式,我们无需重复开发弹窗逻辑,只需关注 “选择组件本身”,极大提升开发效率。
五、优势总结与优化方向
1. 核心优势
- 高复用性:一次定义工厂函数,支持多类型选择弹窗(用户、角色、部门等);
- 类型安全:通过 TypeScript 泛型与接口,明确参数类型与返回值,减少运行时错误;
- 灵活配置:支持动态覆盖标题、单选 / 多选、查询参数等,适配不同业务场景;
- 约定式通信:通过 “组件暴露getSelectedData方法” 的约定,简化组件间通信逻辑。
2. 优化方向
- 支持自定义弹窗样式:当前弹窗宽度、高度为固定配置,可新增style参数,允许动态调整;
- 添加加载状态:在onBeforeOk异步校验时,添加弹窗加载状态(如禁用确认按钮),避免重复点击;
- 支持弹窗销毁回调:新增onClose参数,处理弹窗关闭后的逻辑(如清理组件缓存、重置状态);
- 类型强化:将TableRef的类型从any改为泛型,明确组件实例的方法与属性,提升类型安全性。
六、总结
createSelectDialog工厂函数通过 “封装共性、暴露个性” 的设计思想,解决了 Vue 项目中数据选择弹窗的复用问题。其核心在于:
- 用 TypeScript 接口规范参数结构,保障代码健壮性;
- 用 Ref 引用实现组件间方法调用,简化通信逻辑;
- 用高阶函数分离固定配置与动态配置,提升复用效率。
在实际项目中,我们可以基于该函数的设计思路,进一步拓展弹窗的功能(如自定义按钮、支持分页),也可将其封装为 Vue 插件,在全局范围内复用。这种 “抽象共性、灵活扩展” 的组件设计思想,不仅适用于弹窗,也适用于表单、表格等其他高频组件,是提升前端开发效率的关键。