🌺
摘要
通过 DialogOpenOptions 与 DialogValue 两个泛型类型,把 Vue3 项目里常见的弹窗 open/close 逻辑抽成可复用 hooks。
业务页面里弹窗写法往往高度相似:visible 控制显隐、data 承载当前行数据,有时还要附带 optionType 这类模式字段。每个弹窗都手写一遍,既重复又容易漏类型。
下面是一个通用封装思路:用 TypeScript 泛型把「业务数据」和「可扩展字段」拆开,让 hooks 既能复用,又不丢类型提示。
设计目标
- 统一入口:
open(options)打开,visible关闭; - 类型安全:
data随业务变化,open入参有完整推断; - 可扩展:新增/编辑等场景可通过第二个泛型参数扩展字段;
类型定义
/**
* 通用弹窗
* @template T 弹窗业务数据(如某条列表行)
* @template E 可扩展字段(如 optionType),默认无额外字段
*
* @example
* // 仅 data
* const dialog = useDialogProps<RowItem>();
* dialog.value.open({ data: row });
*
* @example
* // 扩展 optionType
* const dialog = useDialogProps<RowItem, { optionType: 'add' | 'edit' }>({
* optionType: 'add',
* });
* dialog.value.open({ data: row, optionType: 'edit' });
*/
export type DialogOpenOptions<T extends object, E extends object = {}> = {
data?: T;
} & (keyof E extends never ? object : Partial<E>);
export type DialogValue<T extends object, E extends object = {}> = {
visible: boolean;
data?: T;
open: (options?: DialogOpenOptions<T, E>) => void;
} & E;
两个泛型分别管什么
| 泛型 | 含义 | 示例 |
|---|---|---|
T | 弹窗绑定的业务数据 | 列表行、表单草稿 |
E | 除 data 外的扩展状态 | { optionType: 'add' | 'edit' } |
DialogOpenOptions 里用 (keyof E extends never ? object : Partial<E>) 做条件类型:没有扩展字段时,open 只接受 { data? };有扩展字段时,open 可传入对应的 Partial<E>。
Hooks 实现
import { ref } from 'vue';
export const useDialogProps = <T extends object, E extends object = {}>(
initial?: E
) => {
const dialog = ref({
visible: false,
...(initial ?? {}),
open() {},
} as DialogValue<T, E>);
dialog.value.open = (options?: DialogOpenOptions<T, E>) => {
if (options?.data !== undefined) {
dialog.value.data = options.data as DialogValue<T, E>['data'];
}
if (options) {
const { data: _data, ...extra } = options;
Object.assign(dialog.value, extra);
}
dialog.value.visible = true;
};
return dialog;
};
实现要点
initial注入默认值:例如{ optionType: 'add' },弹窗初始即带模式;open内合并扩展字段:data单独赋值,其余字段通过Object.assign写入;as DialogValue<T, E>:先占位open,再挂载真实方法,避免循环引用时的类型报错。
使用示例
场景一:只传业务数据
interface UserRow {
id: number;
name: string;
}
const userDialog = useDialogProps<UserRow>();
// 打开并带入当前行
userDialog.value.open({ data: { id: 1, name: '张三' } });
// 模板中
// <UserDialog v-model:visible="userDialog.visible" :data="userDialog.data" />
场景二:新增 / 编辑共用弹窗
interface FormRecord {
id?: number;
title: string;
}
const formDialog = useDialogProps<FormRecord, { optionType: 'add' | 'edit' }>({
optionType: 'add',
});
// 新增
formDialog.value.open({ optionType: 'add' });
// 编辑
formDialog.value.open({
data: { id: 10, title: '示例标题' },
optionType: 'edit',
});
// formDialog.value.optionType 类型为 'add' | 'edit'
子组件可通过 defineProps 接收 visible、data、optionType,标题文案按 optionType 切换即可。
为什么用泛型而不是 any
// ❌ 失去类型约束
const dialog = ref<{ visible: boolean; data?: any; open: Function }>({ ... });
// ✅ 调用 open 时 data、optionType 均有推断
const dialog = useDialogProps<FormRecord, { optionType: 'add' | 'edit' }>();
dialog.value.open({ data: row, optionType: 'edit' });
泛型让「弹窗壳子」与「业务数据」解耦:hooks 本身不 import 任何业务 VO,却在每个使用点拿到精确类型。
可继续扩展的方向
- 增加
close(),统一重置visible与可选字段; open支持 Promise,配合await dialog.value.open()做提交后刷新;- 用
readonly包装data,避免弹窗内误改外部列表引用; - 多个弹窗共用同一
T、不同E,按页面组合即可。
小结
弹窗逻辑不复杂,难在「每个页面 copy 一份、类型还各自为政」。把 T(数据)和 E(扩展)拆成两个泛型参数,再用条件类型约束 open 的入参,就能在 Vue3 里得到一份轻量、可复用、类型完整的弹窗 hooks。
文章发表于 2026/06/11 20:00
评论
加载中...