Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件

在 Vue 项目开发中,数据选择弹窗是高频出现的交互组件,比如用户选择、角色选择、部门选择等场景。如果每个选择场景都重复开发弹窗逻辑,不仅会导致代码冗余,还会增加维护成本。本文将深入解析一个基于 Vue 3 和 Arco Design 的选择弹窗工厂函数,带你理解其设计思想、实现细节与应用方式,助力提升组件复用效率。

完整源码展示

js 复制代码
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
      }
    })
  }
}

一、函数设计背景与核心目标

在中后台系统中,数据选择弹窗通常具备以下共性需求:

  1. 统一的弹窗容器(标题、确认 / 取消按钮、尺寸控制);
  1. 动态嵌入不同的选择组件(如用户列表、角色表格);
  1. 支持单选 / 多选切换、初始化查询参数传递;
  1. 选中数据校验、前置拦截与结果回调;
  1. 组件间通信与方法调用(如获取选中数据)。

传统开发方式中,这些需求往往通过 "复制粘贴 + 修改" 实现,导致代码重复率高、

逻辑分散而本文解析的createSelectDialog工厂函数,正是为解决这些痛点而生,其核心目标是:封装共性逻辑,暴露个性化配置,实现 "一次定义,多场景复用"

二、核心架构与类型定义解析

在理解函数实现前,我们先梳理其类型接口与整体架构,这是保障代码健壮性和可维护性的基础。

1. 关键接口定义

函数通过 TypeScript 接口明确了参数与配置的结构,避免类型混乱,提升开发体验。

(1)创建弹窗的基础参数接口:CreateSelectDialogParams

该接口定义了创建特定类型选择弹窗的 "固定属性",是工厂函数的 "原料":

js 复制代码
interface CreateSelectDialogParams {
  title: string; // 弹窗默认标题
  component: Component; // 嵌入弹窗的选择组件(如用户列表)
  componentProps?: Record<string, any>; // 传递给选择组件的默认属性
  tip?: string; // 未选择数据时的提示文本
}

  • component:核心属性,指定弹窗内渲染的选择组件(需暴露getSelectedData方法,用于获取选中数据);
  • componentProps:为选择组件设置默认属性,如表格的border、rowKey等通用配置。

(2)弹窗配置选项接口:SelectDialogOptions

该接口定义了每次打开弹窗时的 "动态配置",支持个性化调整,泛型T用于指定选中数据的类型,提升类型安全性:

js 复制代码
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是一个高阶函数,其核心逻辑分为两步:

  1. 接收CreateSelectDialogParams参数,封装弹窗的 "固定逻辑"(如组件渲染、基础样式);
  1. 返回一个新函数,该函数接收SelectDialogOptions参数,处理弹窗的 "动态配置"(如单选 / 多选、回调函数),并打开弹窗。

这种设计的优势在于:将 "固定共性" 与 "动态个性" 分离,一次创建可多次调用,且每次调用可灵活配置。

三、核心功能实现细节

接下来,我们深入函数内部,解析关键功能的实现逻辑,理解其如何解决数据选择弹窗的核心痛点。

1. 动态组件渲染与 Ref 引用

弹窗内容通过 Vue 的h函数(创建虚拟 DOM)动态渲染传入的component,并通过ref获取组件实例,实现方法调用:

js 复制代码
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是弹窗的 "核心校验逻辑",负责确保选中数据合法,并支持自定义拦截,流程如下:

js 复制代码
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. 弹窗样式自适应

为适配不同屏幕尺寸,函数对弹窗样式做了精细化控制:

js 复制代码
width: 'calc(100% - 20px)', // 宽度自适应(左右各留10px边距)
modalStyle: { maxWidth: '1000px' }, // 最大宽度限制(避免大屏下过宽)
bodyStyle: { overflow: 'hidden', height: '500px', padding: 0 } // 固定高度+隐藏滚动
  • 自适应宽度:在小屏设备(如平板)上占满屏幕,大屏设备上限制最大宽度;
  • 固定 body 高度:避免选择组件(如长表格)导致弹窗过高,同时通过overflow: hidden配合组件内部滚动,保证弹窗整体美观。

四、使用示例与场景拓展

理解了函数设计后,我们通过实际示例,看如何在项目中应用该工厂函数。

1. 基础使用:创建用户选择弹窗

假设我们有一个UserSelectComponent(用户选择组件,已暴露getSelectedData方法),通过以下步骤创建用户选择弹窗:

js 复制代码
// 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参数即可:

js 复制代码
// 角色选择弹窗
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 项目中数据选择弹窗的复用问题。其核心在于:

  1. 用 TypeScript 接口规范参数结构,保障代码健壮性;
  1. 用 Ref 引用实现组件间方法调用,简化通信逻辑;
  1. 用高阶函数分离固定配置与动态配置,提升复用效率。

在实际项目中,我们可以基于该函数的设计思路,进一步拓展弹窗的功能(如自定义按钮、支持分页),也可将其封装为 Vue 插件,在全局范围内复用。这种 "抽象共性、灵活扩展" 的组件设计思想,不仅适用于弹窗,也适用于表单、表格等其他高频组件,是提升前端开发效率的关键。

相关推荐
JeffongTan14 分钟前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
不cong明的亚子2 小时前
web live多屏互动平台
javascript·经验分享·音视频
陆枫Larry3 小时前
Astro 是什么
前端
是立不是利4 小时前
写给设计师的 CSS 博客美学指南
前端·css
dsyyyyy11015 小时前
JS复习,难点,易错点总结
开发语言·javascript·ecmascript
默_笙5 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师5 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
kyriewen5 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员
a1117765 小时前
Shirone 二次元博客主题 开源
前端·开源
IT_陈寒5 小时前
Python的切片赋值把我坑惨了,这不是bug是特性
前端·人工智能·后端