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 👍.