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 插件,在全局范围内复用。这种 "抽象共性、灵活扩展" 的组件设计思想,不仅适用于弹窗,也适用于表单、表格等其他高频组件,是提升前端开发效率的关键。

相关推荐
ErMao6 小时前
深入理解let、const和var
前端
IT_陈寒6 小时前
SpringBoot 3.2新特性实战:这5个隐藏功能让开发效率翻倍🚀
前端·人工智能·后端
涛哥AI编程6 小时前
【AI编程干货】Token成为硬通货后,我的7000字Claude Code精算准则
前端·ai编程
IT_陈寒6 小时前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升70% 🚀
前端·人工智能·后端
南山安6 小时前
面试必考点: 深入理解CSS盒子模型
javascript·面试
golang学习记6 小时前
从0死磕全栈之深入理解 Next.js 中的 NextResponse:API 详解与实战示例
前端
木易士心6 小时前
CSS 中 `data-status` 的使用详解
前端
木易士心6 小时前
JavaScript 数组的核心操作方法,从基础到高级
前端
TimelessHaze6 小时前
🧱 一文搞懂盒模型box-sizing:从标准盒到怪异盒的本质区别
前端·css·面试