效果图
image.png

2-1 创建TagsView组件

src/layout/components/TagsView/index.vue

  1. <template>
  2. <div class="tags-view-container">
  3. <div class="tags-view-wrapper">
  4. <!-- 一个个tag view就是router-link -->
  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.meta.title }}
  16. <span
  17. class="el-icon-close"
  18. @click.prevent.stop="closeSelectedTag(tag)"
  19. ></span>
  20. </router-link>
  21. </div>
  22. </div>
  23. </template>
  24. <script lang="ts">
  25. import { defineComponent, computed, watch, onMounted } from 'vue'
  26. import { useRoute, RouteRecordRaw } from 'vue-router'
  27. import { useStore } from '@/store'
  28. export default defineComponent({
  29. name: 'TagsView',
  30. setup() {
  31. const store = useStore()
  32. const route = useRoute()
  33. // 从store里获取 可显示的tags view
  34. const visitedTags = computed(() => store.state.tagsView.visitedViews)
  35. // 添加tag
  36. const addTags = () => {
  37. const { name } = route
  38. if (name) {
  39. store.dispatch('tagsView/addView', route)
  40. }
  41. }
  42. // 路径发生变化追加tags view
  43. watch(() => route.path, () => {
  44. addTags()
  45. })
  46. // 最近当前router到tags view
  47. onMounted(() => {
  48. addTags()
  49. })
  50. // 是否是当前应该激活的tag
  51. const isActive = (tag: RouteRecordRaw) => {
  52. return tag.path === route.path
  53. }
  54. // 关闭当前右键的tag路由
  55. const closeSelectedTag = (view: RouteRecordRaw) => {
  56. store.dispatch('tagsView/delView', view)
  57. }
  58. return {
  59. visitedTags,
  60. isActive,
  61. closeSelectedTag
  62. }
  63. }
  64. })
  65. </script>
  66. <style lang="scss" scoped>
  67. .tags-view-container {
  68. width: 100%;
  69. height: 34px;
  70. background: #fff;
  71. border-bottom: 1px solid #d8dce5;
  72. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  73. .tags-view-wrapper {
  74. .tags-view-item {
  75. display: inline-block;
  76. height: 26px;
  77. line-height: 26px;
  78. border: 1px solid #d8dce5;
  79. background: #fff;
  80. color: #495060;
  81. padding: 0 8px;
  82. box-sizing: border-box;
  83. font-size: 12px;
  84. margin-left: 5px;
  85. margin-top: 4px;
  86. &:first-of-type {
  87. margin-left: 15px;
  88. }
  89. &:last-of-type {
  90. margin-right: 15px;
  91. }
  92. &.active {
  93. background-color: #42b983;
  94. color: #fff;
  95. border-color: #42b983;
  96. &::before {
  97. position: relative;
  98. display: inline-block;
  99. content: '';
  100. width: 8px;
  101. height: 8px;
  102. border-radius: 50%;
  103. margin-right: 5px;
  104. background: #fff;
  105. }
  106. }
  107. }
  108. }
  109. }
  110. </style>
  111. <style lang="scss">
  112. .tags-view-container {
  113. .el-icon-close {
  114. width: 16px;
  115. height: 16px;
  116. position: relative;
  117. left: 2px;
  118. border-radius: 50%;
  119. text-align: center;
  120. transition: all .3s cubic-bezier(.645, .045, .355, 1);
  121. transform-origin: 100% 50%;
  122. &:before {
  123. transform: scale(.6);
  124. display: inline-block;
  125. vertical-align: -1px;
  126. }
  127. &:hover {
  128. background-color: #b4bccc;
  129. color: #fff;
  130. }
  131. }
  132. }
  133. </style>

2-2 定义store

定义tagsView module

src/store/modules/tagsView.ts

  1. import { Module, ActionTree, MutationTree } from 'vuex'
  2. import { RouteRecordRaw } from 'vue-router'
  3. import { IRootState } from '@/store'
  4. export interface ITagsViewState {
  5. // 存放当前显示的tags view集合
  6. visitedViews: RouteRecordRaw[];
  7. }
  8. // 定义mutations
  9. const mutations: MutationTree<ITagsViewState> = {
  10. // 添加可显示tags view
  11. ADD_VISITED_VIEW(state, view) {
  12. // 过滤去重
  13. if (state.visitedViews.some(v => v.path === view.path)) return
  14. // 没有title时处理
  15. state.visitedViews.push(Object.assign({}, view, {
  16. title: view.meta.title || 'tag-name'
  17. }))
  18. },
  19. DEL_VISITED_VIEW(state, view) {
  20. const i = state.visitedViews.indexOf(view)
  21. if (i > -1) {
  22. state.visitedViews.splice(i, 1)
  23. }
  24. }
  25. }
  26. // 定义actions
  27. const actions: ActionTree<ITagsViewState, IRootState> = {
  28. // 添加tags view
  29. addView({ dispatch }, view: RouteRecordRaw) {
  30. dispatch('addVisitedView', view)
  31. },
  32. // 添加可显示的tags view 添加前commit里需要进行去重过滤
  33. addVisitedView({ commit }, view: RouteRecordRaw) {
  34. commit('ADD_VISITED_VIEW', view)
  35. },
  36. // 删除tags view
  37. delView({ dispatch }, view: RouteRecordRaw) {
  38. dispatch('delVisitedView', view)
  39. },
  40. // 从可显示的集合中 删除tags view
  41. delVisitedView({ commit }, view: RouteRecordRaw) {
  42. commit('DEL_VISITED_VIEW', view)
  43. }
  44. }
  45. const tagsView: Module<ITagsViewState, IRootState> = {
  46. namespaced: true,
  47. state: {
  48. visitedViews: []
  49. },
  50. mutations,
  51. actions
  52. }
  53. export default tagsView

修改store导入module

image.png
image.png
src/store/index.ts

  1. import { InjectionKey } from 'vue'
  2. import { createStore, Store, useStore as baseUseStore } from 'vuex'
  3. import createPersistedState from 'vuex-persistedstate'
  4. import app, { IAppState } from '@/store/modules/app'
  5. import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
  6. import getters from './getters'
  7. // 模块声明在根状态下
  8. export interface IRootState {
  9. app: IAppState;
  10. tagsView: ITagsViewState;
  11. }
  12. // 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
  13. // https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
  14. // eslint-disable-next-line symbol-description
  15. export const key: InjectionKey<Store<IRootState>> = Symbol()
  16. // 对于getters在组件使用时没有类型提示
  17. // 有人提交了pr #1896 为getters创建泛型 应该还未发布
  18. // https://github.com/vuejs/vuex/pull/1896
  19. // 代码pr内容详情
  20. // https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
  21. // vuex store持久化 默认使用localstorage持久化
  22. const persisteAppState = createPersistedState({
  23. storage: window.sessionStorage, // 指定storage 也可自定义
  24. key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  25. // paths: ['app'] // 针对app这个模块持久化
  26. // 只针对app模块下sidebar.opened状态持久化
  27. paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
  28. })
  29. export default createStore<IRootState>({
  30. plugins: [
  31. persisteAppState
  32. ],
  33. getters,
  34. modules: {
  35. app,
  36. tagsView
  37. }
  38. })
  39. // 定义自己的 `useStore` 组合式函数
  40. // https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
  41. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  42. export function useStore () {
  43. return baseUseStore(key)
  44. }
  45. // vuex持久化 vuex-persistedstate文档说明
  46. // https://www.npmjs.com/package/vuex-persistedstate

修改navbar 样式

image.png

  1. <template>
  2. <div class="navbar">
  3. <hambuger @toggleClick="toggleSidebar" :is-active="sidebar.opened"/>
  4. <breadcrumb />
  5. <div class="right-menu">
  6. <!-- 全屏 -->
  7. <screenfull id="screefull" class="right-menu-item hover-effect" />
  8. <!-- element组件size切换 -->
  9. <el-tooltip content="Global Size" effect="dark" placement="bottom">
  10. <size-select class="right-menu-item hover-effect" />
  11. </el-tooltip>
  12. <!-- 用户头像 -->
  13. <avatar />
  14. </div>
  15. </div>
  16. </template>
  17. <script lang="ts">
  18. import { defineComponent, computed } from 'vue'
  19. import Breadcrumb from '@/components/Breadcrumb/index.vue'
  20. import Hambuger from '@/components/Hambuger/index.vue'
  21. import { useStore } from '@/store/index'
  22. import Screenfull from '@/components/Screenfull/index.vue'
  23. import SizeSelect from '@/components/SizeSelect/index.vue'
  24. import Avatar from './avatar/index.vue'
  25. export default defineComponent({
  26. name: 'Navbar',
  27. components: {
  28. Breadcrumb,
  29. Hambuger,
  30. Screenfull,
  31. SizeSelect,
  32. Avatar
  33. },
  34. setup() {
  35. // 使用我们自定义的useStore 具备类型提示
  36. // store.state.app.sidebar 对于getters里的属性没有类型提示
  37. const store = useStore()
  38. const toggleSidebar = () => {
  39. store.dispatch('app/toggleSidebar')
  40. }
  41. // 从getters中获取sidebar
  42. const sidebar = computed(() => store.getters.sidebar)
  43. return {
  44. toggleSidebar,
  45. sidebar
  46. }
  47. }
  48. })
  49. </script>
  50. <style lang="scss">
  51. .navbar {
  52. display: flex;
  53. background: #fff;
  54. border-bottom: 1px solid rgba(0, 21, 41, .08);
  55. box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
  56. .right-menu {
  57. flex: 1;
  58. display: flex;
  59. align-items: center;
  60. justify-content: flex-end;
  61. padding-right: 15px;
  62. &-item {
  63. padding: 0 8px;
  64. font-size: 18px;
  65. color: #5a5e66;
  66. vertical-align: text-bottom;
  67. &.hover-effect {
  68. cursor: pointer;
  69. transition: background .3s;
  70. &:hover {
  71. background: rgba(0, 0, 0, .025);
  72. }
  73. }
  74. }
  75. }
  76. }
  77. </style>

2-3 导入到layout组件

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. .app-main {
  41. /* 50= navbar 50 如果有tagsview + 34 */
  42. min-height: calc(100vh - 84px);
  43. }
  44. }
  45. }
  46. </style>

本节参考源码

https://gitee.com/brolly/vue3-element-admin/commit/7bd90896ab32f9295d3f1864fba75952a47d1435