经过上一篇文档ant-design-vue3.x应用框架页面开发的学习,我们准备了一个基本的首页框架页面,只是这个页面还缺少多页签的支持,本编文档将继续完善框架,添加多页签支持。
要支持多页签,需要解决这个问题:ant-design-vue的Tabs组件的内容部分仅支持输出文本内容(如果理解有误,请批评指正),不支持嵌套组件,而且vue也不建议采用iframe嵌入其他页面。因此我们采用只使用页签头而隐藏页签体,用<router-view>替换页签体,在页签切换时配合使用router编程式路由,以达到一个页签对应一个路由的目的。同时为了缓存组件,使用<keep-alive>标签缓存组件的输出,配合使用include属性,在页签关闭后删除缓存。
这就是加入多页签支持后要解决的核心问题,其他细节都在代码中进行了说明。
1、创建Tabs.vue
文件内容如下:
<script setup>
import { ref, defineProps } from 'vue';
import { useRouter } from 'vue-router';
const emit = defineEmits(["removeCachePageId", "addCachePageId"]);//定义可调用的父组件方法名
const activedKeyIdArray = [];//存放曾经激活tab页的key,用于删除时计算应该激活的tab页
const panes = ref([{
key: '0',
title: '首页',
closable: false,
path: '/'
}]); //页签数组
const activeKey = ref(panes.value[0].key);//当前激活的tab页的key
activedKeyIdArray.push(panes.value[0].key);
emit('addCachePageId', "Welcome"); //缓存首页组件名称
const $router = useRouter(); //全局路由对象
const add = (paneObj) => { //添加tab页签,选中侧边栏菜单项时调用
$router.push(paneObj.path);
let oneTab = getTabByKey(paneObj.key);
if (oneTab == null) { //页签不存在,创建页签
emit('addCachePageId', paneObj.path.substring(1));//调用父组件函数,加入组件缓存列表
setActiveedKeyId(paneObj.key);
panes.value.push(paneObj);
} else {
setActiveedKeyId(oneTab.key);
}
}
const remove = targetKey => { //关闭页签函数
/*
let lastIndex = 0;
panes.value.forEach((pane, i) => {
if (pane.key === targetKey) {
lastIndex = i - 1;
}
});*/
emit('removeCachePageId', getTabByKey(targetKey).path.substring(1));//删除组件列表,要求组件名称和和路由path一致(例如path=/page1,组件名称由应该时page1。也可以使用路由定义中的name,只要这个值和组件的<script setup name="xxx">标签中name的值保持一致即可)
panes.value = panes.value.filter(pane => pane.key !== targetKey);
if (activeKey.value === targetKey) {//关闭的是当前激活的页页签,找到最近一次激活的页签并激活
let loopFlag = true;
while(loopFlag) {
let keyId = activedKeyIdArray.pop();
if (keyId != targetKey) {
let oneTab = getTabByKey(keyId)
if (oneTab != null) {
setActiveedKeyId(oneTab.key);
$router.push(oneTab.path);
break;
}
}
}
}
/*
if (panes.value.length && activeKey.value === targetKey) {
if (lastIndex >= 0) {
activeKey.value = panes.value[lastIndex].key;
activedKeyIdArray.push(panes.value[lastIndex].key);
$router.push(panes.value[lastIndex].path);
} else {
activeKey.value = panes.value[0].key;
activedKeyIdArray.push(panes.value[0].key);
$router.push(panes.value[0].path);
}
}*/
};
const onEdit = (targetKey, action) => {
if (action === 'add') {
add();
} else {
remove(targetKey);
}
};
const selectedTab = (activeKeyId) => { //选择某页签时激活该页签
let oneTab = getTabByKey(activeKeyId);
if (oneTab != null) {
setActiveedKeyId(oneTab.key);
$router.push(oneTab.path);
}
};
const getTabByKey = (keyId) => { //根据key获得页签对象
let rtnValue = null;
for (let index = 0;index < panes.value.length;index++) {
if (panes.value[index].key == keyId) {
rtnValue = panes.value[index];
break;
}
}
return rtnValue;
};
const setActiveedKeyId = (keyId) => {
activeKey.value = keyId;
activedKeyIdArray.push(keyId);
};
defineExpose({ add });//定义可被父组件调用的组件
</script>
<template>
<a-tabs v-model:activeKey="activeKey" type="editable-card" @edit="onEdit" :hideAdd="true" @change="selectedTab">
<a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable">
</a-tab-pane>
</a-tabs>
</template>
<style scoped>
:deep(div.ant-tabs-nav-list div.ant-tabs-tab) {
}
:deep(div.ant-tabs-content-holder) {
display: none;/*隐藏标签页内容,标签页内容由<router-view>内容替代*/
}
:deep(div.ant-tabs-nav) {
padding: 2px 2px 0 2px;
margin: 0;
}
/*
:deep(div.ant-tabs-tab) {
background-color: #ededed;
}*/
</style>
对于源码中部分跨组件共享的数据,比如页签定义数组panes、保存页签激活顺序的栈activedKeyIdArray等,可以采用vuex保存,这样就可以避免跨组件调用函数或共享数据。关于vuex的使用是后话。
2、修改Layout.vue
文件内容如下:
<script setup>
import { ref } from 'vue';
import sysmenu from './SysMenu.vue';
import tabs from './Tabs.vue';
const cacheNamesList = ref([]); //需要缓存的组件的name集合,与组件的<script set name="xxx">中的name对应
const isCollapsed = ref(false);
const switchCollapsed = () => {
isCollapsed.value = !isCollapsed.value;
};
const addTab = ref();
function addTabByMenu(tabInfo) {
addTab.value.add(tabInfo);
}
function removeCachePageId(removedId) { //页签关闭前调用,删除需要缓存的组件名称
cacheNamesList.value = cacheNamesList.value.filter(pageId => pageId !== removedId);
}
function addCachePageId(addedId) { //添加页签前时调用,缓存对应组件名称
cacheNamesList.value.push(addedId);
}
</script>
<template>
<a-layout>
<a-layout-header><span style="color: white;">Header</span></a-layout-header>
<a-layout>
<a-layout-sider v-model:collapsed="isCollapsed">
<sysmenu @addTabByMenu="addTabByMenu"></sysmenu>
</a-layout-sider>
<a-layout-content>
<tabs ref="addTab" @removeCachePageId="removeCachePageId" @addCachePageId="addCachePageId"></tabs><!--自定义触发时间,以备子组件调用-->
<div class="mainDiv">
<router-view v-slot="{ Component }">
<keep-alive :include="cacheNamesList"><!--缓存组件的代码,注意与VUE2.x的区别-->
<component :is="Component"/>
</keep-alive>
</router-view>
</div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<style scoped>
section.ant-layout {
height: 100%;
background-color: white;
}
.mainDiv {
height: calc(100% - 42px);/*计算值,42px为顶部header的高度*/
overflow: auto;
padding: 0 2px;
}
</style>
3、修改SysMenu.vue
文件内容如下:
<script setup>
import { ref } from 'vue';
import { PieChartOutlined, MailOutlined } from '@ant-design/icons-vue';
const SubMenu = {
name: 'SubMenu',
props: {
menuInfo: {
type: Object,
default: () => ({}),
},
},
template: `
<a-sub-menu :key="menuInfo.key">
<template #icon><MailOutlined /></template>
<template #title>{{ menuInfo.title }}</template>
<template v-for="item in menuInfo.children" :key="item.key">
<template v-if="!item.children">
<a-menu-item :key="item.key">
<template #icon>
<PieChartOutlined />
</template>
{{ item.title }}
</a-menu-item>
</template>
<template v-else>
<sub-menu :menu-info="item" :key="item.key" />
</template>
</template>
</a-sub-menu>
`,
components: {
PieChartOutlined,
MailOutlined,
},
};
const list = [{
key: '0',
title: '首页',
path: '/'
}, {
key: '1',
title: '页面1',
path: '/page1'
}, {
key: '2',
title: '页面2',
path: '/page2'
}, {
key: '3',
title: '页面3',
path: '/page3'
}, {
key: '4',
title: '页面4',
path: '/page4'
}, {
key: '5',
title: '页面5',
path: '/page5'
}, {
key: '6',
title: '页面6',
path: '/page6'
}, {
key: '7',
title: '页面7',
path: '/page7'
}, {
key: '8',
title: '页面8',
path: '/page8'
}]; //菜单项对应路由及相关vue文件请自行增加
const selectedKeys = ref([]);//处于选中状态的节点key列表
const openKeys = ref([]);//处于展开状态的节点key列表
const emit = defineEmits(["addTabByMenu"]);
function selectedMenuNode(nodeInfo) {
for (let index = 0;index < list.length;index++) {
if (list[index].key == nodeInfo.key) {
emit('addTabByMenu', list[index]); //调用父组件,添加页签
break;
}
}
}
</script>
<template>
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" theme="dark" @select="selectedMenuNode">
<template v-for="item in list" :key="item.key">
<template v-if="!item.children">
<a-menu-item :key="item.key">
<template #icon>
<PieChartOutlined />
</template>
{{ item.title }}
</a-menu-item>
</template>
<template v-else>
<sub-menu :key="item.key" :menu-info="item" />
</template>
</template>
</a-menu>
</template>
<style scoped>
</style>
至此,多页签支持完成。这里需要注意,在<router-view>中嵌入的路由对一个的页面,建议页尽量在<template>标签后页用一个根标签包裹所有元素,否则可能在页签切换过程中,导致部分组件缓存错乱,造成组件缓存错乱。