通过右键选择 关闭所有、 关闭其他、 关闭当前、刷新 对于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-link
class="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-dropdown
trigger="contextmenu"
@command="command => handleTagCommand(command, tag)">
<span>
{{ tag.meta.title }}
<!-- affix固定的路由tag是无法删除 -->
<span
v-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 view
const visitedTags = computed(() => store.state.tagsView.visitedViews)
// 从路由表中过滤出要affixed tagviews
const 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的tag
const initTags = () => {
const affixTags = fillterAffixTags(routes)
for (const tag of affixTags) {
if (tag.name) {
store.dispatch('tagsView/addVisitedView', tag)
}
}
}
// 添加tag
const addTags = () => {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
}
// 路径发生变化追加tags view
watch(() => route.path, () => {
addTags()
})
// 最近当前router到tags view
onMounted(() => {
initTags()
addTags()
})
// 当前是否是激活的tag
const 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) => {
// 关掉并移除view
store.dispatch('tagsView/delView', view).then(() => {
// 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
if (isActive(view)) {
toLastView(visitedTags.value, view)
}
})
}
// 是否是始终固定在tagsview上的tag
const 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中最后一个tag
toLastView(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'
// 携带fullPath
export interface RouteLocationWithFullPath extends RouteRecordNormalized {
fullPath?: string;
}
export interface ITagsViewState {
// 存放当前显示的tags view集合
visitedViews: RouteLocationWithFullPath[];
// 根据路由name缓存集合
cachedViews: RouteRecordName[];
}
// 定义mutations
const mutations: MutationTree<ITagsViewState> = {
// 添加可显示tags view
ADD_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)) return
if (!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 = []
}
}
// 定义actions
const actions: ActionTree<ITagsViewState, IRootState> = {
// 添加tags view
addView({ dispatch }, view: RouteRecordRaw) {
// 添加tag时也要判断该tag是否需要缓存
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
// 添加可显示的tags view 添加前commit里需要进行去重过滤
addVisitedView({ commit }, view: RouteRecordRaw) {
commit('ADD_VISITED_VIEW', view)
},
// 添加可缓存的标签tag
addCachedView({ commit }, view: RouteRecordRaw) {
commit('ADD_CACHED_VIEW', view)
},
// 删除指定tags view
delView({ dispatch }, view: RouteRecordRaw) {
return new Promise(resolve => {
// 删除显示的路由tag
dispatch('delVisitedView', view)
// 删除缓存的路由
dispatch('delCachedView', view)
resolve(null)
})
},
// 从可显示的集合中 删除tags view
delVisitedView({ commit }, view: RouteRecordRaw) {
commit('DEL_VISITED_VIEW', view)
},
// 从缓存列表删除指定tag view
delCachedView({ commit }, view: RouteRecordRaw) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve(null)
})
},
// 清空 可显示列表 和 缓存列表
delAllView({ dispatch }) {
return new Promise(resolve => {
// 删除显示的路由tag
dispatch('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-link
class="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-dropdown
trigger="contextmenu"
@command="command => handleTagCommand(command, tag)">
<span>
{{ tag.meta.title }}
<!-- affix固定的路由tag是无法删除 -->
<span
v-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 view
const visitedTags = computed(() => store.state.tagsView.visitedViews)
// 从路由表中过滤出要affixed tagviews
const 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的tag
const initTags = () => {
const affixTags = fillterAffixTags(routes)
for (const tag of affixTags) {
if (tag.name) {
store.dispatch('tagsView/addVisitedView', tag)
}
}
}
// 添加tag
const addTags = () => {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
}
// 路径发生变化追加tags view
watch(() => route.path, () => {
addTags()
})
// 最近当前router到tags view
onMounted(() => {
initTags()
addTags()
})
// 当前是否是激活的tag
const 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) => {
// 关掉并移除view
store.dispatch('tagsView/delView', view).then(() => {
// 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
if (isActive(view)) {
toLastView(visitedTags.value, view)
}
})
}
// 是否是始终固定在tagsview上的tag
const 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)
break
case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
handleCloseOtherTag(view)
break
case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
closeSelectedTag(view)
}
}
// 删除所有tag 除了affix为true的
const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
// 对于是affix的tag是不会被删除的
store.dispatch('tagsView/delAllView').then(() => {
// 关闭所有后 就让切换到剩下affix中最后一个tag
toLastView(visitedTags.value, view)
})
}
// 删除其他tag 除了当前右键的tag
const 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'
// 携带fullPath
export interface RouteLocationWithFullPath extends RouteRecordNormalized {
fullPath?: string;
}
export interface ITagsViewState {
// 存放当前显示的tags view集合
visitedViews: RouteLocationWithFullPath[];
// 根据路由name缓存集合
cachedViews: RouteRecordName[];
}
// 定义mutations
const mutations: MutationTree<ITagsViewState> = {
// 添加可显示tags view
ADD_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)) return
if (!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 以及当前右键选中的view
DEL_OTHERS_VISITED_VIEWS(state, view: RouteRecordRaw) {
state.visitedViews = state.visitedViews.filter(tag => tag.meta.affix || (tag.path === view.path))
},
// 删除缓存列表里其他tag 除了当前右键选中的view
DEL_OTHERS_CACHED_VIEWS(state, view: RouteRecordRaw) {
state.cachedViews = state.cachedViews.filter(name => name !== view.name)
}
}
// 定义actions
const actions: ActionTree<ITagsViewState, IRootState> = {
// 添加tags view
addView({ dispatch }, view: RouteRecordRaw) {
// 添加tag时也要判断该tag是否需要缓存
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
// 添加可显示的tags view 添加前commit里需要进行去重过滤
addVisitedView({ commit }, view: RouteRecordRaw) {
commit('ADD_VISITED_VIEW', view)
},
// 添加可缓存的标签tag
addCachedView({ commit }, view: RouteRecordRaw) {
commit('ADD_CACHED_VIEW', view)
},
// 删除指定tags view
delView({ dispatch }, view: RouteRecordRaw) {
return new Promise(resolve => {
// 删除显示的路由tag
dispatch('delVisitedView', view)
// 删除缓存的路由
dispatch('delCachedView', view)
resolve(null)
})
},
// 从可显示的集合中 删除tags view
delVisitedView({ commit }, view: RouteRecordRaw) {
commit('DEL_VISITED_VIEW', view)
},
// 从缓存列表删除指定tag view
delCachedView({ commit }, view: RouteRecordRaw) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve(null)
})
},
// 清空 可显示列表 和 缓存列表
delAllView({ dispatch }) {
return new Promise(resolve => {
// 删除显示的路由tag
dispatch('delAllVisitedView')
// 删除缓存的路由
dispatch('delAllCachedViews')
resolve(null)
})
},
// 清空可显示列表
delAllVisitedView({ commit }) {
commit('DEL_ALL_VISITED_VIEWS')
},
// 清空缓存列表
delAllCachedViews({ commit }) {
commit('DEL_ALL_CACHED_VIEWS')
},
// 关闭其他tag
delOthersViews({ dispatch }, view: RouteRecordRaw) {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
},
// 关闭其他可显示tag
delOthersVisitedViews({ commit }, view: RouteRecordRaw) {
commit('DEL_OTHERS_VISITED_VIEWS', view)
},
// 关闭其他缓存tag
delOthersCachedViews({ 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-link
class="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-dropdown
trigger="contextmenu"
@command="command => handleTagCommand(command, tag)">
<span>
{{ tag.meta.title }}
<!-- affix固定的路由tag是无法删除 -->
<span
v-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 view
const visitedTags = computed(() => store.state.tagsView.visitedViews)
// 从路由表中过滤出要affixed tagviews
const 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的tag
const initTags = () => {
const affixTags = fillterAffixTags(routes)
for (const tag of affixTags) {
if (tag.name) {
store.dispatch('tagsView/addVisitedView', tag)
}
}
}
// 添加tag
const addTags = () => {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
}
// 路径发生变化追加tags view
watch(() => route.path, () => {
addTags()
})
// 最近当前router到tags view
onMounted(() => {
initTags()
addTags()
})
// 当前是否是激活的tag
const 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) => {
// 关掉并移除view
store.dispatch('tagsView/delView', view).then(() => {
// 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
if (isActive(view)) {
toLastView(visitedTags.value, view)
}
})
}
// 是否是始终固定在tagsview上的tag
const 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)
break
case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
handleCloseOtherTag(view)
break
case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
closeSelectedTag(view)
break
case TagCommandType.Refresh: // 刷新当前右键选中tag对应的路由
refreshSelectedTag(view)
}
}
// 删除所有tag 除了affix为true的
const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
// 对于是affix的tag是不会被删除的
store.dispatch('tagsView/delAllView').then(() => {
// 关闭所有后 就让切换到剩下affix中最后一个tag
toLastView(visitedTags.value, view)
})
}
// 删除其他tag 除了当前右键的tag
const 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 } = view
nextTick(() => {
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