通过右键选择 关闭所有、 关闭其他、 关闭当前、刷新 对于tag affix为true的固定tag是不允许关闭删除的

效果图
image.png
右键关闭所有

自动切换到dashboard因为它是固定tag

image.png
右键关闭其他

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

image.png
关闭后
image.png
关闭当前右键选中tag

affix为true的tag是不能关闭的

image.png
关闭后
image.png
右键刷新
image.png
刷新后 input内容也没有了
image.png

2-1 修改tagsView组件

添加下拉菜单

需要使用 element dropdown组件,给每一个tag添加。

src/layout/components/TagsView/index.vue
image.png

添加右键事件

右键菜单关闭所有事件

image.png
定义枚举类型
image.png
src/layout/components/TagsView/index.vue

  1. <template>
  2. <div class="tags-view-container">
  3. <scroll-panel>
  4. <div class="tags-view-wrapper">
  5. <router-link
  6. class="tags-view-item"
  7. :class="{
  8. active: isActive(tag)
  9. }"
  10. v-for="(tag, index) in visitedTags"
  11. :key="index"
  12. :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
  13. tag="span"
  14. >
  15. <el-dropdown
  16. trigger="contextmenu"
  17. @command="command => handleTagCommand(command, tag)">
  18. <span>
  19. {{ tag.meta.title }}
  20. <!-- affix固定的路由tag是无法删除 -->
  21. <span
  22. v-if="!isAffix(tag)"
  23. class="el-icon-close"
  24. @click.prevent.stop="closeSelectedTag(tag)"
  25. ></span>
  26. </span>
  27. <template #dropdown>
  28. <el-dropdown-menu>
  29. <el-dropdown-item command="all">关闭所有</el-dropdown-item>
  30. <el-dropdown-item command="other">关闭其他</el-dropdown-item>
  31. <el-dropdown-item command="self">关闭</el-dropdown-item>
  32. </el-dropdown-menu>
  33. </template>
  34. </el-dropdown>
  35. </router-link>
  36. </div>
  37. </scroll-panel>
  38. </div>
  39. </template>
  40. <script lang="ts">
  41. import path from 'path'
  42. import { defineComponent, computed, watch, onMounted } from 'vue'
  43. import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
  44. import { useStore } from '@/store'
  45. import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
  46. import { routes } from '@/router'
  47. import ScrollPanel from './ScrollPanel.vue'
  48. // 右键菜单
  49. enum TagCommandType {
  50. All = 'all',
  51. Other = 'other',
  52. Self = 'self',
  53. }
  54. export default defineComponent({
  55. name: 'TagsView',
  56. components: {
  57. ScrollPanel
  58. },
  59. setup() {
  60. const store = useStore()
  61. const router = useRouter()
  62. const route = useRoute()
  63. // 可显示的tags view
  64. const visitedTags = computed(() => store.state.tagsView.visitedViews)
  65. // 从路由表中过滤出要affixed tagviews
  66. const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
  67. let tags: RouteLocationWithFullPath[] = []
  68. routes.forEach(route => {
  69. if (route.meta && route.meta.affix) {
  70. // 把路由路径解析成完整路径,路由可能是相对路径
  71. const tagPath = path.resolve(basePath, route.path)
  72. tags.push({
  73. name: route.name,
  74. path: tagPath,
  75. fullPath: tagPath,
  76. meta: { ...route.meta }
  77. } as RouteLocationWithFullPath)
  78. }
  79. // 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
  80. if (route.children) {
  81. const childTags = fillterAffixTags(route.children, route.path)
  82. if (childTags.length) {
  83. tags = [...tags, ...childTags]
  84. }
  85. }
  86. })
  87. return tags
  88. }
  89. // 初始添加affix的tag
  90. const initTags = () => {
  91. const affixTags = fillterAffixTags(routes)
  92. for (const tag of affixTags) {
  93. if (tag.name) {
  94. store.dispatch('tagsView/addVisitedView', tag)
  95. }
  96. }
  97. }
  98. // 添加tag
  99. const addTags = () => {
  100. const { name } = route
  101. if (name) {
  102. store.dispatch('tagsView/addView', route)
  103. }
  104. }
  105. // 路径发生变化追加tags view
  106. watch(() => route.path, () => {
  107. addTags()
  108. })
  109. // 最近当前router到tags view
  110. onMounted(() => {
  111. initTags()
  112. addTags()
  113. })
  114. // 当前是否是激活的tag
  115. const isActive = (tag: RouteRecordRaw) => {
  116. return tag.path === route.path
  117. }
  118. // 让删除后tags view集合中最后一个为选中状态
  119. const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
  120. // 得到集合中最后一个项tag view 可能没有
  121. const lastView = visitedViews[visitedViews.length - 1]
  122. if (lastView) {
  123. router.push(lastView.fullPath as string)
  124. } else { // 集合中都没有tag view时
  125. // 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
  126. if (view.name === 'Dashboard') {
  127. router.replace({ path: '/redirect' + view.fullPath as string })
  128. } else {
  129. // tag都没有了 删除的也不是Dashboard 只能跳转首页
  130. router.push('/')
  131. }
  132. }
  133. }
  134. // 关闭当前右键的tag路由
  135. const closeSelectedTag = (view: RouteLocationWithFullPath) => {
  136. // 关掉并移除view
  137. store.dispatch('tagsView/delView', view).then(() => {
  138. // 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
  139. if (isActive(view)) {
  140. toLastView(visitedTags.value, view)
  141. }
  142. })
  143. }
  144. // 是否是始终固定在tagsview上的tag
  145. const isAffix = (tag: RouteLocationWithFullPath) => {
  146. return tag.meta && tag.meta.affix
  147. }
  148. // 右键菜单
  149. const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {
  150. switch (command) {
  151. case TagCommandType.All:
  152. handleCloseAllTag(view)
  153. }
  154. }
  155. const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
  156. // 对于是affix的tag是不会被删除的
  157. store.dispatch('tagsView/delAllView').then(() => {
  158. // 关闭所有后 就让切换到剩下affix中最后一个tag
  159. toLastView(visitedTags.value, view)
  160. })
  161. }
  162. return {
  163. visitedTags,
  164. isActive,
  165. closeSelectedTag,
  166. isAffix,
  167. handleTagCommand
  168. }
  169. }
  170. })
  171. </script>
  172. <style lang="scss" scoped>
  173. .tags-view-container {
  174. height: 34px;
  175. background: #fff;
  176. border-bottom: 1px solid #d8dce5;
  177. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  178. overflow: hidden;
  179. .tags-view-wrapper {
  180. .tags-view-item {
  181. display: inline-block;
  182. height: 26px;
  183. line-height: 26px;
  184. border: 1px solid #d8dce5;
  185. background: #fff;
  186. color: #495060;
  187. padding: 0 8px;
  188. box-sizing: border-box;
  189. font-size: 12px;
  190. margin-left: 5px;
  191. margin-top: 4px;
  192. &:first-of-type {
  193. margin-left: 15px;
  194. }
  195. &:last-of-type {
  196. margin-right: 15px;
  197. }
  198. &.active {
  199. background-color: #42b983;
  200. color: #fff;
  201. border-color: #42b983;
  202. ::v-deep {
  203. .el-dropdown {
  204. color: #fff;
  205. }
  206. }
  207. &::before {
  208. position: relative;
  209. display: inline-block;
  210. content: '';
  211. width: 8px;
  212. height: 8px;
  213. border-radius: 50%;
  214. margin-right: 5px;
  215. background: #fff;
  216. }
  217. }
  218. }
  219. }
  220. }
  221. </style>
  222. <style lang="scss">
  223. .tags-view-container {
  224. .el-icon-close {
  225. width: 16px;
  226. height: 16px;
  227. position: relative;
  228. left: 2px;
  229. border-radius: 50%;
  230. text-align: center;
  231. transition: all .3s cubic-bezier(.645, .045, .355, 1);
  232. transform-origin: 100% 50%;
  233. &:before {
  234. transform: scale(.6);
  235. display: inline-block;
  236. vertical-align: -1px;
  237. }
  238. &:hover {
  239. background-color: #b4bccc;
  240. color: #fff;
  241. }
  242. }
  243. }
  244. </style>

修改store

我们的tag列表和缓存列表都在store 所有对于它们的增删改查需要 调用action mutaions来删除

image.png
src/store/modules/tagsView.ts

  1. import { Module, ActionTree, MutationTree } from 'vuex'
  2. import { RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router'
  3. import { IRootState } from '@/store'
  4. // 携带fullPath
  5. export interface RouteLocationWithFullPath extends RouteRecordNormalized {
  6. fullPath?: string;
  7. }
  8. export interface ITagsViewState {
  9. // 存放当前显示的tags view集合
  10. visitedViews: RouteLocationWithFullPath[];
  11. // 根据路由name缓存集合
  12. cachedViews: RouteRecordName[];
  13. }
  14. // 定义mutations
  15. const mutations: MutationTree<ITagsViewState> = {
  16. // 添加可显示tags view
  17. ADD_VISITED_VIEW(state, view) {
  18. // 过滤去重
  19. if (state.visitedViews.some(v => v.path === view.path)) return
  20. // 没有titles时处理
  21. state.visitedViews.push(Object.assign({}, view, {
  22. title: view.meta.title || 'tag-name'
  23. }))
  24. },
  25. // 如果路由meta.noCache没有 默认或为false代表进行缓存,为true不缓存
  26. // 默认缓存所有路由
  27. ADD_CACHED_VIEW(state, view) {
  28. // 只有路由有name才可缓存集合keep-alive inludes使用
  29. if (state.cachedViews.includes(view.name)) return
  30. if (!view.meta.noCache) {
  31. state.cachedViews.push(view.name)
  32. }
  33. },
  34. DEL_VISITED_VIEW(state, view) {
  35. const i = state.visitedViews.indexOf(view)
  36. if (i > -1) {
  37. state.visitedViews.splice(i, 1)
  38. }
  39. },
  40. // 可删除指定的一个view缓存
  41. DEL_CACHED_VIEW(state, view) {
  42. const index = state.cachedViews.indexOf(view.name)
  43. index > -1 && state.cachedViews.splice(index, 1)
  44. },
  45. // 清空可显示列表
  46. DEL_ALL_VISITED_VIEWS(state) {
  47. // 对于affix为true的路由 tag view 是不能删除的
  48. const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
  49. state.visitedViews = affixTags
  50. },
  51. // 清空缓存列表
  52. DEL_ALL_CACHED_VIEWS(state) {
  53. state.cachedViews = []
  54. }
  55. }
  56. // 定义actions
  57. const actions: ActionTree<ITagsViewState, IRootState> = {
  58. // 添加tags view
  59. addView({ dispatch }, view: RouteRecordRaw) {
  60. // 添加tag时也要判断该tag是否需要缓存
  61. dispatch('addVisitedView', view)
  62. dispatch('addCachedView', view)
  63. },
  64. // 添加可显示的tags view 添加前commit里需要进行去重过滤
  65. addVisitedView({ commit }, view: RouteRecordRaw) {
  66. commit('ADD_VISITED_VIEW', view)
  67. },
  68. // 添加可缓存的标签tag
  69. addCachedView({ commit }, view: RouteRecordRaw) {
  70. commit('ADD_CACHED_VIEW', view)
  71. },
  72. // 删除指定tags view
  73. delView({ dispatch }, view: RouteRecordRaw) {
  74. return new Promise(resolve => {
  75. // 删除显示的路由tag
  76. dispatch('delVisitedView', view)
  77. // 删除缓存的路由
  78. dispatch('delCachedView', view)
  79. resolve(null)
  80. })
  81. },
  82. // 从可显示的集合中 删除tags view
  83. delVisitedView({ commit }, view: RouteRecordRaw) {
  84. commit('DEL_VISITED_VIEW', view)
  85. },
  86. // 从缓存列表删除指定tag view
  87. delCachedView({ commit }, view: RouteRecordRaw) {
  88. return new Promise(resolve => {
  89. commit('DEL_CACHED_VIEW', view)
  90. resolve(null)
  91. })
  92. },
  93. // 清空 可显示列表 和 缓存列表
  94. delAllView({ dispatch }) {
  95. return new Promise(resolve => {
  96. // 删除显示的路由tag
  97. dispatch('delAllVisitedView')
  98. // 删除缓存的路由
  99. dispatch('delAllCachedViews')
  100. resolve(null)
  101. })
  102. },
  103. // 清空可显示列表
  104. delAllVisitedView({ commit }) {
  105. commit('DEL_ALL_VISITED_VIEWS')
  106. },
  107. // 清空缓存列表
  108. delAllCachedViews({ commit }) {
  109. commit('DEL_ALL_CACHED_VIEWS')
  110. }
  111. }
  112. const tagsView: Module<ITagsViewState, IRootState> = {
  113. namespaced: true,
  114. state: {
  115. visitedViews: [],
  116. cachedViews: []
  117. },
  118. mutations,
  119. actions
  120. }
  121. export default tagsView

2-2 右键关闭其他和关闭

右键关闭其他 除了 affix tag 和 当前右键tag 右键关闭 是处理affix tag下拉菜单不会显示此项

修改tagsview

src/layout/components/TagsView/index.vue
image.png
image.png

src/layout/components/TagsView/index.vue

  1. <template>
  2. <div class="tags-view-container">
  3. <scroll-panel>
  4. <div class="tags-view-wrapper">
  5. <router-link
  6. class="tags-view-item"
  7. :class="{
  8. active: isActive(tag)
  9. }"
  10. v-for="(tag, index) in visitedTags"
  11. :key="index"
  12. :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
  13. tag="span"
  14. >
  15. <el-dropdown
  16. trigger="contextmenu"
  17. @command="command => handleTagCommand(command, tag)">
  18. <span>
  19. {{ tag.meta.title }}
  20. <!-- affix固定的路由tag是无法删除 -->
  21. <span
  22. v-if="!isAffix(tag)"
  23. class="el-icon-close"
  24. @click.prevent.stop="closeSelectedTag(tag)"
  25. ></span>
  26. </span>
  27. <template #dropdown>
  28. <el-dropdown-menu>
  29. <el-dropdown-item command="all">关闭所有</el-dropdown-item>
  30. <el-dropdown-item command="other">关闭其他</el-dropdown-item>
  31. <el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item>
  32. </el-dropdown-menu>
  33. </template>
  34. </el-dropdown>
  35. </router-link>
  36. </div>
  37. </scroll-panel>
  38. </div>
  39. </template>
  40. <script lang="ts">
  41. import path from 'path'
  42. import { defineComponent, computed, watch, onMounted } from 'vue'
  43. import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
  44. import { useStore } from '@/store'
  45. import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
  46. import { routes } from '@/router'
  47. import ScrollPanel from './ScrollPanel.vue'
  48. // 右键菜单
  49. enum TagCommandType {
  50. All = 'all',
  51. Other = 'other',
  52. Self = 'self',
  53. }
  54. export default defineComponent({
  55. name: 'TagsView',
  56. components: {
  57. ScrollPanel
  58. },
  59. setup() {
  60. const store = useStore()
  61. const router = useRouter()
  62. const route = useRoute()
  63. // 可显示的tags view
  64. const visitedTags = computed(() => store.state.tagsView.visitedViews)
  65. // 从路由表中过滤出要affixed tagviews
  66. const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
  67. let tags: RouteLocationWithFullPath[] = []
  68. routes.forEach(route => {
  69. if (route.meta && route.meta.affix) {
  70. // 把路由路径解析成完整路径,路由可能是相对路径
  71. const tagPath = path.resolve(basePath, route.path)
  72. tags.push({
  73. name: route.name,
  74. path: tagPath,
  75. fullPath: tagPath,
  76. meta: { ...route.meta }
  77. } as RouteLocationWithFullPath)
  78. }
  79. // 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
  80. if (route.children) {
  81. const childTags = fillterAffixTags(route.children, route.path)
  82. if (childTags.length) {
  83. tags = [...tags, ...childTags]
  84. }
  85. }
  86. })
  87. return tags
  88. }
  89. // 初始添加affix的tag
  90. const initTags = () => {
  91. const affixTags = fillterAffixTags(routes)
  92. for (const tag of affixTags) {
  93. if (tag.name) {
  94. store.dispatch('tagsView/addVisitedView', tag)
  95. }
  96. }
  97. }
  98. // 添加tag
  99. const addTags = () => {
  100. const { name } = route
  101. if (name) {
  102. store.dispatch('tagsView/addView', route)
  103. }
  104. }
  105. // 路径发生变化追加tags view
  106. watch(() => route.path, () => {
  107. addTags()
  108. })
  109. // 最近当前router到tags view
  110. onMounted(() => {
  111. initTags()
  112. addTags()
  113. })
  114. // 当前是否是激活的tag
  115. const isActive = (tag: RouteRecordRaw) => {
  116. return tag.path === route.path
  117. }
  118. // 让删除后tags view集合中最后一个为选中状态
  119. const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
  120. // 得到集合中最后一个项tag view 可能没有
  121. const lastView = visitedViews[visitedViews.length - 1]
  122. if (lastView) {
  123. router.push(lastView.fullPath as string)
  124. } else { // 集合中都没有tag view时
  125. // 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
  126. if (view.name === 'Dashboard') {
  127. router.replace({ path: '/redirect' + view.fullPath as string })
  128. } else {
  129. // tag都没有了 删除的也不是Dashboard 只能跳转首页
  130. router.push('/')
  131. }
  132. }
  133. }
  134. // 关闭当前右键的tag路由
  135. const closeSelectedTag = (view: RouteLocationWithFullPath) => {
  136. // 关掉并移除view
  137. store.dispatch('tagsView/delView', view).then(() => {
  138. // 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
  139. if (isActive(view)) {
  140. toLastView(visitedTags.value, view)
  141. }
  142. })
  143. }
  144. // 是否是始终固定在tagsview上的tag
  145. const isAffix = (tag: RouteLocationWithFullPath) => {
  146. return tag.meta && tag.meta.affix
  147. }
  148. // 右键菜单
  149. const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {
  150. switch (command) {
  151. case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的
  152. handleCloseAllTag(view)
  153. break
  154. case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
  155. handleCloseOtherTag(view)
  156. break
  157. case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
  158. closeSelectedTag(view)
  159. }
  160. }
  161. // 删除所有tag 除了affix为true的
  162. const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
  163. // 对于是affix的tag是不会被删除的
  164. store.dispatch('tagsView/delAllView').then(() => {
  165. // 关闭所有后 就让切换到剩下affix中最后一个tag
  166. toLastView(visitedTags.value, view)
  167. })
  168. }
  169. // 删除其他tag 除了当前右键的tag
  170. const handleCloseOtherTag = (view: RouteLocationWithFullPath) => {
  171. store.dispatch('tagsView/delOthersViews', view).then(() => {
  172. if (!isActive(view)) { // 删除其他tag后 让该view路由激活
  173. router.push(view.path)
  174. }
  175. })
  176. }
  177. return {
  178. visitedTags,
  179. isActive,
  180. closeSelectedTag,
  181. isAffix,
  182. handleTagCommand
  183. }
  184. }
  185. })
  186. </script>
  187. <style lang="scss" scoped>
  188. .tags-view-container {
  189. height: 34px;
  190. background: #fff;
  191. border-bottom: 1px solid #d8dce5;
  192. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  193. overflow: hidden;
  194. .tags-view-wrapper {
  195. .tags-view-item {
  196. display: inline-block;
  197. height: 26px;
  198. line-height: 26px;
  199. border: 1px solid #d8dce5;
  200. background: #fff;
  201. color: #495060;
  202. padding: 0 8px;
  203. box-sizing: border-box;
  204. font-size: 12px;
  205. margin-left: 5px;
  206. margin-top: 4px;
  207. &:first-of-type {
  208. margin-left: 15px;
  209. }
  210. &:last-of-type {
  211. margin-right: 15px;
  212. }
  213. &.active {
  214. background-color: #42b983;
  215. color: #fff;
  216. border-color: #42b983;
  217. ::v-deep {
  218. .el-dropdown {
  219. color: #fff;
  220. }
  221. }
  222. &::before {
  223. position: relative;
  224. display: inline-block;
  225. content: '';
  226. width: 8px;
  227. height: 8px;
  228. border-radius: 50%;
  229. margin-right: 5px;
  230. background: #fff;
  231. }
  232. }
  233. }
  234. }
  235. }
  236. </style>
  237. <style lang="scss">
  238. .tags-view-container {
  239. .el-icon-close {
  240. width: 16px;
  241. height: 16px;
  242. vertical-align: 2px;
  243. border-radius: 50%;
  244. text-align: center;
  245. transition: all .3s cubic-bezier(.645, .045, .355, 1);
  246. transform-origin: 100% 50%;
  247. &:before {
  248. transform: scale(.6);
  249. display: inline-block;
  250. vertical-align: -3px;
  251. }
  252. &:hover {
  253. background-color: #b4bccc;
  254. color: #fff;
  255. }
  256. }
  257. }
  258. </style>

store里添加相关action和mutation

image.png
src/store/modules/tagsView.ts

  1. import { Module, ActionTree, MutationTree } from 'vuex'
  2. import { RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router'
  3. import { IRootState } from '@/store'
  4. // 携带fullPath
  5. export interface RouteLocationWithFullPath extends RouteRecordNormalized {
  6. fullPath?: string;
  7. }
  8. export interface ITagsViewState {
  9. // 存放当前显示的tags view集合
  10. visitedViews: RouteLocationWithFullPath[];
  11. // 根据路由name缓存集合
  12. cachedViews: RouteRecordName[];
  13. }
  14. // 定义mutations
  15. const mutations: MutationTree<ITagsViewState> = {
  16. // 添加可显示tags view
  17. ADD_VISITED_VIEW(state, view) {
  18. // 过滤去重
  19. if (state.visitedViews.some(v => v.path === view.path)) return
  20. // 没有titles时处理
  21. state.visitedViews.push(Object.assign({}, view, {
  22. title: view.meta.title || 'tag-name'
  23. }))
  24. },
  25. // 如果路由meta.noCache没有 默认或为false代表进行缓存,为true不缓存
  26. // 默认缓存所有路由
  27. ADD_CACHED_VIEW(state, view) {
  28. // 只有路由有name才可缓存集合keep-alive inludes使用
  29. if (state.cachedViews.includes(view.name)) return
  30. if (!view.meta.noCache) {
  31. state.cachedViews.push(view.name)
  32. }
  33. },
  34. DEL_VISITED_VIEW(state, view) {
  35. const i = state.visitedViews.indexOf(view)
  36. if (i > -1) {
  37. state.visitedViews.splice(i, 1)
  38. }
  39. },
  40. // 可删除指定的一个view缓存
  41. DEL_CACHED_VIEW(state, view) {
  42. const index = state.cachedViews.indexOf(view.name)
  43. index > -1 && state.cachedViews.splice(index, 1)
  44. },
  45. // 清空可显示列表
  46. DEL_ALL_VISITED_VIEWS(state) {
  47. // 对于affix为true的路由 tag view 是不能删除的
  48. const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
  49. state.visitedViews = affixTags
  50. },
  51. // 清空缓存列表
  52. DEL_ALL_CACHED_VIEWS(state) {
  53. state.cachedViews = []
  54. },
  55. // 删除标签导航其他可显示tag 除了 affix为true 以及当前右键选中的view
  56. DEL_OTHERS_VISITED_VIEWS(state, view: RouteRecordRaw) {
  57. state.visitedViews = state.visitedViews.filter(tag => tag.meta.affix || (tag.path === view.path))
  58. },
  59. // 删除缓存列表里其他tag 除了当前右键选中的view
  60. DEL_OTHERS_CACHED_VIEWS(state, view: RouteRecordRaw) {
  61. state.cachedViews = state.cachedViews.filter(name => name !== view.name)
  62. }
  63. }
  64. // 定义actions
  65. const actions: ActionTree<ITagsViewState, IRootState> = {
  66. // 添加tags view
  67. addView({ dispatch }, view: RouteRecordRaw) {
  68. // 添加tag时也要判断该tag是否需要缓存
  69. dispatch('addVisitedView', view)
  70. dispatch('addCachedView', view)
  71. },
  72. // 添加可显示的tags view 添加前commit里需要进行去重过滤
  73. addVisitedView({ commit }, view: RouteRecordRaw) {
  74. commit('ADD_VISITED_VIEW', view)
  75. },
  76. // 添加可缓存的标签tag
  77. addCachedView({ commit }, view: RouteRecordRaw) {
  78. commit('ADD_CACHED_VIEW', view)
  79. },
  80. // 删除指定tags view
  81. delView({ dispatch }, view: RouteRecordRaw) {
  82. return new Promise(resolve => {
  83. // 删除显示的路由tag
  84. dispatch('delVisitedView', view)
  85. // 删除缓存的路由
  86. dispatch('delCachedView', view)
  87. resolve(null)
  88. })
  89. },
  90. // 从可显示的集合中 删除tags view
  91. delVisitedView({ commit }, view: RouteRecordRaw) {
  92. commit('DEL_VISITED_VIEW', view)
  93. },
  94. // 从缓存列表删除指定tag view
  95. delCachedView({ commit }, view: RouteRecordRaw) {
  96. return new Promise(resolve => {
  97. commit('DEL_CACHED_VIEW', view)
  98. resolve(null)
  99. })
  100. },
  101. // 清空 可显示列表 和 缓存列表
  102. delAllView({ dispatch }) {
  103. return new Promise(resolve => {
  104. // 删除显示的路由tag
  105. dispatch('delAllVisitedView')
  106. // 删除缓存的路由
  107. dispatch('delAllCachedViews')
  108. resolve(null)
  109. })
  110. },
  111. // 清空可显示列表
  112. delAllVisitedView({ commit }) {
  113. commit('DEL_ALL_VISITED_VIEWS')
  114. },
  115. // 清空缓存列表
  116. delAllCachedViews({ commit }) {
  117. commit('DEL_ALL_CACHED_VIEWS')
  118. },
  119. // 关闭其他tag
  120. delOthersViews({ dispatch }, view: RouteRecordRaw) {
  121. dispatch('delOthersVisitedViews', view)
  122. dispatch('delOthersCachedViews', view)
  123. },
  124. // 关闭其他可显示tag
  125. delOthersVisitedViews({ commit }, view: RouteRecordRaw) {
  126. commit('DEL_OTHERS_VISITED_VIEWS', view)
  127. },
  128. // 关闭其他缓存tag
  129. delOthersCachedViews({ commit }, view: RouteRecordRaw) {
  130. commit('DEL_OTHERS_CACHED_VIEWS', view)
  131. }
  132. }
  133. const tagsView: Module<ITagsViewState, IRootState> = {
  134. namespaced: true,
  135. state: {
  136. visitedViews: [],
  137. cachedViews: []
  138. },
  139. mutations,
  140. actions
  141. }
  142. export default tagsView

2-3 右键刷新

image.png
image.png

  1. <template>
  2. <div class="tags-view-container">
  3. <scroll-panel>
  4. <div class="tags-view-wrapper">
  5. <router-link
  6. class="tags-view-item"
  7. :class="{
  8. active: isActive(tag)
  9. }"
  10. v-for="(tag, index) in visitedTags"
  11. :key="index"
  12. :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
  13. tag="span"
  14. >
  15. <el-dropdown
  16. trigger="contextmenu"
  17. @command="command => handleTagCommand(command, tag)">
  18. <span>
  19. {{ tag.meta.title }}
  20. <!-- affix固定的路由tag是无法删除 -->
  21. <span
  22. v-if="!isAffix(tag)"
  23. class="el-icon-close"
  24. @click.prevent.stop="closeSelectedTag(tag)"
  25. ></span>
  26. </span>
  27. <template #dropdown>
  28. <el-dropdown-menu>
  29. <el-dropdown-item command="refresh">刷新</el-dropdown-item>
  30. <el-dropdown-item command="all">关闭所有</el-dropdown-item>
  31. <el-dropdown-item command="other">关闭其他</el-dropdown-item>
  32. <el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item>
  33. </el-dropdown-menu>
  34. </template>
  35. </el-dropdown>
  36. </router-link>
  37. </div>
  38. </scroll-panel>
  39. </div>
  40. </template>
  41. <script lang="ts">
  42. import path from 'path'
  43. import { defineComponent, computed, watch, onMounted, nextTick } from 'vue'
  44. import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
  45. import { useStore } from '@/store'
  46. import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
  47. import { routes } from '@/router'
  48. import ScrollPanel from './ScrollPanel.vue'
  49. // 右键菜单
  50. enum TagCommandType {
  51. All = 'all',
  52. Other = 'other',
  53. Self = 'self',
  54. Refresh = 'refresh'
  55. }
  56. export default defineComponent({
  57. name: 'TagsView',
  58. components: {
  59. ScrollPanel
  60. },
  61. setup() {
  62. const store = useStore()
  63. const router = useRouter()
  64. const route = useRoute()
  65. // 可显示的tags view
  66. const visitedTags = computed(() => store.state.tagsView.visitedViews)
  67. // 从路由表中过滤出要affixed tagviews
  68. const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
  69. let tags: RouteLocationWithFullPath[] = []
  70. routes.forEach(route => {
  71. if (route.meta && route.meta.affix) {
  72. // 把路由路径解析成完整路径,路由可能是相对路径
  73. const tagPath = path.resolve(basePath, route.path)
  74. tags.push({
  75. name: route.name,
  76. path: tagPath,
  77. fullPath: tagPath,
  78. meta: { ...route.meta }
  79. } as RouteLocationWithFullPath)
  80. }
  81. // 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
  82. if (route.children) {
  83. const childTags = fillterAffixTags(route.children, route.path)
  84. if (childTags.length) {
  85. tags = [...tags, ...childTags]
  86. }
  87. }
  88. })
  89. return tags
  90. }
  91. // 初始添加affix的tag
  92. const initTags = () => {
  93. const affixTags = fillterAffixTags(routes)
  94. for (const tag of affixTags) {
  95. if (tag.name) {
  96. store.dispatch('tagsView/addVisitedView', tag)
  97. }
  98. }
  99. }
  100. // 添加tag
  101. const addTags = () => {
  102. const { name } = route
  103. if (name) {
  104. store.dispatch('tagsView/addView', route)
  105. }
  106. }
  107. // 路径发生变化追加tags view
  108. watch(() => route.path, () => {
  109. addTags()
  110. })
  111. // 最近当前router到tags view
  112. onMounted(() => {
  113. initTags()
  114. addTags()
  115. })
  116. // 当前是否是激活的tag
  117. const isActive = (tag: RouteRecordRaw) => {
  118. return tag.path === route.path
  119. }
  120. // 让删除后tags view集合中最后一个为选中状态
  121. const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
  122. // 得到集合中最后一个项tag view 可能没有
  123. const lastView = visitedViews[visitedViews.length - 1]
  124. if (lastView) {
  125. router.push(lastView.fullPath as string)
  126. } else { // 集合中都没有tag view时
  127. // 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
  128. if (view.name === 'Dashboard') {
  129. router.replace({ path: '/redirect' + view.fullPath as string })
  130. } else {
  131. // tag都没有了 删除的也不是Dashboard 只能跳转首页
  132. router.push('/')
  133. }
  134. }
  135. }
  136. // 关闭当前右键的tag路由
  137. const closeSelectedTag = (view: RouteLocationWithFullPath) => {
  138. // 关掉并移除view
  139. store.dispatch('tagsView/delView', view).then(() => {
  140. // 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
  141. if (isActive(view)) {
  142. toLastView(visitedTags.value, view)
  143. }
  144. })
  145. }
  146. // 是否是始终固定在tagsview上的tag
  147. const isAffix = (tag: RouteLocationWithFullPath) => {
  148. return tag.meta && tag.meta.affix
  149. }
  150. // 右键菜单
  151. const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {
  152. switch (command) {
  153. case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的
  154. handleCloseAllTag(view)
  155. break
  156. case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
  157. handleCloseOtherTag(view)
  158. break
  159. case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
  160. closeSelectedTag(view)
  161. break
  162. case TagCommandType.Refresh: // 刷新当前右键选中tag对应的路由
  163. refreshSelectedTag(view)
  164. }
  165. }
  166. // 删除所有tag 除了affix为true的
  167. const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
  168. // 对于是affix的tag是不会被删除的
  169. store.dispatch('tagsView/delAllView').then(() => {
  170. // 关闭所有后 就让切换到剩下affix中最后一个tag
  171. toLastView(visitedTags.value, view)
  172. })
  173. }
  174. // 删除其他tag 除了当前右键的tag
  175. const handleCloseOtherTag = (view: RouteLocationWithFullPath) => {
  176. store.dispatch('tagsView/delOthersViews', view).then(() => {
  177. if (!isActive(view)) { // 删除其他tag后 让该view路由激活
  178. router.push(view.path)
  179. }
  180. })
  181. }
  182. // 右键刷新 清空当前对应路由缓存
  183. const refreshSelectedTag = (view: RouteLocationWithFullPath) => {
  184. // 刷新前 将该路由名称从缓存列表中移除
  185. store.dispatch('tagsView/delCachedView', view).then(() => {
  186. const { fullPath } = view
  187. nextTick(() => {
  188. router.replace('/redirect' + fullPath)
  189. })
  190. })
  191. }
  192. return {
  193. visitedTags,
  194. isActive,
  195. closeSelectedTag,
  196. isAffix,
  197. handleTagCommand
  198. }
  199. }
  200. })
  201. </script>
  202. <style lang="scss" scoped>
  203. .tags-view-container {
  204. height: 34px;
  205. background: #fff;
  206. border-bottom: 1px solid #d8dce5;
  207. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  208. overflow: hidden;
  209. .tags-view-wrapper {
  210. .tags-view-item {
  211. display: inline-block;
  212. height: 26px;
  213. line-height: 26px;
  214. border: 1px solid #d8dce5;
  215. background: #fff;
  216. color: #495060;
  217. padding: 0 8px;
  218. box-sizing: border-box;
  219. font-size: 12px;
  220. margin-left: 5px;
  221. margin-top: 4px;
  222. &:first-of-type {
  223. margin-left: 15px;
  224. }
  225. &:last-of-type {
  226. margin-right: 15px;
  227. }
  228. &.active {
  229. background-color: #42b983;
  230. color: #fff;
  231. border-color: #42b983;
  232. ::v-deep {
  233. .el-dropdown {
  234. color: #fff;
  235. }
  236. }
  237. &::before {
  238. position: relative;
  239. display: inline-block;
  240. content: '';
  241. width: 8px;
  242. height: 8px;
  243. border-radius: 50%;
  244. margin-right: 5px;
  245. background: #fff;
  246. }
  247. }
  248. }
  249. }
  250. }
  251. </style>
  252. <style lang="scss">
  253. .tags-view-container {
  254. .el-icon-close {
  255. width: 16px;
  256. height: 16px;
  257. vertical-align: 2px;
  258. border-radius: 50%;
  259. text-align: center;
  260. transition: all .3s cubic-bezier(.645, .045, .355, 1);
  261. transform-origin: 100% 50%;
  262. &:before {
  263. transform: scale(.6);
  264. display: inline-block;
  265. vertical-align: -3px;
  266. }
  267. &:hover {
  268. background-color: #b4bccc;
  269. color: #fff;
  270. }
  271. }
  272. }
  273. </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