# 多标签页
在后台管理系统中,多标签页是一个很常见的功能,用来记住用户已打开的页面并进行缓存以帮助用户快速操作。
1、实现原理
简易多标签页样式和交互效果主要借助UI框架的Tab切换组件,实现原理很简单,通过一个路由信息列表来渲染和维护标签列表,通过组件内的路由导航守卫,监听路由的更改,如果此路由是缓存路由,通过路由表中的keepAlive字段判断,并且不在标签页中就把当前的路由信息添加进去,如果存在就调到此标签,并通过路由元信息中的 path 进行点击切换跳转,删除时如果删除当前页就重新跳到第一个。
2、页面缓存
在用户打开页面时把页面添加到标签页中,然后对所有的标签页进行缓存,我们借助vue的keep-alive进行实现,但是有一个问题,标签页页面缓存后关闭页面再次打开还是被缓存,显然,这是不符合的,我们需要在标签页中切换时是缓存的,当关闭页面重新打开后缓存被清除,再重新缓存,这也是标签页最基础的功能。
如果要实现上述缓存功能,我们需要使用keep-alive的include方法结合vuex来实现,打开页面时把路由name添加到vuex中,关闭标签页时删除,keep-alive的include方法就能识别路由的name实现动态缓存。
这使用keep-alive缓存一级路由时是没有问题的,但是如果缓存多级路由就会出现问题,我们渲染二级时使用router-view来显示子路由,每次切换一级路由时,因为router-view没有名称所以切换时被刷新,导致router-view下面的子路由也无法被缓存,解决这个只需要给router-view组件添加name,然后在一级路由include缓存列表里加上这个名字,不能加到vuex中会失效。
MulTab组件实现:
<template>
<div class="tab">
<a-tabs v-model:active-key="activeKey" type="card-gutter" :editable="true" @tab-click="selectRoute" @delete="deleteRoute" >
<a-tab-pane v-for="page in state.pageList" :key="page.path" :title="(page?.meta?.title as string)" :closable="state.pageList.length > 1"></a-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
/**
* 简易多标签页
* @desc 头部多标签页展示
* 默认在路由中开启keepAlive属性就会缓存组件,为了不让页面一直被缓存,重新打开后还是被缓存,
* 通过cacheList配合include属性控制让其再关闭标签页后就不在缓存,重新打开可以刷新
* @author changz
* @example 调用示例
* <MultiTab></MultiTab>
* */
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute, onBeforeRouteUpdate, type RouteLocationNormalizedLoaded } from 'vue-router'
import { usePublicStore } from '@/stores/modules/public'
const publicStore = usePublicStore()
const router = useRouter()
const route = useRoute()
const activeKey = ref('') // 被选中tab
interface State {
pageList: RouteLocationNormalizedLoaded[]
}
const state = reactive<State>({
pageList: []
})
onMounted(() => {
const deepRoute = Object.assign({}, route)
getCurrentTabByRoute(deepRoute)
})
// 监听当前路由更改
onBeforeRouteUpdate((to, from, next) => {
getCurrentTabByRoute(to)
next()
})
// 获取当前Tab
const getCurrentTabByRoute = (route: RouteLocationNormalizedLoaded) => {
activeKey.value = route.path
const pathList = state.pageList.map(item => item.path)
if (!pathList.includes(route.path)) {
state.pageList.push(route)
setTabsCache()
}
}
// 删除Tab
const deleteRoute = (key: string | number) => {
state.pageList = state.pageList.filter(page => page.path !== key)
setTabsCache()
// 删除当前页时自动跳转到最后一个
if (activeKey.value === key) {
const pathList = state.pageList.map(item => item.path)
activeKey.value = pathList[pathList.length - 1]
selectRoute()
}
}
// 设置缓存
const setTabsCache = () => {
const cacheList = state.pageList.filter(item => item.meta.keepAlive).map(item => item.name)
publicStore.cacheList = cacheList as string[]
}
const selectRoute = () => {
router.push({ path: activeKey.value })
}
</script>
<style lang="less" scoped>
.tab {
width: 100%;
padding: 8px 16px;
background-color: #ffffff;
border-top: 1px solid #f2f2f2;
border-bottom: 1px solid #f1f1f1;
}
// 去除tab默认样式
:deep(.arco-tabs-nav::before) {
display: none;
}
:deep(.arco-tabs-content) {
display: none;
}
:deep(.arco-tabs-tab-active) {
border-bottom: 1px solid var(--color-neutral-3);
}
:deep(.arco-tabs-nav-type-card-gutter) {
.arco-tabs-tab-active {
border-bottom: 1px solid var(--color-neutral-3);
}
.arco-tabs-tab-active:hover {
border-bottom: 1px solid var(--color-neutral-3);
}
}
</style>
路由表:
import { shallowRef } from 'vue'
import BasicLayout from '@/layouts/BasicLayout.vue'
import RouteView from '@/layouts/RouteView.vue'
import { type RouteRecordRaw } from 'vue-router'
/**
* @desc 权限路由表
* 路由分为需要登录的不需要登录的
* 不需要登录的放在基础路由里,如登录、注册、活动页,需要登录的放在权限路由里,
* 白名单页面放在基础路由里,如活动页,通过meta字段isWhite区分登录注册页
* 需要登录的又分为需要权限控制和不需要权限控制的,通过meta字段isAuth进行判断需不需要权限判断
* 在导航栏显示的路由统一放在根路由下走基础布局
* 不在导航栏显示的可以放在根路由外面走其他布局,比如个人中心
* 或者在根路由走基础布局,通过hidden自动控制,比如详情页
* keepAlive是否缓存该组件,缓存必须在每一层router-view加keep-alive才会生效
* permission为权限id,全局必须保持唯一
* */
const HomeComponent = () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
const Workbench = () => import(/* webpackChunkName: 'workbench' */ '@/views/workbench/index.vue')
const MapCharts = () => import(/* webpackChunkName: 'map-charts' */ '@/views/map-charts/index.vue')
// 权限路由
export const asyncRouterMap: RouteRecordRaw[] = [
{
path: '/',
name: 'Index',
component: shallowRef(BasicLayout),
meta: { hidden: false, keepAlive: false, isAuth: false },
children: [
{
path: '/home',
name: 'Home',
component: HomeComponent,
meta: { title: '主页', icon: 'icon-apps', hidden: false, keepAlive: true, isAuth: true, permission: 'home' }
},
{
path: '/dashboard',
name: 'Dashboard',
component: shallowRef(RouteView),
meta: { title: '仪表盘', icon: 'icon-dashboard', hidden: false, keepAlive: true, isAuth: true, permission: 'dashboard' },
children: [
{
path: '/workbench',
name: 'Workbench',
component: Workbench,
meta: { title: '工作台', icon: 'icon-common', hidden: false, keepAlive: false, isAuth: true, permission: 'workbench' }
},
{
path: '/map-charts',
name: 'MapCharts',
component: MapCharts,
meta: { title: '实时监控', icon: 'icon-computer', hidden: false, keepAlive: true, isAuth: true, permission: 'map_charts' }
}
]
}
]
}
]
router-view组件:
<template>
<!-- 每个路由只缓存当前的子路由 -->
<RouterView v-slot="{ Component }">
<KeepAlive :include="publicStore.cacheList">
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>
<script setup lang="ts" name="RouteView">
/**
* @desc 子路由模板
* 用于包含子路由但没有页面的组件上,只用来渲染子组件内容
* 设置name用来防止在父级路由添加缓存时刷新缓存页
* 一般多层级菜单缓存失效都是因为RouterView模板是动态的,切换时直接刷新了无法缓存
* */
import { usePublicStore } from '@/stores/modules/public'
const publicStore = usePublicStore()
</script>
一级路由基础布局组件:
<template>
<a-spin dot :loading="publicStore.pageLoad" :style="{ width: '100%' }" tip="加载中...">
<a-layout class="basic">
<!-- 侧边导航栏 start -->
<a-layout-sider>
</a-layout-sider>
<!-- 侧边导航栏 end -->
<a-layout>
<!-- 头部 start -->
<a-layout-header>
<GlobalHeader @COLLAPSE_EVENT="onCollapse"></GlobalHeader>
</a-layout-header>
<!-- 头部 end -->
<a-layout class="basic-layout">
<MultiTab></MultiTab>
<!-- <Breadcrumb></Breadcrumb> -->
<div class="layout-content">
<!-- 路由缓存,只针对当前子路由进行缓存 -->
<!-- RouteView 子路由模板名称,防止刷新子路由 -->
<RouterView v-slot="{ Component }">
<!-- <Transition name="fade" mode="out-in" appear> -->
<KeepAlive :include="['RouteView', ...publicStore.cacheList]">
<component :is="Component"></component>
</KeepAlive>
<!-- </Transition> -->
</RouterView>
</div>
<!-- <a-layout-footer>Footer</a-layout-footer> -->
</a-layout>
</a-layout>
</a-layout>
</a-spin>
</template>
<script setup lang="ts" name="BasicLayout">
/**
* @description 侧栏菜单布局
* */
import { ref, reactive } from 'vue'
import { usePublicStore } from '@/stores/modules/public'
import MultiTab from '@/components/MultiTab.vue'
const publicStore = usePublicStore()
</script>