效果图
2-1 创建TagsView组件
src/layout/components/TagsView/index.vue
<template><div class="tags-view-container"><div class="tags-view-wrapper"><!-- 一个个tag view就是router-link --><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.meta.title }}<spanclass="el-icon-close"@click.prevent.stop="closeSelectedTag(tag)"></span></router-link></div></div></template><script lang="ts">import { defineComponent, computed, watch, onMounted } from 'vue'import { useRoute, RouteRecordRaw } from 'vue-router'import { useStore } from '@/store'export default defineComponent({name: 'TagsView',setup() {const store = useStore()const route = useRoute()// 从store里获取 可显示的tags viewconst visitedTags = computed(() => store.state.tagsView.visitedViews)// 添加tagconst addTags = () => {const { name } = routeif (name) {store.dispatch('tagsView/addView', route)}}// 路径发生变化追加tags viewwatch(() => route.path, () => {addTags()})// 最近当前router到tags viewonMounted(() => {addTags()})// 是否是当前应该激活的tagconst isActive = (tag: RouteRecordRaw) => {return tag.path === route.path}// 关闭当前右键的tag路由const closeSelectedTag = (view: RouteRecordRaw) => {store.dispatch('tagsView/delView', view)}return {visitedTags,isActive,closeSelectedTag}}})</script><style lang="scss" scoped>.tags-view-container {width: 100%;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);.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>
2-2 定义store
定义tagsView module
src/store/modules/tagsView.ts
import { Module, ActionTree, MutationTree } from 'vuex'import { RouteRecordRaw } from 'vue-router'import { IRootState } from '@/store'export interface ITagsViewState {// 存放当前显示的tags view集合visitedViews: RouteRecordRaw[];}// 定义mutationsconst mutations: MutationTree<ITagsViewState> = {// 添加可显示tags viewADD_VISITED_VIEW(state, view) {// 过滤去重if (state.visitedViews.some(v => v.path === view.path)) return// 没有title时处理state.visitedViews.push(Object.assign({}, view, {title: view.meta.title || 'tag-name'}))},DEL_VISITED_VIEW(state, view) {const i = state.visitedViews.indexOf(view)if (i > -1) {state.visitedViews.splice(i, 1)}}}// 定义actionsconst actions: ActionTree<ITagsViewState, IRootState> = {// 添加tags viewaddView({ dispatch }, view: RouteRecordRaw) {dispatch('addVisitedView', view)},// 添加可显示的tags view 添加前commit里需要进行去重过滤addVisitedView({ commit }, view: RouteRecordRaw) {commit('ADD_VISITED_VIEW', view)},// 删除tags viewdelView({ dispatch }, view: RouteRecordRaw) {dispatch('delVisitedView', view)},// 从可显示的集合中 删除tags viewdelVisitedView({ commit }, view: RouteRecordRaw) {commit('DEL_VISITED_VIEW', view)}}const tagsView: Module<ITagsViewState, IRootState> = {namespaced: true,state: {visitedViews: []},mutations,actions}export default tagsView
修改store导入module


src/store/index.ts
import { InjectionKey } from 'vue'import { createStore, Store, useStore as baseUseStore } from 'vuex'import createPersistedState from 'vuex-persistedstate'import app, { IAppState } from '@/store/modules/app'import tagsView, { ITagsViewState } from '@/store/modules/tagsView'import getters from './getters'// 模块声明在根状态下export interface IRootState {app: IAppState;tagsView: ITagsViewState;}// 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage// eslint-disable-next-line symbol-descriptionexport const key: InjectionKey<Store<IRootState>> = Symbol()// 对于getters在组件使用时没有类型提示// 有人提交了pr #1896 为getters创建泛型 应该还未发布// https://github.com/vuejs/vuex/pull/1896// 代码pr内容详情// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce// vuex store持久化 默认使用localstorage持久化const persisteAppState = createPersistedState({storage: window.sessionStorage, // 指定storage 也可自定义key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖// paths: ['app'] // 针对app这个模块持久化// 只针对app模块下sidebar.opened状态持久化paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径})export default createStore<IRootState>({plugins: [persisteAppState],getters,modules: {app,tagsView}})// 定义自己的 `useStore` 组合式函数// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-typesexport function useStore () {return baseUseStore(key)}// vuex持久化 vuex-persistedstate文档说明// https://www.npmjs.com/package/vuex-persistedstate
修改navbar 样式

<template><div class="navbar"><hambuger @toggleClick="toggleSidebar" :is-active="sidebar.opened"/><breadcrumb /><div class="right-menu"><!-- 全屏 --><screenfull id="screefull" class="right-menu-item hover-effect" /><!-- element组件size切换 --><el-tooltip content="Global Size" effect="dark" placement="bottom"><size-select class="right-menu-item hover-effect" /></el-tooltip><!-- 用户头像 --><avatar /></div></div></template><script lang="ts">import { defineComponent, computed } from 'vue'import Breadcrumb from '@/components/Breadcrumb/index.vue'import Hambuger from '@/components/Hambuger/index.vue'import { useStore } from '@/store/index'import Screenfull from '@/components/Screenfull/index.vue'import SizeSelect from '@/components/SizeSelect/index.vue'import Avatar from './avatar/index.vue'export default defineComponent({name: 'Navbar',components: {Breadcrumb,Hambuger,Screenfull,SizeSelect,Avatar},setup() {// 使用我们自定义的useStore 具备类型提示// store.state.app.sidebar 对于getters里的属性没有类型提示const store = useStore()const toggleSidebar = () => {store.dispatch('app/toggleSidebar')}// 从getters中获取sidebarconst sidebar = computed(() => store.getters.sidebar)return {toggleSidebar,sidebar}}})</script><style lang="scss">.navbar {display: flex;background: #fff;border-bottom: 1px solid rgba(0, 21, 41, .08);box-shadow: 0 1px 4px rgba(0, 21, 41, .08);.right-menu {flex: 1;display: flex;align-items: center;justify-content: flex-end;padding-right: 15px;&-item {padding: 0 8px;font-size: 18px;color: #5a5e66;vertical-align: text-bottom;&.hover-effect {cursor: pointer;transition: background .3s;&:hover {background: rgba(0, 0, 0, .025);}}}}}</style>
2-3 导入到layout组件

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;.app-main {/* 50= navbar 50 如果有tagsview + 34 */min-height: calc(100vh - 84px);}}}</style>
本节参考源码
https://gitee.com/brolly/vue3-element-admin/commit/7bd90896ab32f9295d3f1864fba75952a47d1435
