通常我们的主页面会包含多个dialog,这种情况下我们需要将这些dialog组件悉数引入到页面中,并写在template模板中;同时还需要多个visible、多个confirm、多个cancel来分别控制各个dialog。 这样做当然可行,但多少有点不够优雅。 基于之前vue2中基于Promise封装dialogManager的经验,在新项目中同样封装了一个弹窗管理工具。 DialogManage.ts
import {render,createVNode} from 'vue'
export default class M_Dialog{
static root = document.createElement('div')
static openDialog = (component,props)=>{
return new Promise((resolve,reject)=>{
const dialogInstance = initDialogInstance(component,{
myProps: props,
visible:true,
resolve:data=>resolve({dialogInstance,props}),
reject:data => reject({dialogInstance,props})
})
dialogInstanceMount(dialogInstance,this.root)
})
}
static closeDialog = (dialogInstance)=>{
dialogInstanceUnmount(dialogInstance)
}
}
function initDialogInstance(component,props){
return createVNode(component,props)
}
function dialogInstanceMount(dialogInstance,root){
document.body.appendChild(root)
render(dialogInstance,root)
}
function dialogInstanceUnmount(dialogInstance){
const el = dialogInstance.el.parentNode
// 卸载组件
if(dialogInstance) render(null,el)
dialogInstance = null;
// 移除组件父元素
document.body.removeChild(el);
}
dialogText.vue 业务dialog组件,需要引入基础组件dialog.vue。
<template>
<Dialog :title="'标题'" :visible="dialogVisible" @dialogConfirm="dialogConfirm" @dialogCancel="dialogCancel">
<div> 哈哈 </div>
</Dialog>
</template>
<script setup lang="ts">
import Dialog from "@/components/common/dialog.vue";
const dialogVisible = ref(false)
const props = defineProps({
myProps:{
type:Object,
default:()=>{
return{
}
}
},
resolve:{
type:Function,
default:()=>{}
},
reject:{
type:Function,
default:()=>{}
},
visible:{
type:Boolean,
default:false
}
})
onMounted(()=>{
dialogVisible.value = props.visible;
console.log(props,dialogVisible)
})
onBeforeUnmount(()=>{
dialogVisible.value = false
})
function dialogConfirm(){
dialogVisible.value = false
props.resolve('confirm')
}
function dialogCancel(){
dialogVisible.value = false
props.reject('cancel')
}
</script>
<style scoped lang="scss">
</style>
dialog.vue 基础dialog组件,提供插槽将业务dialog的内容插入。
<template>
<el-dialog
:model-value="visible"
:title="title"
:close-on-click-modal="false"
class="dm-dialog"
ref="dialog"
:show-close="true"
@close="cancel">
<div class="dm-dialog-body">
<slot/>
</div>
<div slot="footer" class="dm-dialog-footer" v-if="showFooter">
<el-button @click="cancel">{{cancelText}}</el-button>
<el-button type="primary" @click="confirm">{{confirmText}}</el-button>
</div>
</el-dialog>
</template>
<script setup lang="ts">
const props = defineProps({
visible:{
type:Boolean,
default:false,
},
title:{
type:String,
default:'标题'
},
cancelText:{
type:String,
default:'取消'
},
confirmText:{
type:String,
default:'确定'
},
showFooter:{
type:Boolean,
default:true
},
width:{
type:String,
default:'30%'
}
})
const emits = defineEmits(['dialogConfirm','dialogCancel'])
function confirm (){
emits('dialogConfirm')
}
function cancel(){
emits('dialogCancel')
}
</script>
<style scoped lang="scss">
.dm-dialog{
.dm-dialog-footer{
display: flex;
justify-content: flex-end;
}
}
</style>
使用业务dialog的父组件,需要引入业务dialog组件及DialogManager.ts文件
<template>
<div class="vehicle-set">
<h2 class="page-title">{{$t('message.vehicleSet')}}</h2>
<div class="vehicle-list-wrap">
<header>
<h3>{{ $t('message.vehicleSetList') }}</h3>
<el-button type="primary" @click="addVehicleSet">{{ $t('message.add') }}</el-button>
</header>
<main>
<vehicle-set-table :table-data="tableData"></vehicle-set-table>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import VehicleSetTable from "@/components/table/vehicleSet/vehicleSetTable.vue"
import M_Dialog from "@/utils/DialogManager";
import dialogTest from "@/components/common/dialogTest.vue"
const tableData = ref([
{id: 1,name:'aaa',type:'1',count:100,createTime:'2022-03-15 12:48:41'}
])
const addVehicleSet = ()=>{
M_Dialog.openDialog(dialogTest,{a:1}).then(data => {
const { dialogInstance , props } = data;
console.log(props)
M_Dialog.closeDialog(dialogInstance)
}).catch(data=>{
const {dialogInstance,props} = data;
console.log(props)
M_Dialog.closeDialog(dialogInstance)
})
}
const getTableData = ()=>{}
onMounted(()=>{
getTableData()
})
</script>
<style scoped lang="scss">
.vehicle-set{
width: 100%;
height: 100%;
color: var(--el-text-color-regular);
padding: 24px;
display: flex;
flex-direction: column;
.page-title{
font-size: 20px;
padding-bottom: 24px;
}
.vehicle-list-wrap{
flex-grow: 1;
display: flex;
flex-direction: column;
border: var(--el-border);
padding: 10px;
header{
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
main{
flex-grow: 1;
}
}
}
</style>
在实际开发中发现了新问题: 在使用dialog的时候用到了vue-i18n,然而,通过以上方式创建的组件,是无法获得当前上下文的,因此就无法使用$t。在看Element-plus 的 ElMessageBox组件的时候发现它的功能与我的很相似,且它可以将当前上下文传递进去并挂载到组件上。于是就去lMessageBox的原码中找了找,照猫画虎的实现了工功能: DialogManager代码修改为:
import {render,createVNode} from 'vue'
export default class M_Dialog{
static root = document.createElement('div')
static openDialog = (component,props,appContext=null)=>{
return new Promise((resolve,reject)=>{
const dialogInstance = initDialogInstance(component,{
myProps: props,
visible:true,
resolve:data=>resolve({dialogInstance,props}),
reject:data => reject({dialogInstance,props})
},appContext)
dialogInstanceMount(dialogInstance,this.root)
})
}
static closeDialog = (dialogInstance)=>{
dialogInstanceUnmount(dialogInstance)
}
}
function initDialogInstance(component,props,appContext){
const vNode = createVNode(component,props)
vNode.appContext = appContext // 调用openDialog的时候,传入上下文,挂载到vNode上就可以了
return vNode
}
function dialogInstanceMount(dialogInstance,root){
document.body.appendChild(root)
render(dialogInstance,root)
}
function dialogInstanceUnmount(dialogInstance){
const el = dialogInstance.el.parentNode
// 卸载组件
if(dialogInstance) render(null,el)
dialogInstance = null;
// 移除组件父元素
document.body.removeChild(el);
}
使用的时候通过
import { getCurrentInstance } from 'vue
const { appContext } = getCurrentInstance()
的方式获取appContext就可以了