技术难题初登场
家人们,最近在开发一个超复杂的后台管理系统项目,里面有各种数据展示、表单提交、权限控制等功能,在这个过程中,我频繁地使用到了element-plus
组件库中的el-dialog
组件 。它就像一个小弹窗,可以用来显示各种提示信息、编辑表单之类的。比如说在用户点击 “编辑” 按钮时,就弹出一个el-dialog
,里面放着编辑表单,让用户修改数据。
但随着功能的不断完善,我发现原生的el-dialog
组件在某些场景下真的不够灵活。比如说,我想在不同的组件中根据不同的业务逻辑动态地控制对话框的显示和隐藏,还要传递不同的数据给对话框,原生组件用起来就很麻烦,每次都要写很多重复的代码,这可太影响开发效率了。所以,我就决定对el-dialog
进行二次封装,实现动态调用,让它更好地满足我的项目需求。接下来,就把我的经验分享给大家,一起看看怎么解决这个问题。
实现效果
代码实现
import { ElButton, ElDialog } from "element-plus";
import { createApp, h } from "vue";
// 创建一个 DialogManager 类来管理对话框
class DialogManager {
constructor() {
this.dialogs = [];
}
/**
* 创建并显示一个动态对话框
* @param {Object} options - 对话框配置选项
* @returns {Object} - 返回对话框实例
*/
create(options = {}) {
// 合并默认配置
const defaultOptions = {
title: "提示",
visible: true,
fullscreen: false,
top: "15vh",
modal: true,
lockScroll: true,
closeOnClickModal: true,
closeOnPressEscape: true,
beforeClose: null,
footerBtns: [
{
label: "取消",
type: "default",
handler: (instance) => {
instance.close();
},
},
{
label: "确定",
type: "primary",
handler: (instance) => {
instance.close();
},
},
],
};
const dialogOptions = { ...defaultOptions, ...options };
// 创建一个容器元素
const container = document.createElement("div");
document.body.appendChild(container);
// 创建 Dialog 组件实例
const app = createApp({
data() {
return {
dialogVisible: dialogOptions.visible,
};
},
provide() {
return {
manager: this.$options.manager, // 提供manager
};
},
inject: ["manager"], // 注入manager
render() {
const footerNodes = dialogOptions.footerBtns.map((btn, index) => {
return h(
ElButton,
{
key: index,
type: btn.type || "default",
size: "small",
onClick: () => {
if (btn.handler) {
btn.handler(this);
}
},
},
() => btn.label
);
});
// 改进 content 渲染方式
const renderContent = () => {
if (typeof dialogOptions.content === "string") {
return h("div", { class: "dialog-content" }, dialogOptions.content);
} else if (dialogOptions.content) {
// 只传递必要的属性和方法
const props = {
// 提供关闭对话框的回调
closeDialog: () => {
this.close();
},
// 可以添加其他必要的属性
};
// 如果有额外的 props,合并它们
if (dialogOptions.contentProps) {
Object.assign(props, dialogOptions.contentProps);
}
return h(dialogOptions.content, props);
} else {
return h("div", { class: "dialog-content" }, "No content provided");
}
};
return h(
ElDialog,
{
title: dialogOptions.title,
modelValue: this.dialogVisible,
"onUpdate:modelValue": (val) => {
this.dialogVisible = val;
},
fullscreen: dialogOptions.fullscreen,
top: dialogOptions.top,
modal: dialogOptions.modal,
lockScroll: dialogOptions.lockScroll,
closeOnClickModal: dialogOptions.closeOnClickModal,
closeOnPressEscape: dialogOptions.closeOnPressEscape,
beforeClose: dialogOptions.beforeClose,
onClose: () => {
this.dialogVisible = false;
if (dialogOptions.onClose) {
dialogOptions.onClose(this);
}
// 延迟销毁,让关闭动画完成
setTimeout(() => {
this.destroy();
}, 300);
},
},
{
// 默认插槽用于对话框内容
default: renderContent,
footer: () =>
h(
"div",
{
class: "dialog-footer",
},
footerNodes
),
}
);
},
methods: {
// 关闭对话框
close() {
this.dialogVisible = false;
},
// 销毁对话框实例
destroy() {
app.unmount();
if (container.parentNode) {
container.parentNode.removeChild(container);
}
// 从管理列表中移除
if (this.manager) {
const index = this.manager.dialogs.indexOf(this);
if (index !== -1) {
this.manager.dialogs.splice(index, 1);
}
}
},
},
mounted() {
// 添加到管理列表
if (this.manager) {
this.manager.dialogs.push(this);
} else {
console.error("Manager is not initialized");
}
},
}).provide("manager", this); // 全局提供manager
// 挂载应用
const instance = app.mount(container);
instance.manager = this; // 直接在实例上设置manager
return instance;
}
/**
* 关闭所有对话框
*/
closeAll() {
this.dialogs.forEach((dialog) => {
dialog.close();
});
}
}
// 创建单例实例
const dialogManager = new DialogManager();
// 导出创建对话框的函数
export function createDialog(options) {
return dialogManager.create(options);
}
// 导出关闭所有对话框的函数
export function closeAllDialogs() {
dialogManager.closeAll();
}
调用示例
-------------------------------- 基本文本对话框 -------------------------------------
import { createDialog } from '@/utils/dialog-manager';
export default {
methods: {
showSimpleDialog() {
createDialog({
title: '确认操作',
content: '你确定要执行这个操作吗?',
onClose: () => {
console.log('对话框已关闭');
},
footerBtns: [
{
label: '取消',
handler: (instance) => {
console.log('点击了取消');
instance.close();
},
},
{
label: '确认',
type: 'primary',
handler: (instance) => {
console.log('执行确认操作');
instance.close();
},
},
],
});
},
},
};
-------------------------- 包含组件的对话框 --------------------------------
import { createDialog } from '@/utils/dialog-manager';
import MyComponent from '@/components/MyComponent.vue';
export default {
methods: {
showComponentDialog() {
createDialog({
title: '自定义组件对话框',
content: MyComponent,
contentProps: { // 这里可以传入组件的props
message: '这是来自父组件的消息',
src: 'xxx',
},
width: '600px',
footerBtns: [
{
label: '关闭',
handler: (instance) => {
instance.close();
},
},
],
});
},
},
};
-------------------------- 异步操作对话框 --------------------------------
import { createDialog } from '@/utils/dialog-manager';
export default {
methods: {
async showAsyncDialog() {
const dialog = createDialog({
title: '正在加载...',
content: '数据加载中,请稍候...',
showClose: false, // 隐藏关闭按钮
footerBtns: [], // 不显示底部按钮
});
try {
// 模拟异步操作
const result = await this.fetchData();
dialog.close();
// 显示成功消息
createDialog({
title: '操作成功',
content: '数据加载完成',
footerBtns: [
{
label: '确定',
type: 'primary',
handler: (instance) => instance.close(),
},
],
});
} catch (error) {
dialog.close();
// 显示错误消息
createDialog({
title: '操作失败',
content: `错误: ${error.message}`,
footerBtns: [
{
label: '关闭',
handler: (instance) => instance.close(),
},
],
});
}
},
},
};
原理剖析
这里面的原理其实也不难理解,主要是利用了Vue
的响应式原理 。我们都知道,在Vue
中,数据发生变化时,视图会自动更新。我们封装的组件就是基于这个特性,通过一个响应式的变量来控制el-dialog
的显示与隐藏。比如说,我们定义一个isDialogVisible
变量,当它为true
时,el-dialog
就显示,为false
时就隐藏 。
而动态传递参数呢,则是通过props
来实现的。我们在封装的组件中定义好props
,然后在调用组件的时候,就可以把需要的数据通过props
传递进去,这样对话框就能根据不同的数据展示不同的内容啦 。比如说,我们要在对话框里显示不同的提示信息,就可以把提示信息作为props
传递给对话框组件 。再结合provide
和inject
,它们就像是一座桥梁,能够让不同层级的组件之间方便地进行通信,让数据的传递更加灵活 。
实际应用场景
经过二次封装实现动态调用的el-dialog
在实际项目中的应用场景可太广泛了 。比如说在用户信息编辑场景,当用户点击 “编辑” 按钮,就可以动态弹出我们封装好的对话框,里面填充好用户当前的信息,用户修改完成后点击确认,就能提交新的信息,整个过程非常流畅自然 。
在文件上传确认场景,当用户选择好文件准备上传时,弹出对话框让用户确认文件信息,比如文件名、文件大小等 。如果没问题再点击上传,这样可以避免用户误操作上传错误的文件 。还有在权限管理中,当管理员要给某个用户分配新的权限时,通过动态调用对话框,展示所有权限选项,管理员勾选后提交,就能完成权限分配,操作简单又高效 。
总结
经过二次封装实现动态调用的el-dialog
组件,不仅大大提高了代码的复用性,让我们在不同的业务场景中能够轻松应对对话框的各种需求,还增强了项目的灵活性和可维护性。以前写一堆重复代码的日子一去不复返啦!
强烈建议各位小伙伴在自己的项目中也尝试应用这种二次封装的方法 ,相信你们会发现它的强大之处。要是在实践过程中有什么经验,或者遇到了问题,都欢迎在评论区留言分享。咱们一起交流,共同进步 !