The previous chapter mainly improved the sidebar menu components Vite2 + vue3 + TS + ElementPlus from zero to build the background management system (IV)

This chapter begins to refine the Header component, which will contain the breadcrumb and tagsView

1. Add multiple routing menus

  • Create a new file under SRC /views:

  • demo/index.vue

  • icon/index.vue

Keep it simple and casual

To change the router/index. Ts:

const router = createRouter({
  history: routerHistory,
  routes: [{path: '/'.component: Layout,
      children: [{path: '/home'.name:'home'.component: () = >import('views/home/index.vue'),}, {path: '/demo'.name:'demo'.component: () = >import('views/demo/index.vue'),}, {path: '/icon'.name:'icon'.component: () = >import('views/icon/index.vue'),}]}]})Copy the code
  • Add the following to the store/interface/index.ts directory:
/ / new
export interface RoutesListState {
	routesList: Array<object>; }...export interface RootStateTypes {
	themeConfig: ThemeConfigState;
  app:App;
	routesList:RoutesListState; / / new
}
Copy the code
  • Add routesList.ts in store/modules

RoutesList. Ts:

import { Module } from 'vuex';

import { RoutesListState, RootStateTypes } from 'store/interface/index';

const routesListModule: Module<RoutesListState, RootStateTypes> = {
	namespaced: true.state: {
		routesList: [{meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-menu'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'home'.index: '1'
        },
        name: 'home'.path: '/home'
      },
      {
        meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-s-grid'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'demo'.index: '2'
        },
        name: 'demo'.path: '/demo'
      },
      {
        meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-s-grid'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'icon'.index: '3'
        },
        name: 'icon'.path: '/icon'}],},mutations: {
		// Set route, used in the menu
		getRoutesList(state: any, data: Array<object>){ state.routesList = data; }},actions: {
		// Set route, used in the menu
		async setRoutesList({ commit }, data: any) {
			commit('getRoutesList', data); ,}}};export default routesListModule;

Copy the code

Remember to refer to the new routesList.ts in store/index.ts

  • Modify the layout/component/value. Vue file menuList obtained from the store

aside.vue:

    / / modify + +
    const state: any = reactive({
      menuList: []./ / modify + +
      clientWidth: ' '
    })

    / / add + +
    const setFilterRoutes = () = > {
      state.menuList = filterRoutesFun(store.state.routesList.routesList)
    }

    / / add + +
    const filterRoutesFun = (arr: Array<object>) = > {
      return arr
        .filter((item: any) = >! item.meta.isHide) .map((item: any) = > {
          item = Object.assign({}, item)
          if (item.children) item.children = filterRoutesFun(item.children)
          return item
        })
    }

    / / modify + +
    onBeforeMount(() = > {
      initMenuFixed(document.body.clientWidth)
      setFilterRoutes(); / / add + +
    })
Copy the code

This completes the new route menu and gets it from Store

2. Added the breadcrumb component

Add two new states to the themeConfig module of the store:

  • IsBreadcrumb :true // Whether Breadcrumb is enabled
  • IsBreadcrumbIcon :true // Whether to enable the Breadcrumb icon
  • Add new layout/ Component /navBars:

Vue // Handles the breadcrumb related content

Breadcrumb/breadcrumb. Vue / / breadcrumb components

Breadcrumb/index. Vue:

<template> <div class="layout-navbars-breadcrumb-index"> <Breadcrumb /> <! </div> </template> <script lang="ts"> import Breadcrumb from '.. /breadcrumb/breadcrumb.vue'; export default { name: 'layoutBreadcrumbIndex', components: { Breadcrumb }, }; </script> <style scoped lang="scss"> .layout-navbars-breadcrumb-index { height: 50px; display: flex; align-items: center; padding-right: 15px; background: var(--bg-topBar); overflow: hidden; border-bottom: 1px solid #f1f2f3; } </style>Copy the code

breadcrumb/breadcrumb.vue

<template> <div class="layout-navbars-breadcrumb" v-show="getThemeConfig.isBreadcrumb"> <i class="layout-navbars-breadcrumb-icon" :class="getThemeConfig.isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" @click="onThemeConfigChange" ></i> <el-breadcrumb class="layout-navbars-breadcrumb-hide"> <transition-group name="breadcrumb" mode="out-in"> <el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="v.meta.title" > <span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span" > <i :class="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon" ></i> {{ v.meta.title }} </span> <a v-else @click.prevent="onBreadcrumbClick(v)">  <i :class="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon" ></i> {{ v.meta.title }} </a> </el-breadcrumb-item> </transition-group> </el-breadcrumb> </div> </template> <script lang="ts"> import { toRefs, reactive, computed, onMounted } from 'vue' import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router' import { useStore } from 'store/index' export default { name: 'layoutBreadcrumb', setup() { const store = useStore() const route = useRoute() const router = useRouter() const state: any = reactive({ breadcrumbList: [], routeSplit: [], routeSplitFirst: '', routeSplitIndex: 1}) // Get layout configuration information const getThemeConfig = computed(() => store.state.themeconfig) // Const onBreadcrumbClick = (v: any) => { const { redirect, Path} = v if (redirect) router.push(redirect) else router.push(path)} const onThemeConfigChange = () =>  { store.state.themeConfig.isCollapse = ! Store. State. ThemeConfig. IsCollapse} / / processing - classification - bread crumbs data const getBreadcrumbList = (arr: Array < object >) = > {arr. The map ((item: any) => { state.routeSplit.map((v: any, k: number, arrs: any) => { if (state.routeSplitFirst === item.path) { state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}` state.breadcrumbList.push(item) state.routeSplitIndex++ if (item.children) getBreadcrumbList(item.children) } }) }) } // Const initRouteSplit = (Path: string) => {if (! store.state.themeConfig.isBreadcrumb) return false state.breadcrumbList = [store.state.routesList.routesList[0]] state.routeSplit = path.split('/') state.routeSplit.shift() state.routeSplitFirst = `/${state.routeSplit[0]}` State. The routeSplitIndex = 1 getBreadcrumbList (store. State. RoutesList. RoutesList)} / / when the page loads onMounted (() = > { InitRouteSplit (route.path)}) // onBeforeRouteUpdate((to) => {initRouteSplit(to.path)}) return {getThemeConfig,  onBreadcrumbClick, onThemeConfigChange, ... toRefs(state) } } } </script> <style scoped lang="scss"> .layout-navbars-breadcrumb { flex: 1; height: inherit; display: flex; align-items: center; padding-left: 15px; .layout-navbars-breadcrumb-icon { cursor: pointer; font-size: 18px; margin-right: 15px; color: var(--bg-topBarColor); }.layout-navbars-breadcrumb-span {opacity: 0.7; color: var(--bg-topBarColor); } .layout-navbars-breadcrumb-iconfont { font-size: 14px; margin-right: 5px; } ::v-deep(. El-breadcrumb__separator) {opacity: 0.7; color: var(--bg-topBarColor); } } </style>Copy the code
  • To modify the layout/component/navBars/index. The vue introduced BreadcrumbIndex components:
<template> <div class="layout-navbars-container"> <BreadcrumbIndex></BreadcrumbIndex> </div> </template> <script lang="ts"> import { computed } from 'vue' import { useStore } from 'store/index' import BreadcrumbIndex from './breadcrumb/index.vue' export default { name: 'layoutNavBars', components: { BreadcrumbIndex }, Setup () {const store = useStore() // Get layout configuration information const getThemeConfig = computed(() => store.state.themeconfig) // Open/pack up the left side menu click const onThemeConfigChange = () = > {store. State. ThemeConfig. IsCollapse =! store.state.themeConfig.isCollapse } return { getThemeConfig, onThemeConfigChange } } } </script> <style scoped lang="scss"> .layout-navbars-container { display: flex; flex-direction: column; width: 100%; height: 100%; } </style>Copy the code

Now the breadcrumb is almost complete:

3. Added the tagsView component

Add: store/interface/index.ts

/ / add + +
export interface TagsViewRoutesState {
	tagsViewRoutes: Array<object>; }...export interface RootStateTypes {
	themeConfig: ThemeConfigState;
  app:App;
	tagsViewRoutes:TagsViewRoutesState; / / add + +
}
Copy the code
  • Add tagsViewroutes.ts to store/modules

TagsViewRoutes. Ts:

import { Module } from 'vuex';
// the suffix '.ts' is added to the file
import { TagsViewRoutesState, RootStateTypes } from 'store/interface/index';

const tagsViewRoutesModule: Module<TagsViewRoutesState, RootStateTypes> = {
	namespaced: true.state: {
		tagsViewRoutes: [{meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-menu'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'home'.index: '1'
        },
        name: 'home'.path: '/home'
      },
      {
        meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-s-grid'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'demo'.index: '2'
        },
        name: 'demo'.path: '/demo'
      },
      {
        meta: {
          auth: ['admin'.'test'].icon: 'iconfont el-icon-s-grid'.isAffix: true.isHide: false.isIframe: false.isKeepAlive: true.title: 'icon'.index: '3'
        },
        name: 'icon'.path: '/icon'}],},mutations: {
		// Set the TagsView route
		getTagsViewRoutes(state: any, data: Array<string>){ state.tagsViewRoutes = data; }},actions: {
		// Set the TagsView route
		async setTagsViewRoutes({ commit }, data: Array<string>) {
			commit('getTagsViewRoutes', data); ,}}};export default tagsViewRoutesModule;

Copy the code

In tagsViewRoutes. Ts, the values of tagsViewRoutes and routesList default to the same values

Add a new state to the themeConfig module of the store:

  • IsCacheTagsView :false // Whether to enable TagsView cache

The new tagsViewroutes.ts is referenced in store/index.ts

  • Modify @ / utils/storage. Ts:

New three handling sessionStorage functions

// Set temporary cache
export function setSession(key: string, val: any) {
	window.sessionStorage.setItem(key, JSON.stringify(val));
}
// Get temporary cache
export function getSession(key: string) {
	let json: any = window.sessionStorage.getItem(key);
	return JSON.parse(json);
}
// Remove temporary cache
export function removeSession(key: string) {
	window.sessionStorage.removeItem(key);
}
// Remove all temporary cache
export function clearSession() {
	window.sessionStorage.clear();
}
Copy the code
  • Added tagsView/ TagsView.vue under Layout/Component /navBars

TagsView. Vue:

<template>
  <div
    class="layout-navbars-tagsview"
    :class="{
      'layout-navbars-tagsview-shadow': getThemeConfig.layout === 'classic'
    }"
  >
    <el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
      <ul class="layout-navbars-tagsview-ul tags-style-one" ref="tagsUlRef">
        <li
          v-for="(v, k) in tagsViewList"
          :key="k"
          class="layout-navbars-tagsview-ul-li"
          :data-name="v.name"
          :class="{ 'is-active': isActive(v.path) }"
          @click="onTagsClick(v, k)"
          :ref="
            (el) => {
              if (el) tagsRefs[k] = el
            }
          "
        >
          <i
            class="iconfont icon-webicon318 layout-navbars-tagsview-ul-li-iconfont font14"
            v-if="isActive(v.path)"
          ></i>
          <i
            class="layout-navbars-tagsview-ul-li-iconfont"
            :class="v.meta.icon"
            v-if="!isActive(v.path) && getThemeConfig.isTagsviewIcon"
          ></i>
          <span>{{ v.meta.title }}</span>
          <template v-if="isActive(v.path)">
            <i
              class="el-icon-close layout-navbars-tagsview-ul-li-icon layout-icon-active"
              v-if="!v.meta.isAffix"
              @click.stop="closeCurrentTagsView(v.path)"
            ></i>
          </template>
          <i
            class="el-icon-close layout-navbars-tagsview-ul-li-icon layout-icon-three"
            v-if="!v.meta.isAffix"
            @click.stop="closeCurrentTagsView(v.path)"
          ></i>
        </li>
      </ul>
    </el-scrollbar>
  </div>
</template>

<script lang="ts">
  import {
    toRefs,
    reactive,
    onMounted,
    computed,
    ref,
    nextTick,
    onBeforeUpdate,
    onBeforeMount,
    onUnmounted,
    getCurrentInstance,
    watch
  } from 'vue'
  import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'
  import { useStore } from 'store/index'
  import { setSession, removeSession } from '@/utils/storage'

  export default {
    name: 'layoutTagsView',
    components: {},
    setup() {
      const { proxy } = getCurrentInstance() as any
      const tagsRefs = ref([])
      const scrollbarRef = ref()
      const tagsUlRef = ref()
      const store = useStore()
      const route = useRoute()
      const router = useRouter()
      const state: any = reactive({
        routePath: route.path,
        tagsRefsIndex: 0,
        tagsViewList: [],
        sortable: '',
        tagsViewRoutesList: []
      })

      // 获取布局配置信息
      const getThemeConfig = computed(() => {
        return store.state.themeConfig
      })
      // 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
      const addBrowserSetSession = (tagsViewList: Array<object>) => {
        setSession('tagsViewList', tagsViewList)
      }
      // 获取 vuex 中的 tagsViewRoutes 列表
      const getTagsViewRoutes = () => {
        state.routePath = route.path
        state.tagsViewList = []
        if (!store.state.themeConfig.isCacheTagsView)
          removeSession('tagsViewList')
        state.tagsViewRoutesList = store.state.tagsViewRoutes.tagsViewRoutes
        addTagsView(route.path)
      }

      // 添加 tagsView:未设置隐藏(isHide)也添加到在 tagsView 中
      const addTagsView = (path: string, to?: any) => {
        if (state.tagsViewList.some((v: any) => v.path === path)) return false
        const item = state.tagsViewRoutesList.find((v: any) => v.path === path)
        if (item.meta.isLink && !item.meta.isIframe) return false
        item.query = to?.query ? to?.query : route.query
        state.tagsViewList.push({ ...item })
        addBrowserSetSession(state.tagsViewList)
      }

      // 关闭当前 tagsView:如果是设置了固定的(isAffix),不可以关闭
      const closeCurrentTagsView = (path: string) => {
        state.tagsViewList.map((v: any, k: number, arr: any) => {
          if (!v.meta.isAffix) {
            if (v.path === path) {
              state.tagsViewList.splice(k, 1)
              setTimeout(() => {
                // 最后一个
                if (state.tagsViewList.length === k)
                  router.push({
                    path: arr[arr.length - 1].path,
                    query: arr[arr.length - 1].query
                  })
                // 否则,跳转到下一个
                else router.push({ path: arr[k].path, query: arr[k].query })
              }, 0)
            }
          }
        })
        addBrowserSetSession(state.tagsViewList)
      }

      // 判断页面高亮
      const isActive = (path: string) => {
        return path === state.routePath
      }

      // 当前的 tagsView 项点击时
      const onTagsClick = (v: any, k: number) => {
        state.routePath = v.path
        state.tagsRefsIndex = k
        router.push(v)
      }
      // 更新滚动条显示
      const updateScrollbar = () => {
        proxy.$refs.scrollbarRef.update()
      }
      // 鼠标滚轮滚动
      const onHandleScroll = (e: any) => {
        proxy.$refs.scrollbarRef.$refs.wrap.scrollLeft += e.wheelDelta / 4
      }
      // tagsView 横向滚动
      const tagsViewmoveToCurrentTag = () => {
        nextTick(() => {
          if (tagsRefs.value.length <= 0) return false
          // 当前 li 元素
          let liDom = tagsRefs.value[state.tagsRefsIndex]
          // 当前 li 元素下标
          let liIndex = state.tagsRefsIndex
          // 当前 ul 下 li 元素总长度
          let liLength = tagsRefs.value.length
          // 最前 li
          let liFirst: any = tagsRefs.value[0]
          // 最后 li
          let liLast: any = tagsRefs.value[tagsRefs.value.length - 1]
          // 当前滚动条的值
          let scrollRefs = proxy.$refs.scrollbarRef.$refs.wrap
          // 当前滚动条滚动宽度
          let scrollS = scrollRefs.scrollWidth
          // 当前滚动条偏移宽度
          let offsetW = scrollRefs.offsetWidth
          // 当前滚动条偏移距离
          let scrollL = scrollRefs.scrollLeft
          // 上一个 tags li dom
          let liPrevTag: any = tagsRefs.value[state.tagsRefsIndex - 1]
          // 下一个 tags li dom
          let liNextTag: any = tagsRefs.value[state.tagsRefsIndex + 1]
          // 上一个 tags li dom 的偏移距离
          let beforePrevL: any = ''
          // 下一个 tags li dom 的偏移距离
          let afterNextL: any = ''
          if (liDom === liFirst) {
            // 头部
            scrollRefs.scrollLeft = 0
          } else if (liDom === liLast) {
            // 尾部
            scrollRefs.scrollLeft = scrollS - offsetW
          } else {
            // 非头/尾部
            if (liIndex === 0) beforePrevL = liFirst.offsetLeft - 5
            else beforePrevL = liPrevTag?.offsetLeft - 5
            if (liIndex === liLength)
              afterNextL = liLast.offsetLeft + liLast.offsetWidth + 5
            else afterNextL = liNextTag.offsetLeft + liNextTag.offsetWidth + 5
            if (afterNextL > scrollL + offsetW) {
              scrollRefs.scrollLeft = afterNextL - offsetW
            } else if (beforePrevL < scrollL) {
              scrollRefs.scrollLeft = beforePrevL
            }
          }
          // 更新滚动条,防止不出现
          updateScrollbar()
        })
      }
      // 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
      const getTagsRefsIndex = (path: string) => {
        if (state.tagsViewList.length > 0) {
          state.tagsRefsIndex = state.tagsViewList.findIndex(
            (item: any) => item.path === path
          )
        }
      }

      // 监听路由的变化,动态赋值给 tagsView
      watch(store.state, (val) => {
        if (
          val.tagsViewRoutes.tagsViewRoutes.length ===
          state.tagsViewRoutesList.length
        )
          return false
        getTagsViewRoutes()
      })
      // 页面加载前
      onBeforeMount(() => {})
      // 页面卸载时
      onUnmounted(() => {})
      // 页面更新时
      onBeforeUpdate(() => {
        tagsRefs.value = []
      })
      // 页面加载时
      onMounted(() => {
        // 初始化 vuex 中的 tagsViewRoutes 列表
        getTagsViewRoutes()
      })
      // 路由更新时
      onBeforeRouteUpdate((to) => {
        state.routePath = to.path
        addTagsView(to.path, to)
        getTagsRefsIndex(to.path)
        tagsViewmoveToCurrentTag()
      })
      return {
        isActive,
        getTagsViewRoutes,
        onTagsClick,
        tagsRefs,
        scrollbarRef,
        tagsUlRef,
        onHandleScroll,
        getThemeConfig,
        closeCurrentTagsView,
        ...toRefs(state)
      }
    }
  }
</script>

<style scoped lang="scss">
  .layout-navbars-tagsview {
    flex: 1;
    background-color: #ffffff;
    border-bottom: 1px solid #f1f2f3;
    ::v-deep(.el-scrollbar__wrap) {
      overflow-x: auto !important;
    }
    &-ul {
      list-style: none;
      margin: 0;
      padding: 0;
      height: 34px;
      display: flex;
      align-items: center;
      color: #606266;
      font-size: 12px;
      white-space: nowrap;
      padding: 0 15px;
      &-li {
        height: 26px;
        line-height: 26px;
        display: flex;
        align-items: center;
        border: 1px solid #e6e6e6;
        padding: 0 15px;
        margin-right: 5px;
        border-radius: 2px;
        position: relative;
        z-index: 0;
        cursor: pointer;
        justify-content: space-between;
        &:hover {
          background-color: var(--color-primary-light-9);
          color: var(--color-primary);
          border-color: var(--color-primary-light-6);
        }
        &-iconfont {
          position: relative;
          left: -5px;
          font-size: 12px;
        }
        &-icon {
          border-radius: 100%;
          position: relative;
          height: 14px;
          width: 14px;
          text-align: center;
          line-height: 14px;
          right: -5px;
          &:hover {
            color: #fff;
            background-color: var(--color-primary-light-3);
          }
        }
        .layout-icon-active {
          display: block;
        }
        .layout-icon-three {
          display: none;
        }
      }
      .is-active {
        color: #ffffff;
        background: var(--color-primary);
        border-color: var(--color-primary);
      }
    }
    // 风格2
    .tags-style-two {
      .layout-navbars-tagsview-ul-li {
        height: 34px !important;
        line-height: 34px !important;
        border: none !important;
        .layout-navbars-tagsview-ul-li-iconfont {
          display: none;
        }
        .layout-icon-active {
          display: none;
        }
        .layout-icon-three {
          display: block;
        }
      }
      .is-active {
        background: none !important;
        color: var(--color-primary) !important;
        border-bottom: 2px solid !important;
        border-color: var(--color-primary) !important;
        border-radius: 0 !important;
      }
    }
    // 风格3
    .tags-style-three {
      .layout-navbars-tagsview-ul-li {
        height: 34px !important;
        line-height: 34px !important;
        border-right: 1px solid #f6f6f6 !important;
        border-top: none !important;
        border-bottom: none !important;
        border-left: none !important;
        border-radius: 0 !important;
        margin-right: 0 !important;
        &:first-of-type {
          border-left: 1px solid #f6f6f6 !important;
        }
        .layout-icon-active {
          display: none;
        }
        .layout-icon-three {
          display: block;
        }
      }
      .is-active {
        background: white !important;
        color: var(--color-primary) !important;
        border-top: 1px solid !important;
        border-top-color: var(--color-primary) !important;
      }
    }
    // 风格4
    .tags-style-four {
      .layout-navbars-tagsview-ul-li {
        margin-right: 0 !important;
        border: none !important;
        position: relative;
        border-radius: 3px !important;
        .layout-icon-active {
          display: none;
        }
        .layout-icon-three {
          display: block;
        }
        &:hover {
          background: none !important;
        }
      }
      .is-active {
        background: none !important;
        color: var(--color-primary) !important;
      }
    }
  }
  .layout-navbars-tagsview-shadow {
    box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px;
  }
</style>

Copy the code

The successful addition of breadcrumb and tagsView looks like this:

The last

So far, the background management system routing, sidebar menu, top breadcrumb and navigation TAB foundation is basically complete.

Thank you very much for clicking 👍.