当标签导航太多时,超出页面宽度时,可以左右横向滑动

image.png
滑动后
image.png

4-1 添加scrollbar组件

element.ts中导入el-scrollbar

image.png

创建ScrollPanel组件

src/layout/components/TagsView/ScrollPanel.vue

  1. <template>
  2. <el-scrollbar
  3. wrap-class="scroll-wrapper"
  4. >
  5. <slot />
  6. </el-scrollbar>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'ScrollPanel'
  11. }
  12. </script>
  13. <style lang="scss">
  14. .scroll-wrapper {
  15. position: relative;
  16. width: 100%;
  17. white-space: nowrap;
  18. }
  19. </style>

以插槽形式包裹tagsview里面内容

4-2 修改tagsview

用scrollpanel组件包裹tagsview组件

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

4-3 样式调整

image.png
src/layout/index.vue

  1. <template>
  2. <div class="app-wrapper">
  3. <div class="sidebar-container">
  4. <Sidebar />
  5. </div>
  6. <div class="main-container">
  7. <div class="header">
  8. <navbar />
  9. <tags-view />
  10. </div>
  11. <!-- AppMain router-view -->
  12. <app-main />
  13. </div>
  14. </div>
  15. </template>
  16. <script lang="ts">
  17. import { defineComponent } from 'vue'
  18. import Sidebar from './components/Sidebar/index.vue'
  19. import AppMain from './components/AppMain.vue'
  20. import Navbar from './components/Navbar.vue'
  21. import TagsView from './components/TagsView/index.vue'
  22. export default defineComponent({
  23. components: {
  24. Sidebar,
  25. AppMain,
  26. Navbar,
  27. TagsView
  28. }
  29. })
  30. </script>
  31. <style lang="scss" scoped>
  32. .app-wrapper {
  33. display: flex;
  34. width: 100%;
  35. height: 100%;
  36. .main-container {
  37. flex: 1;
  38. display: flex;
  39. flex-direction: column;
  40. overflow: hidden;
  41. .app-main {
  42. /* 50= navbar 50 如果有tagsview + 34 */
  43. min-height: calc(100vh - 84px);
  44. }
  45. }
  46. }
  47. </style>

4-4 注意使用标签导航

使用标签导航的路由 必须要name属性 因为方便我们根据 name进行路由筛选和缓存keep-alive

image.png

本节参考源码

https://gitee.com/brolly/vue3-element-admin/commit/59741362e40d74bd4834f752ee61752d165b2e54