当标签导航太多时,超出页面宽度时,可以左右横向滑动
4-1 添加scrollbar组件
element.ts中导入el-scrollbar
创建ScrollPanel组件
src/layout/components/TagsView/ScrollPanel.vue
<template><el-scrollbarwrap-class="scroll-wrapper"><slot /></el-scrollbar></template><script>export default {name: 'ScrollPanel'}</script><style lang="scss">.scroll-wrapper {position: relative;width: 100%;white-space: nowrap;}</style>
4-2 修改tagsview
用scrollpanel组件包裹tagsview组件

这里样式简单做了个修改
src/layout/components/TagsView/index.vue
<template><div class="tags-view-container"><scroll-panel><div class="tags-view-wrapper"><router-linkclass="tags-view-item":class="{active: isActive(tag)}"v-for="(tag, index) in visitedTags":key="index":to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"tag="span">{{ tag.title }}<!-- affix固定的路由tag是无法删除 --><spanv-if="!isAffix(tag)"class="el-icon-close"@click.prevent.stop="closeSelectedTag(tag)"></span></router-link></div></scroll-panel></div></template><script lang="ts">import path from 'path'import { defineComponent, computed, watch, onMounted } from 'vue'import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'import { useStore } from '@/store'import { RouteLocationWithFullPath } from '@/store/modules/tagsView'import { routes } from '@/router'import ScrollPanel from './ScrollPanel.vue'export default defineComponent({name: 'TagsView',components: {ScrollPanel},setup() {const store = useStore()const router = useRouter()const route = useRoute()// 可显示的tags viewconst visitedTags = computed(() => store.state.tagsView.visitedViews)// 从路由表中过滤出要affixed tagviewsconst fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {let tags: RouteLocationWithFullPath[] = []routes.forEach(route => {if (route.meta && route.meta.affix) {// 把路由路径解析成完整路径,路由可能是相对路径const tagPath = path.resolve(basePath, route.path)tags.push({name: route.name,path: tagPath,fullPath: tagPath,meta: { ...route.meta }} as RouteLocationWithFullPath)}// 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)if (route.children) {const childTags = fillterAffixTags(route.children, route.path)if (childTags.length) {tags = [...tags, ...childTags]}}})return tags}// 初始添加affix的tagconst initTags = () => {const affixTags = fillterAffixTags(routes)for (const tag of affixTags) {if (tag.name) {store.dispatch('tagsView/addVisitedView', tag)}}}// 添加tagconst addTags = () => {const { name } = routeif (name) {store.dispatch('tagsView/addView', route)}}// 路径发生变化追加tags viewwatch(() => route.path, () => {addTags()})// 最近当前router到tags viewonMounted(() => {initTags()addTags()})// 当前是否是激活的tagconst isActive = (tag: RouteRecordRaw) => {return tag.path === route.path}// 让删除后tags view集合中最后一个为选中状态const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {// 得到集合中最后一个项tag view 可能没有const lastView = visitedViews[visitedViews.length - 1]if (lastView) {router.push(lastView.fullPath as string)} else { // 集合中都没有tag view时// 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)if (view.name === 'Dashboard') {router.replace({ path: '/redirect' + view.fullPath as string })} else {// tag都没有了 删除的也不是Dashboard 只能跳转首页router.push('/')}}}// 关闭当前右键的tag路由const closeSelectedTag = (view: RouteLocationWithFullPath) => {// 关掉并移除viewstore.dispatch('tagsView/delView', view).then(() => {// 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态if (isActive(view)) {toLastView(visitedTags.value, view)}})}// 是否是始终固定在tagsview上的tagconst isAffix = (tag: RouteLocationWithFullPath) => {return tag.meta && tag.meta.affix}return {visitedTags,isActive,closeSelectedTag,isAffix}}})</script><style lang="scss" scoped>.tags-view-container {height: 34px;background: #fff;border-bottom: 1px solid #d8dce5;box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);overflow: hidden;.tags-view-wrapper {.tags-view-item {display: inline-block;height: 26px;line-height: 26px;border: 1px solid #d8dce5;background: #fff;color: #495060;padding: 0 8px;box-sizing: border-box;font-size: 12px;margin-left: 5px;margin-top: 4px;&:first-of-type {margin-left: 15px;}&:last-of-type {margin-right: 15px;}&.active {background-color: #42b983;color: #fff;border-color: #42b983;&::before {position: relative;display: inline-block;content: '';width: 8px;height: 8px;border-radius: 50%;margin-right: 5px;background: #fff;}}}}}</style><style lang="scss">.tags-view-container {.el-icon-close {width: 16px;height: 16px;position: relative;left: 2px;border-radius: 50%;text-align: center;transition: all .3s cubic-bezier(.645, .045, .355, 1);transform-origin: 100% 50%;&:before {transform: scale(.6);display: inline-block;vertical-align: -1px;}&:hover {background-color: #b4bccc;color: #fff;}}}</style>
4-3 样式调整

src/layout/index.vue
<template><div class="app-wrapper"><div class="sidebar-container"><Sidebar /></div><div class="main-container"><div class="header"><navbar /><tags-view /></div><!-- AppMain router-view --><app-main /></div></div></template><script lang="ts">import { defineComponent } from 'vue'import Sidebar from './components/Sidebar/index.vue'import AppMain from './components/AppMain.vue'import Navbar from './components/Navbar.vue'import TagsView from './components/TagsView/index.vue'export default defineComponent({components: {Sidebar,AppMain,Navbar,TagsView}})</script><style lang="scss" scoped>.app-wrapper {display: flex;width: 100%;height: 100%;.main-container {flex: 1;display: flex;flex-direction: column;overflow: hidden;.app-main {/* 50= navbar 50 如果有tagsview + 34 */min-height: calc(100vh - 84px);}}}</style>
4-4 注意使用标签导航
使用标签导航的路由 必须要name属性 因为方便我们根据 name进行路由筛选和缓存keep-alive
本节参考源码
https://gitee.com/brolly/vue3-element-admin/commit/59741362e40d74bd4834f752ee61752d165b2e54

