1. 介绍
后台管理系统通常都是统一的布局结构,通常有:sideBar、headerBar、main,其中:headerBar包括navBar等,sideBar分为左侧和右侧等。简单结构如下图:
如何让所有的页面都会通过Layout结构,展现在mian中呢?
答:我们会在Layout中使用router-view组件。并且在路由中,每个父路由都是指向的Layout组件,再通过重定向来跳转至具体页面,这样可以实现每个路由都必须通过Layout结构。
2. 详解
注:Layout组件的路径为 src/layout
layout
- components
- header
- breadcrumb
- tagBar
- themeSwitch
- userDropdown
- sideBar
- index.vue
2.1 headerBar介绍
本框架中haderBar的功能有:
- 控制sideBar收缩按钮,v-model绑定isCollapsed控制侧边栏。
- breadBrumb面包屑,用于记录当前页面的位置
- screenfull全屏按钮,将管理系统全屏展示
- themeSwitch主题切换按钮,用于切换封装的主题风格,主题后面单独文章介绍
- 用户头像,包含外链的跳转和退出登录等功能
- tagsBar用于记录打开过的页面,并实现来回切换、并实现部分页面保活功能。
2.1.1 收缩按钮
在Layout的入口文件index.vue中将两个组件绑定isCollapsed属性来控制sideBar进行收缩。
在header入口中定义一个icon按钮去控制isCollapsed的切换。
// layout>index.vue
<my-side :is-collapse="isCollapsed"/>
<my-header v-model:isCollapsed="isCollapsed"></my-header>
// header>index.vue
<template>
<div class="header-top">
<el-icon @click="()=>emit('update:isCollapsed',!isCollapsed)" class="header-icon" :size="20">
<component :is="isCollapsed ? Expand:Fold"/> // 切换图标
</el-icon>
...
</div>
</template>
<script setup lang="ts">
import {Fold, Expand} from '@element-plus/icons-vue'
defineProps({
isCollapsed: {
type: Boolean,
}
})
const emit = defineEmits(['update:isCollapsed'])
</script>
2.1.2 breadCrumb面包屑
面包屑的功能就是一个路由展示。如:当前在首页的仪表盘页面,那么breadCrumb就是 首页 > 仪表盘
。
通过elementPlus中的el-breadcrumb
,和route中的route.matched
的路由记录来实现,如下:
// > header>components>breadcrumb>index.vue
<template>
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item class="breadcrumb-item" v-for="item in breadcrumbItems" :to="{ path: item.path }">{{item.meta.title}}</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import {ref, watch} from 'vue'
import {useRoute,type RouteLocationMatched} from 'vue-router'
import {ArrowRight} from '@element-plus/icons-vue'
const route = useRoute()
const breadcrumbItems = ref<RouteLocationMatched[]>([])
const refreshBreadcrumbItems = ()=>{
breadcrumbItems.value = route.matched.filter(r=>r.meta.title)
}
// 监听路由改变
watch(
()=>route.path,
(newPath)=>{
refreshBreadcrumbItems()
},{
immediate:true
}
)
</script>
<style lang="less" scoped>
.breadcrumb-item{
:deep(.is-link){
color: inherit;
}
}
</style>
2.1.3 screenfull全屏按钮
这个功能是使用了 screenfull
包实现的,我们把他封装在全局组件中使用。
添加一个el-tooltip,这个是elemenPlus中用于提示的一个组件,提高用户体验。
// > src>components>screenfull>index.vue
<template>
<el-icon @click="trigger" >
<el-tooltip :content="isFull? '退出全屏' : '全屏'" placement="bottom" effect="dark">
<svg-icon :name=" isFull? 'closeScreenfull': 'openScreenfull'"/>
</el-tooltip>
</el-icon>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {ElMessage } from 'element-plus'
import screenfull from 'screenfull'
const props = defineProps({
el:{
type:HTMLElement,
default:document.documentElement
},
})
const isFull = ref<boolean>(false)
const trigger = ()=>{
if(screenfull.isEnabled){ // 判断是否支持全屏
screenfull.toggle(props.el)
screenfull.isFullscreen
? isFull.value = false
: isFull.value = true
}else{
ElMessage.error('此设备不支持全屏操作!')
}
}
</script>
2.1.4 theme-switch 主题切换
在一般文档页面、后台管理页面、官网等用户长时间使用的页面中,通常会加上主题切换。可以避免视觉疲劳。主题中最少有:亮、暗两种风格。
本文章也封装了两套:明亮、黑暗,由于主题也是一大块,这里不做细解。具体实现请看本系列文章(自定义主题详解)。
// header > themeSwitch > index.vue
<template>
<el-dropdown trigger="click" @command="setTheme">
<el-icon>
<el-tooltip effect="dark" content="主题" placement="bottom">
<svg-icon name="palette"/>
</el-tooltip>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="theme in themeList"
:command="theme"
>{{theme.title}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import {useTheme} from '@/hooks/useTheme'
const {initTheme,themeList,setTheme} = useTheme()
initTheme()
</script>
<style lang="less" scoped>
.el-icon{
height: 100%;
svg{
cursor: pointer;
}
}
.el-dropdown{
color:inherit;
}
</style>
2.1.5 头像
头像使用的是elementPlus组件-el-dropdown
,同时伴有el-dropdown-menu
做了一个下拉菜单,用来做登出操作。
// header > userDropdown > index.vue
<template>
<el-dropdown >
<span>
<el-avatar :size="30" :fit="'cover'" :src="circleUrl" />
</span>
<template #dropdown>
<el-dropdown-menu>
<span @click="toAbout">
<el-dropdown-item>关于</el-dropdown-item>
</span>
<el-dropdown-item>GitHub</el-dropdown-item>
<span @click="logout">
<el-dropdown-item >退出登录</el-dropdown-item>
</span>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {useUserStore} from '@/store/user'
const userStore = useUserStore()
const circleUrl = ref<string>('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const {logout} = userStore // 删除用户缓存信息,跳转登录页面
</script>
// store > user.ts
...
actions:{
logout(){
this.$reset()
cache.removeCookie(ACCESS_TOKEN_KEY)
cache.clear()
resetRoutes()
router.push({name:'login'})
}
}
...
2.1.6 tagsBar
tagsBar中功能相对较多。我们需要实现:获取、添加、删除、选择并跳转、固定等功能。
思路:首页的tagBar不能关闭,这样实现会更加简单些,毕竟不可能一个页面都不展示,所以默认是展示首页。其他页面添加进来都会进行一个保存,保存其路由信息。当前的标签还要进行样式区别,增加体验感。
函数解释:
computeRoutes:,正常路由都是父路由嵌套子路由,我们只需要显示具体的子路由。所以需要将路由进行一个扁平化操作,获取全部的子路由。这里使用了递归的思想。
<template>
<section class="tag-wrapper">
<el-tag
v-for="tag in displayTags"
class="tag"
:class="{isActive:tag.name===currentTag?.name}"
effect="plain"
:closable="tag.name !== defaultTagName"
@close="closeTag(tag)"
@click='selectTag(tag)'
>
{{ tag?.meta?.title }}
</el-tag>
</section>
</template>
<script setup lang="ts">
import {onMounted, ref, watch} from 'vue'
import {RouteRecordRaw, useRoute, useRouter} from "vue-router";
import {useUserStore} from '@/store/user'
const route = useRoute()
const router = useRouter()
const {routes} = useUserStore()
// 默认tag,不能关闭
const defaultTagName = 'dashboard'
// 当前tag
const currentTag = ref<RouteRecordRaw>()
// tagBar中全部tag
const displayTags = ref<RouteRecordRaw[]>([])
/** 获取当前全部可用路由信息 */
const computeRoutes = (targetList: RouteRecordRaw[]) => {
let result: RouteRecordRaw[] = []
targetList.forEach(item => {
if (item.children) {
const childResult = computeRoutes(item.children) as RouteRecordRaw[]
if (childResult.length > 0) {
result = result.concat(childResult)
}
}
result.push(item)
})
return result
}
const usableRoutes = computeRoutes(routes)
/** 获取标签 */
const getDisplayTag = (tagName)=>{
return displayTags.value.find(route=>route.name === tagName)
}
const getUsableTag = (tagName)=>{
return usableRoutes.find(route=>route.name === tagName)
}
/** 选择标签 */
const selectTag = (tag) => {
router.push({path:tag.path,replace:true})
}
/** 关闭标签 */
const closeTag = (tag) => {
const index = displayTags.value.findIndex(item => item.name == tag.name)
if (index !== -1) {
displayTags.value.splice(index, 1)
if (tag.name === currentTag?.value?.name) {
const chooseIndex = index - 1 >= 0 ? index - 1 : 0
selectTag(displayTags.value[chooseIndex])
}
}
}
/** 初始化tag,保证有个默认的tag存在 */
onMounted(()=>{
const hasDefault = getDisplayTag(defaultTagName)
if(!hasDefault){
const defaultTag = getUsableTag(defaultTagName)
if(defaultTag){
displayTags.value.splice(0,0,defaultTag)
}
}
})
/** 检测路由 */
watch(
() => route.name,
(newVal) => {
const currentPathName = newVal
const chosenTag = getDisplayTag(currentPathName)
if (chosenTag) {
currentTag.value = chosenTag
} else {
const route = getUsableTag(currentPathName)
if (route && route?.meta?.title) {
displayTags.value.push(route)
currentTag.value = route
}
}
}, {
immediate: true
}
)
</script>
<style lang="less" scoped>
.tag {
margin: 5px;
background-color: @theme-color;
border: 1px solid @theme-line-color;
color: @theme-font-color;
cursor: pointer;
:deep(.el-icon) {
color: @theme-font-color;
}
}
.isActive {
background-color: @theme-label-active-color;
color: @theme-label-active-font-color;
:deep(.el-icon) {
color: @theme-label-active-font-color;
}
}
</style>
本篇内容过多,拆分为两篇,下篇讲解sideBar和整体Layout。
3. 本框架其他文章链接
GitHub开源链接:GitHub - grxynl/vue3-admin-template: vue3+TypeScript+pinia 后台管理系统模板
其他文章
从0开始搭建后台管理系统(首篇)
从0开始搭建后台管理系统-01(Login登录)
4. 结束语
本框架完全免费,框架尚有不足,供前端爱好者一起讨论,一起学习。
源码和文档都制作不易,如果觉得您还可以的话,求一个stars,这是对我最大的支持,也是本框架前进的最大动力。