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>
- 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;
- 封装对应的 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