通过右键选择 关闭所有、 关闭其他、 关闭当前、刷新 对于tag affix为true的固定tag是不允许关闭删除的
效果图
右键关闭所有
自动切换到dashboard因为它是固定tag

右键关闭其他
当前tag 以及 affix为true的tag是不能关闭,并且自动切换到当前右键的tag

关闭后
关闭当前右键选中tag
affix为true的tag是不能关闭的

关闭后
右键刷新
刷新后 input内容也没有了
2-1 修改tagsView组件
添加下拉菜单
需要使用 element dropdown组件,给每一个tag添加。
src/layout/components/TagsView/index.vue
添加右键事件
右键菜单关闭所有事件

定义枚举类型
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"><el-dropdowntrigger="contextmenu"@command="command => handleTagCommand(command, tag)"><span>{{ tag.meta.title }}<!-- affix固定的路由tag是无法删除 --><spanv-if="!isAffix(tag)"class="el-icon-close"@click.prevent.stop="closeSelectedTag(tag)"></span></span><template #dropdown><el-dropdown-menu><el-dropdown-item command="all">关闭所有</el-dropdown-item><el-dropdown-item command="other">关闭其他</el-dropdown-item><el-dropdown-item command="self">关闭</el-dropdown-item></el-dropdown-menu></template></el-dropdown></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'// 右键菜单enum TagCommandType {All = 'all',Other = 'other',Self = 'self',}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}// 右键菜单const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {switch (command) {case TagCommandType.All:handleCloseAllTag(view)}}const handleCloseAllTag = (view: RouteLocationWithFullPath) => {// 对于是affix的tag是不会被删除的store.dispatch('tagsView/delAllView').then(() => {// 关闭所有后 就让切换到剩下affix中最后一个tagtoLastView(visitedTags.value, view)})}return {visitedTags,isActive,closeSelectedTag,isAffix,handleTagCommand}}})</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;::v-deep {.el-dropdown {color: #fff;}}&::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>
修改store
我们的tag列表和缓存列表都在store 所有对于它们的增删改查需要 调用action mutaions来删除

src/store/modules/tagsView.ts
import { Module, ActionTree, MutationTree } from 'vuex'import { RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router'import { IRootState } from '@/store'// 携带fullPathexport interface RouteLocationWithFullPath extends RouteRecordNormalized {fullPath?: string;}export interface ITagsViewState {// 存放当前显示的tags view集合visitedViews: RouteLocationWithFullPath[];// 根据路由name缓存集合cachedViews: RouteRecordName[];}// 定义mutationsconst mutations: MutationTree<ITagsViewState> = {// 添加可显示tags viewADD_VISITED_VIEW(state, view) {// 过滤去重if (state.visitedViews.some(v => v.path === view.path)) return// 没有titles时处理state.visitedViews.push(Object.assign({}, view, {title: view.meta.title || 'tag-name'}))},// 如果路由meta.noCache没有 默认或为false代表进行缓存,为true不缓存// 默认缓存所有路由ADD_CACHED_VIEW(state, view) {// 只有路由有name才可缓存集合keep-alive inludes使用if (state.cachedViews.includes(view.name)) returnif (!view.meta.noCache) {state.cachedViews.push(view.name)}},DEL_VISITED_VIEW(state, view) {const i = state.visitedViews.indexOf(view)if (i > -1) {state.visitedViews.splice(i, 1)}},// 可删除指定的一个view缓存DEL_CACHED_VIEW(state, view) {const index = state.cachedViews.indexOf(view.name)index > -1 && state.cachedViews.splice(index, 1)},// 清空可显示列表DEL_ALL_VISITED_VIEWS(state) {// 对于affix为true的路由 tag view 是不能删除的const affixTags = state.visitedViews.filter(tag => tag.meta.affix)state.visitedViews = affixTags},// 清空缓存列表DEL_ALL_CACHED_VIEWS(state) {state.cachedViews = []}}// 定义actionsconst actions: ActionTree<ITagsViewState, IRootState> = {// 添加tags viewaddView({ dispatch }, view: RouteRecordRaw) {// 添加tag时也要判断该tag是否需要缓存dispatch('addVisitedView', view)dispatch('addCachedView', view)},// 添加可显示的tags view 添加前commit里需要进行去重过滤addVisitedView({ commit }, view: RouteRecordRaw) {commit('ADD_VISITED_VIEW', view)},// 添加可缓存的标签tagaddCachedView({ commit }, view: RouteRecordRaw) {commit('ADD_CACHED_VIEW', view)},// 删除指定tags viewdelView({ dispatch }, view: RouteRecordRaw) {return new Promise(resolve => {// 删除显示的路由tagdispatch('delVisitedView', view)// 删除缓存的路由dispatch('delCachedView', view)resolve(null)})},// 从可显示的集合中 删除tags viewdelVisitedView({ commit }, view: RouteRecordRaw) {commit('DEL_VISITED_VIEW', view)},// 从缓存列表删除指定tag viewdelCachedView({ commit }, view: RouteRecordRaw) {return new Promise(resolve => {commit('DEL_CACHED_VIEW', view)resolve(null)})},// 清空 可显示列表 和 缓存列表delAllView({ dispatch }) {return new Promise(resolve => {// 删除显示的路由tagdispatch('delAllVisitedView')// 删除缓存的路由dispatch('delAllCachedViews')resolve(null)})},// 清空可显示列表delAllVisitedView({ commit }) {commit('DEL_ALL_VISITED_VIEWS')},// 清空缓存列表delAllCachedViews({ commit }) {commit('DEL_ALL_CACHED_VIEWS')}}const tagsView: Module<ITagsViewState, IRootState> = {namespaced: true,state: {visitedViews: [],cachedViews: []},mutations,actions}export default tagsView
2-2 右键关闭其他和关闭
右键关闭其他 除了 affix tag 和 当前右键tag 右键关闭 是处理affix tag下拉菜单不会显示此项
修改tagsview
src/layout/components/TagsView/index.vue

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"><el-dropdowntrigger="contextmenu"@command="command => handleTagCommand(command, tag)"><span>{{ tag.meta.title }}<!-- affix固定的路由tag是无法删除 --><spanv-if="!isAffix(tag)"class="el-icon-close"@click.prevent.stop="closeSelectedTag(tag)"></span></span><template #dropdown><el-dropdown-menu><el-dropdown-item command="all">关闭所有</el-dropdown-item><el-dropdown-item command="other">关闭其他</el-dropdown-item><el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item></el-dropdown-menu></template></el-dropdown></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'// 右键菜单enum TagCommandType {All = 'all',Other = 'other',Self = 'self',}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}// 右键菜单const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {switch (command) {case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的handleCloseAllTag(view)breakcase TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的taghandleCloseOtherTag(view)breakcase TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项closeSelectedTag(view)}}// 删除所有tag 除了affix为true的const handleCloseAllTag = (view: RouteLocationWithFullPath) => {// 对于是affix的tag是不会被删除的store.dispatch('tagsView/delAllView').then(() => {// 关闭所有后 就让切换到剩下affix中最后一个tagtoLastView(visitedTags.value, view)})}// 删除其他tag 除了当前右键的tagconst handleCloseOtherTag = (view: RouteLocationWithFullPath) => {store.dispatch('tagsView/delOthersViews', view).then(() => {if (!isActive(view)) { // 删除其他tag后 让该view路由激活router.push(view.path)}})}return {visitedTags,isActive,closeSelectedTag,isAffix,handleTagCommand}}})</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;::v-deep {.el-dropdown {color: #fff;}}&::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;vertical-align: 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: -3px;}&:hover {background-color: #b4bccc;color: #fff;}}}</style>
store里添加相关action和mutation

src/store/modules/tagsView.ts
import { Module, ActionTree, MutationTree } from 'vuex'import { RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router'import { IRootState } from '@/store'// 携带fullPathexport interface RouteLocationWithFullPath extends RouteRecordNormalized {fullPath?: string;}export interface ITagsViewState {// 存放当前显示的tags view集合visitedViews: RouteLocationWithFullPath[];// 根据路由name缓存集合cachedViews: RouteRecordName[];}// 定义mutationsconst mutations: MutationTree<ITagsViewState> = {// 添加可显示tags viewADD_VISITED_VIEW(state, view) {// 过滤去重if (state.visitedViews.some(v => v.path === view.path)) return// 没有titles时处理state.visitedViews.push(Object.assign({}, view, {title: view.meta.title || 'tag-name'}))},// 如果路由meta.noCache没有 默认或为false代表进行缓存,为true不缓存// 默认缓存所有路由ADD_CACHED_VIEW(state, view) {// 只有路由有name才可缓存集合keep-alive inludes使用if (state.cachedViews.includes(view.name)) returnif (!view.meta.noCache) {state.cachedViews.push(view.name)}},DEL_VISITED_VIEW(state, view) {const i = state.visitedViews.indexOf(view)if (i > -1) {state.visitedViews.splice(i, 1)}},// 可删除指定的一个view缓存DEL_CACHED_VIEW(state, view) {const index = state.cachedViews.indexOf(view.name)index > -1 && state.cachedViews.splice(index, 1)},// 清空可显示列表DEL_ALL_VISITED_VIEWS(state) {// 对于affix为true的路由 tag view 是不能删除的const affixTags = state.visitedViews.filter(tag => tag.meta.affix)state.visitedViews = affixTags},// 清空缓存列表DEL_ALL_CACHED_VIEWS(state) {state.cachedViews = []},// 删除标签导航其他可显示tag 除了 affix为true 以及当前右键选中的viewDEL_OTHERS_VISITED_VIEWS(state, view: RouteRecordRaw) {state.visitedViews = state.visitedViews.filter(tag => tag.meta.affix || (tag.path === view.path))},// 删除缓存列表里其他tag 除了当前右键选中的viewDEL_OTHERS_CACHED_VIEWS(state, view: RouteRecordRaw) {state.cachedViews = state.cachedViews.filter(name => name !== view.name)}}// 定义actionsconst actions: ActionTree<ITagsViewState, IRootState> = {// 添加tags viewaddView({ dispatch }, view: RouteRecordRaw) {// 添加tag时也要判断该tag是否需要缓存dispatch('addVisitedView', view)dispatch('addCachedView', view)},// 添加可显示的tags view 添加前commit里需要进行去重过滤addVisitedView({ commit }, view: RouteRecordRaw) {commit('ADD_VISITED_VIEW', view)},// 添加可缓存的标签tagaddCachedView({ commit }, view: RouteRecordRaw) {commit('ADD_CACHED_VIEW', view)},// 删除指定tags viewdelView({ dispatch }, view: RouteRecordRaw) {return new Promise(resolve => {// 删除显示的路由tagdispatch('delVisitedView', view)// 删除缓存的路由dispatch('delCachedView', view)resolve(null)})},// 从可显示的集合中 删除tags viewdelVisitedView({ commit }, view: RouteRecordRaw) {commit('DEL_VISITED_VIEW', view)},// 从缓存列表删除指定tag viewdelCachedView({ commit }, view: RouteRecordRaw) {return new Promise(resolve => {commit('DEL_CACHED_VIEW', view)resolve(null)})},// 清空 可显示列表 和 缓存列表delAllView({ dispatch }) {return new Promise(resolve => {// 删除显示的路由tagdispatch('delAllVisitedView')// 删除缓存的路由dispatch('delAllCachedViews')resolve(null)})},// 清空可显示列表delAllVisitedView({ commit }) {commit('DEL_ALL_VISITED_VIEWS')},// 清空缓存列表delAllCachedViews({ commit }) {commit('DEL_ALL_CACHED_VIEWS')},// 关闭其他tagdelOthersViews({ dispatch }, view: RouteRecordRaw) {dispatch('delOthersVisitedViews', view)dispatch('delOthersCachedViews', view)},// 关闭其他可显示tagdelOthersVisitedViews({ commit }, view: RouteRecordRaw) {commit('DEL_OTHERS_VISITED_VIEWS', view)},// 关闭其他缓存tagdelOthersCachedViews({ commit }, view: RouteRecordRaw) {commit('DEL_OTHERS_CACHED_VIEWS', view)}}const tagsView: Module<ITagsViewState, IRootState> = {namespaced: true,state: {visitedViews: [],cachedViews: []},mutations,actions}export default tagsView
2-3 右键刷新


<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"><el-dropdowntrigger="contextmenu"@command="command => handleTagCommand(command, tag)"><span>{{ tag.meta.title }}<!-- affix固定的路由tag是无法删除 --><spanv-if="!isAffix(tag)"class="el-icon-close"@click.prevent.stop="closeSelectedTag(tag)"></span></span><template #dropdown><el-dropdown-menu><el-dropdown-item command="refresh">刷新</el-dropdown-item><el-dropdown-item command="all">关闭所有</el-dropdown-item><el-dropdown-item command="other">关闭其他</el-dropdown-item><el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item></el-dropdown-menu></template></el-dropdown></router-link></div></scroll-panel></div></template><script lang="ts">import path from 'path'import { defineComponent, computed, watch, onMounted, nextTick } 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'// 右键菜单enum TagCommandType {All = 'all',Other = 'other',Self = 'self',Refresh = 'refresh'}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}// 右键菜单const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {switch (command) {case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的handleCloseAllTag(view)breakcase TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的taghandleCloseOtherTag(view)breakcase TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项closeSelectedTag(view)breakcase TagCommandType.Refresh: // 刷新当前右键选中tag对应的路由refreshSelectedTag(view)}}// 删除所有tag 除了affix为true的const handleCloseAllTag = (view: RouteLocationWithFullPath) => {// 对于是affix的tag是不会被删除的store.dispatch('tagsView/delAllView').then(() => {// 关闭所有后 就让切换到剩下affix中最后一个tagtoLastView(visitedTags.value, view)})}// 删除其他tag 除了当前右键的tagconst handleCloseOtherTag = (view: RouteLocationWithFullPath) => {store.dispatch('tagsView/delOthersViews', view).then(() => {if (!isActive(view)) { // 删除其他tag后 让该view路由激活router.push(view.path)}})}// 右键刷新 清空当前对应路由缓存const refreshSelectedTag = (view: RouteLocationWithFullPath) => {// 刷新前 将该路由名称从缓存列表中移除store.dispatch('tagsView/delCachedView', view).then(() => {const { fullPath } = viewnextTick(() => {router.replace('/redirect' + fullPath)})})}return {visitedTags,isActive,closeSelectedTag,isAffix,handleTagCommand}}})</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;::v-deep {.el-dropdown {color: #fff;}}&::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;vertical-align: 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: -3px;}&:hover {background-color: #b4bccc;color: #fff;}}}</style>
参考本节源码
右键关闭所有
https://gitee.com/brolly/vue3-element-admin/commit/293d91875e315cec0b489421f1404e05d77f8a70
右键关闭其他
https://gitee.com/brolly/vue3-element-admin/commit/2246c3d9e7d7b56d4ad8959e45f177652a6f3cce
右键刷新
https://gitee.com/brolly/vue3-element-admin/commit/c8c1d110074238f25946a028b694494e7109c2a7
