上一章主要完善了侧边栏菜单组件 Vite2 + vue3 + TS + ElementPlus 从零搭建后台管理系统(四)
这一章开始完善header组件,将包含breadcrumb和tagsView
1. 新增多个路由菜单
-
在 src/views 下新建:
-
demo/index.vue
-
icon/index.vue
内容简单随意
再更改 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'),
}
]
}
]
})
复制代码
- 在 store/interface/index.ts 目录下新增:
//新增
export interface RoutesListState {
routesList: Array<object>;
}
...
export interface RootStateTypes {
themeConfig: ThemeConfigState;
app:App;
routesList:RoutesListState; //新增
}
复制代码
- 在 store/modules 目录下新增 routesList.ts
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: '首页',
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: {
// 设置路由,菜单中使用到
getRoutesList(state: any, data: Array<object>) {
state.routesList = data;
},
},
actions: {
// 设置路由,菜单中使用到
async setRoutesList({ commit }, data: any) {
commit('getRoutesList', data);
},
},
};
export default routesListModule;
复制代码
新增的routesList.ts 记得再store/index.ts中引用
- 修改 layout/component/aside.vue 文件中menuList 从 store中获取
aside.vue:
// 修改++
const state: any = reactive({
menuList: [], // 修改++
clientWidth: ''
})
// 新增++
const setFilterRoutes = () => {
state.menuList = filterRoutesFun(store.state.routesList.routesList)
}
// 新增++
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
})
}
// 修改++
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth)
setFilterRoutes(); // 新增++
})
复制代码
到此完成了 新增路由菜单,并从store中获取
2. 新增 breadcrumb(面包屑) 组件
先在store的themeConfig模块里新增开两个状态:
- isBreadcrumb:true // 是否开启 Breadcrumb
- isBreadcrumbIcon:true // 是否开启 Breadcrumb 图标
- 在 layout/component/navBars 下新增:
breadcrumb/index.vue // 处理 breadcrumb 相关内容
breadcrumb/breadcrumb.vue // breadcrumb组件
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>
复制代码
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
})
// 获取布局配置信息
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
}
// 处理-分级-面包屑数据
const getBreadcrumbList = (arr: Array<object>) => {
arr.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.routeSplitIndex = 1
getBreadcrumbList(store.state.routesList.routesList)
}
// 页面加载时
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>
复制代码
- 再修改 layout/component/navBars/index.vue 引入 BreadcrumbIndex组件:
<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()
// 获取布局配置信息
const getThemeConfig = computed(() => store.state.themeConfig)
// 展开/收起左侧菜单点击
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>
复制代码
至此 breadcrumb 基本完成了:
3. 新增 tagsView(导航标签) 组件
先在 store/interface/index.ts 内新增:
//新增++
export interface TagsViewRoutesState {
tagsViewRoutes: Array<object>;
}
...
export interface RootStateTypes {
themeConfig: ThemeConfigState;
app:App;
tagsViewRoutes:TagsViewRoutesState; //新增++
}
复制代码
- 在 store/modules 目录下新增 tagsViewRoutes.ts
tagsViewRoutes.ts:
import { Module } from 'vuex';
// 此处加上 `.ts` 后缀报错,具体原因不详
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: '首页',
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: {
// 设置 TagsView 路由
getTagsViewRoutes(state: any, data: Array<string>) {
state.tagsViewRoutes = data;
},
},
actions: {
// 设置 TagsView 路由
async setTagsViewRoutes({ commit }, data: Array<string>) {
commit('getTagsViewRoutes', data);
},
},
};
export default tagsViewRoutesModule;
复制代码
在tagsViewRoutes.ts中先 tagsViewRoutes 的值和routesList的值 默认为一样的后面再做处理
接着在 store 的 themeConfig模块 里新增个状态:
- isCacheTagsView:false // 是否开启 TagsView 缓存
新增的 tagsViewRoutes.ts 记得再store/index.ts中引用
- 修改 @/utils/storage.ts :
新增三个处理sessionStorage函数
// 设置临时缓存
export function setSession(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
}
// 获取临时缓存
export function getSession(key: string) {
let json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
}
// 移除临时缓存
export function removeSession(key: string) {
window.sessionStorage.removeItem(key);
}
// 移除全部临时缓存
export function clearSession() {
window.sessionStorage.clear();
}
复制代码
- 在 layout/component/navBars 下新增 tagsView/tagsView.vue
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>
复制代码
添加成功 breadcrumb 和 tagsView 的效果如下图:
最后
到目前为止,后台管理系统路由,侧边栏菜单,顶部面包屑和导航标签 基础基本完成。
这里非常感谢大家的点赞?。