Vue3 项目中封装dialogManager,优雅的使用el-dialog

1,465 阅读2分钟

通常我们的主页面会包含多个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就可以了