在vue中优雅的使用 dialog 组件(解决:弹窗无限套娃导致代码难以维护)

1.效果图:

*tips:组件案例基于vite+vue3+pinia+elementplubs

gitee 源码地址 gitee.com/onlySmokeRi...

github 源码地址github.com/zxxaoligei/...

1.1只需调用hook 并传入基础配置

1.2 dialog的坑

平常开发中在一个页面中引入Dialog,最少需要额外维护一个visible变量,如果有多个dailog 甚至又要维护多个dialog的visible变量,如此一来代码就优雅不起来,且多个变量变来变去 看着也烦人。

所以如果将每个dialog 视为一个组件页面,就把它抽离了出去,也就不会有这种问题出现了 因此,结合 饿了么的tab 选项卡组件与弹框组件 2者相磨合。根据传入的异步组件,动态的来回切换弹框所显示的内容,每一个弹框页面对应的每个.vue文件。 能够使多层弹框相叠,随便套娃也不会影响代码的优雅性。

核心代码/文件目录如下:(如需全部代码可执行clone项目查看)

1.先在全局挂载弹窗组件

2.dialogCom全局组件: ``

**如果需要每次来回切换弹窗时,其组件页面每次都渲染一次 可以将 v-show 换成 v-for --24段代码

vue 复制代码
```<!-- 
* 全局挂载的弹框组件
*-->
<template>
    <el-dialog :fullscreen="dailogInfo.dialogFullscreen" v-model="dialogVisbled" :width="dailogInfo.dialogWidth"
        @closed="dialogClose" :append-to-body="false" draggable>
        <template #header>
            <!-- svg图标可自行使用符合项目的风格 -->
            <svg-icon name="module" />
            {{ dailogInfo.dialogTitle }}
        </template>

        <el-tabs type="card" v-model="dialogActiveTag" @tab-remove="tabClose" @tab-click="handTabClick">
            <el-tab-pane v-for="(item, index) in dailogInfo.children" :key="index" :name="index" :closable="index != 0">
                <template #label>
                    <svg-icon :name="item.iconName" />
                    {{ item.topTagName }}
                </template>
            </el-tab-pane>
        </el-tabs>
        <div class="dialogContianer">
            <template v-for="(item, index) in dailogInfo.children" :key="index">
                <transition name="fade-transform" mode="out-in">
                    <div v-show="index === dialogActiveTag">
                        <component @toCloseDialog="dialogClose" :is="item.component" :conf="item.data" :id="index"
                            @removePage="componentRemove" />
                    </div>
                </transition>
            </template>
        </div>
    </el-dialog>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { computed, ref, watch } from "vue";
import usedialogStore from '@/store/modules/dialog'

const dialogStore = usedialogStore()
const { dialogVisbled, activeComponent } = storeToRefs(dialogStore)
const dailogInfo = computed(() => {
    return dialogStore.dialogOption
})
/** 每当push子级页面时当前选中项都默认选中push后的组件 */
watch(
    () => activeComponent.value,
    (val) => {
        if (val != null && val != undefined) {
            dialogActiveTag.value = activeComponent.value
            dialogStore.initActiveComponent()
        }
    }
)

const dialogActiveTag = ref(0)
/**关闭弹框 并 重置store的状态 */
const dialogClose = () => {
    dialogStore.$reset()
    dialogActiveTag.value = 0
}
/**移除当前组件,重置激活项 */
const tabClose = (index: number) => {
    dialogStore.removeItem(index)
    // 激活项===删除项 直接删除
    if (dialogActiveTag.value === index) {
        dialogActiveTag.value = index - 1
    }
    //激活项>删除项 则直接赋删除项
    if (dialogActiveTag.value > index) {
        dialogActiveTag.value = index;
    }
    if (!dailogInfo.value.children.length) {
        dialogClose()
    }
}
/**设置激活的组件 */
const handTabClick = (tab: any) => {
    dialogActiveTag.value = Number(tab.index);
}
/**子级组件关闭
 * @params {id} 关闭项
 * @params {isSaveFn :true 嵌套的子级页面操作了表单,需要通知父级页面刷新列表之类的操作" }
 */
const componentRemove = (id: number, isSaveFn: boolean) => {
    if (isSaveFn) {
        dailogInfo.value.children.forEach((item, index) => {
            if (index === id && item.collbackFn) {
                //回调-通知上一级页面
                dialogStore.collbackFnList[index]()
            }
        })
    }
    if (id == 0) dialogClose()
    else tabClose(id)
}
</script>
<style scoped>
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
    transition: all 0.5s;
}

.fade-transform-enter {
    opacity: 0;
    transform: translateX(-30px);
}

.fade-transform-leave-to {
    opacity: 0;
    transform: translateX(30px);
}
</style>
  1. store/modules/dialog(setup 风格的store/state)
js 复制代码
import { reactive, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { type dialogOptionsType, type childrensType } from '@/store/types';

const usedialogStore = defineStore('dialogStore', () => {
  const dialogVisbled = ref<boolean>(false);
  const activeComponent = ref(); //子级激活项
  const collbackFnList = ref({}); //保存表单信息后外界刷新列表数据的回调函数
  const initOptions = {
    dialogFullscreen: false, //是否全屏
    dialogWidth: '30%', //弹窗宽度
    dialogTitle: '', //标题
    children: [],
  }
  let dialogOption = reactive<dialogOptionsType>({
    ...initOptions
  });

  const initDialog = (options: dialogOptionsType) => {
    Object.assign(dialogOption, options);
    dialogVisbled.value = true;
  };

  const removeItem = (index: number) => {
    dialogOption.children.splice(index, 1);
  };
  /**在第一个弹框中再次新增一个同级的页面 */
  const addDialog = (childrenOption: childrensType) => {
    if (!dialogOption.dialogTitle) {
      return console.error('请确保是否含有初始弹框容器,再添加新的弹框页面');
    }

    // 此处使用名称作为唯一标识 若二次弹框的名称一致,可自定义id 属性区分
    const obj = dialogOption.children.find(
      (item) => item.topTagName === childrenOption.topTagName
    );
    activeComponent.value = dialogOption.children.length;
    if (obj) {
      //如果已经存在了某个页面 但是没有关闭  再次点击则不新增新页面 而是回到旧页面
      activeComponent.value = dialogOption.children.findIndex(
        (item) => item.topTagName === childrenOption.topTagName
      );
      return;
    }
    dialogOption.children.push(childrenOption);
  };

  /**为每个组件添加回调 */
  const addCollBackFn = () => {
    dialogOption.children.forEach((item,index) => {
      if (
        item.collbackFn
      ) {
        collbackFnList.value[index] = item.collbackFn;
      }
    });
  };
  /**重置子级激活项 */
  const initActiveComponent = () => {
    activeComponent.value = null;
  };
  /**监听组件列表是否被移除/添加:以便收集回调 */
  watch(
    () => dialogOption.children,
    () => {
      addCollBackFn();
    },
    {
      deep:true,
    }
)
  return {
    dialogVisbled,
    dialogOption,
    activeComponent,
    collbackFnList,
    removeItem,
    initDialog,
    addDialog,
    initActiveComponent,
    $reset: () => {
      //重写重置方法
      dialogVisbled.value = false;
      Object.assign(dialogOption,initOptions)
      activeComponent.value = null;
      collbackFnList.value = {};
    },
  };
});

export default usedialogStore;
  1. 封装对应的 hooks:
js 复制代码
import usedialogStore from '@/store/modules/dialog';
import { defineAsyncComponent, shallowRef } from 'vue';
import { type dialogOptionsType, type childrensType } from '@/store/types';
import { AsyncComponentLoader } from 'vue';

const dialogStore = usedialogStore();

export const useDialogHooks = () => {
  const initDialog = (options: dialogOptionsType) => {
    const { children } = options;
    children.forEach((item) => {
      item.component = shallowRef(
        defineAsyncComponent(item.component as AsyncComponentLoader)
      );
    });
    dialogStore.initDialog(options);
  };

  const addDialog = (options: childrensType) => {
    options.component = shallowRef(
      defineAsyncComponent(options.component as AsyncComponentLoader)
    );
    dialogStore.addDialog(options);
  };
  return {
    initDialog,
    addDialog,
  };
};

export default useDialogHooks;

5.调用对应的hooks 传入基础配置

js 复制代码
import useDialogHooks from '@/hooks/dialog'

const { initDialog } = useDialogHooks()
//打开初始弹窗 initDialog 只需调用一次 往后每次新增弹窗
都调用addDialog方法
const openDialog = () => {
    initDialog({
        dialogFullscreen: false,
        dialogWidth: '50%',
        dialogTitle: '明细',
        children: [{
            topTagName: "新增",
            iconName: "form",
            // detailPage1 就是这次要打开的弹窗内容
            component: () => import("@/views/detailPage1.vue"),
            data: {
                params: {
                    aa: 11,
                    bb: 22,
                },
                edit: 'add'
            },
            collbackFn: () => { //弹框内部组件 保存成功后的回调函数
                console.log("当弹窗任务结束后,调用父页面的回掉函数。(比如我新增完成了需要刷新列表页面)--Home");
            }
        }],
    })
}

6.在原有的弹窗新增一个弹窗

js 复制代码
import useDialogHooks from '@/hooks/dialog'
const { addDialog } = useDialogHooks()

const props = defineProps(['conf', 'id'])
const emit = defineEmits(['removePage'])

const openDialog = () => {
    addDialog({
        topTagName: '第二个明细内容',
        iconName: "form",
        component: () => import("@/views/detailPage2.vue"),
        data: {
        },
        collbackFn: () => { //弹框内部组件 保存成功后的回调函数
            console.log("当弹窗任务结束后,调用父页面的回掉函数。(比如我新增完成了需要刷新列表页面)--detailPage1");
        }
    })
}
console.log(props.conf) //上一层组件的传递数据

//保存表单/会回调上层组件的回调方法
const saveInfo = () => {
    emit('removePage', props.id, true)
}

7.最后: 虽然没有使用无限套娃的方式不断往body嵌套弹窗, 并不断增加层级。 而是采用组件 平铺的方式,会比原来的无限嵌套更加清晰简洁,且界面也会比较"人性化"
每个新的弹窗相当于一个.vue 文件, 上层父容器 所在的 组件 也就不需要 额外维护一个visible变量了,对比原来实在是优雅很多。。。

q

相关推荐
世俗ˊ18 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92118 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_23 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人32 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript