前言
在 Vue3 项目开发中,对话框是一个使用频率极高的组件。Element Plus 提供了功能强大的 Dialog 组件,但在实际业务中,痛点太多,维护弹窗变量,关闭弹窗清除数据状态,固定代码量等等,本文将分享如何将 Element Plus 的 Dialog 组件封装成一个灵活易用的 Hooks,大幅提升开发效率。
1.实现内容
- 函数式调用
- 自定义且异步加载dialog组件内容并支持骨架屏
- 统一维护visible变量和loading状态
- 统一操作方法并异步回调
- 弹窗每次开启都是新建的dom 不会遗留上次打开留下的数据状态

2.使用示例代码 (写法基本都是固定的)
hooks使用示例:
javascript
import { useDialog } from '@/components/UseDialog';
useDialog({
title: "title",
width: "60%", // 或者 '1200px'
dialogID: 'demo-id', // 同时有多个弹窗打开需要用到,否则只会打开一个弹窗
component: defineAsyncComponent(() => import("xxx.vue")),
demo1: '2', // 参数名自己定义
actions: [
{type: "default", label: "关闭", key: "close"},
{type: "primary", label: "保存", key: "save"}
]
}).then((...args) => console.log(...args, '我是异步执行的'))
引入组件示例
xml
<template>
<div>
我是你自定义引入的组件,
</div>
</template>
<script setup lang="ts">
defineExpose({ handleAction });
const props = defineProps({}) // 自定义传入
const emits = defineEmits(["close", "loading"]);
// 这是固定的方法,用来处理弹窗的操作 与 接口交互相关
async function handleAction() {
try {
emits("loading", true);
const data = {};
await xxx(data);
ElMessage.success("操作成功");
} finally {
emits("loading", false);
emits("close", {我是传到外面的参数});
}
}
</script>
<style lang="scss" scoped></style>
3.封装的hooks代码
代码目录
UseDialog
index.js
index.vue
index.js代码:
javascript
import FinderDialog from "./index.vue";
import { render, createVNode } from "vue";
import { app } from "@/main"; // 需要再main.ts文件中将app暴漏出来
export function useDialog(params) {
return new Promise((resolve) => {
const id = params.dialogID
? "__demo_dialog_id__" + params.dialogID
: "__demo_dialog_id__";
let el;
if (!(el = document.getElementById(id))) {
el = document.createElement("div");
el.id = id;
document.body.appendChild(el);
}
// 创建VNode
const vnode = createVNode(FinderDialog, {
attrs: params,
data: params.data,
component: params.component,
state: true,
onDestroy: (...args) => {
resolve(...args);
// 清理DOM
render(null, el);
if (el.parentNode) {
el.parentNode.removeChild(el);
}
},
});
// 使用应用实例的上下文
vnode.appContext = app._context;
// 渲染到DOM
render(vnode, el);
});
}
index.vue代码:
ini
<template>
<el-dialog
ref="dialog"
v-bind="attrs"
append-to-body
:close-on-click-modal="false"
:show-close="!loading"
:model-value="visible"
@close="handleClose"
>
<el-config-provider :locale="locale">
<Suspense>
<template #default>
<component
:is="component"
ref="commonDialog"
v-loading="loading"
v-bind="attrs"
:data="data"
:element-loading-text="loadingText"
@close="handleClose"
@loading="setLoading"
/>
</template>
<template #fallback>
<el-skeleton :rows="3" animated />
</template>
</Suspense>
</el-config-provider>
<template v-if="attrs.actions" #footer>
<div>
<el-button
v-for="(item, index) in attrs.actions"
:key="index"
:loading="loading"
v-bind="item"
@click="handleAction(item)"
>
{{ item.label }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { useAppStore } from "@/store"; // 这是多语言相关的 不用可以去掉
defineOptions({
name: "FinderDialog",
inheritAttrs: false,
});
const props = defineProps({
data: {
type: Array,
},
loadingText: {
type: String,
default: "正在处理中...",
},
component: {
type: [Function, Object],
required: true,
default: null,
},
state: {
type: Boolean,
},
attrs: {
type: Object,
default: () => ({}),
},
});
const appStore = useAppStore();
const emit = defineEmits(["destroy"]);
const visible = ref(false);
const loading = ref(false);
const commonDialog = ref(null);
const dialog = ref(null);
const locale = computed(() => appStore.locale);
onMounted(() => {
visible.value = props.state;
});
async function handleAction(item) {
if (item.key === "close") {
handleClose();
return;
}
const actionHandler = commonDialog.value?.handleAction;
actionHandler && actionHandler(item);
}
function handleClose(...args) {
visible.value = false;
emit("destroy", ...args);
}
function setLoading(state) {
loading.value = state;
}
</script>