一、el-menu 和 el-submenu
- 如果需要实现一个侧边栏,会如何设计?
- 侧边栏的核心是将根据权限过滤后的
router
和 el-menu
组件进行映射,所以 el-menu
和 el-submenu
是理解 sidebar
的基础。
el-menu
表示菜单容器组件,如下所示:
default-active
:当前激活菜单的 index
,注意如果存在子菜单,需要填入子菜单 ID
unique-opened
是否只保持一个子菜单的展开
mode
模式,枚举值,horizontal / vertical
这两种
collapse
是否水平折叠收起菜单(仅在 mode
为 vertical
时可用)
collapse-transition
是否开启折叠动画
@select
点击菜单事件,菜单激活回调 index
: 选中菜单项的 index
, indexPath
: 选中菜单项的 index path
@open
: sub-menu
展开的回调
@close
: sub-menu
收起的回调
el-submenu
,子菜单容器,el-menu
表示整个菜单, el-submenu
表示一个具体菜单,只是该菜单还包括了子菜单。 el-submenu
可以通过定制 slot
的 title
来自定义菜单模式。el-submenu
容器内 default
的 slot
用来存放子菜单,可以包括三种子菜单组件,如下所示:
el-menu-item-group
:菜单分组,为一组菜单添加一个标题,容器内需要存放 el-menu-item
,支持通过 title
的 slot
来定制标题样式
el-submenu
:支持循环嵌套 el-submenu
,可以使得超过两级子组件得以实现
el-menu-item
:子菜单组件
- 示例代码,如下所示:
<el-row class="tac">
<el-col :span="12">
<h5>默认颜色</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-col>
<el-col :span="12">
<h5>自定义颜色</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
<script>
export default {
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
}
}
}
</script>
二、sidebar 分析
sidebar
,如下所示:
activeMenu
:通过 meta.activeMenu
属性,指定路由对应的高亮菜单,meta.activeMenu
需要提供一个合法的路由,否则不能生效。
isCollapse
:NavBar
中点击按钮,会修改 Cookie
中的 sidebarStatus
,从 vuex
取值时会将 sidebarStatus
转为 Boolean
,并判断默认是否需要收缩左侧菜单栏
showLogo
:判断 settings.js
中的配置项是否需要展示 logo
variables
:从 @style/variables.css
中获取 scss
对象,从而获取样式
sidebar
,代码实现如下:
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import {
mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: {
SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const {
meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
sidebar
中通过 sidebar -item
实现子菜单,sidebar-item
的 props
是 item
为路由对象,basePath
是路由路径。sidebar-item
的展示逻辑,如下所示:
- 通过
item.hidden
控制菜单是否展示
- 通过
hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow
逻辑判断 template
菜单是否展示,template
代表单一菜单,如下所示:
hasOneShowingChild
:判断是否只有一个需要展示的子路由
!onlyOneChild.children||onlyOneChild.noShowingChildren
: 判断需要展示的子菜单,是否包含 children
属性,如果包含,则说明子菜单可能存在孙子菜单,此时需要再判断 noShowingChildren
属性
!item.alwaysShow
:判断路由中是否存在 alwaysShow
属性,如何存在,则返回 false
,不展示 template
菜单,也就是只要配置了 alwaysShow
属性就会直接进入 el-submenu
组件
- 对于
hasOneShowingChild
方法,children
是 router
对象的 children
属性,item
是 router
对象,代码如下所示:
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
this.onlyOneChild = item
return true
}
})
if (showingChildren.length === 1) {
return true
}
if (showingChildren.length === 0) {
this.onlyOneChild = {
... parent, path: '', noShowingChildren: true }
return true
}
return false
}
- 对于它们之间的关系,如下所示:
- 如果展示
template
组件,首先会展示 app-link
组件,然后是 el-menu-item
,最里面嵌套的是 item
组件。item
组件需要 meta
中包含 title
和 icon
属性,否则将渲染内容为空的 vnode
对象。
- 如果
template
菜单不展示,则展示 el-submenu
菜单,el-submenu
逻辑中采用了嵌套组件的做法,将 sidebar-item
嵌套在 el-submenu
中
el-submenu
中的 sidebar-item
的区别,第一个是传入 is-nest
参数,第二个是传入 base-path
参数
sidebar-item
,代码如下所示:
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import {
isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: {
Item, AppLink },
mixins: [FixiOSBug],
props: {
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {
}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
this.onlyOneChild = item
return true
}
})
if (showingChildren.length === 1) {
return true
}
if (showingChildren.length === 0) {
this.onlyOneChild = {
... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
-
app-link
,是一个动态组件,通过 to
参数,如果包含 http
前缀则变成一个 a
标签,否则变成一个 router-link
组件。
-
app-link
,代码如下所示:
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
import {
isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
return {
href: to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
}
}
</script>
-
item
组件,通过定义的 render
函数完成组件渲染。
-
item
,代码如下所示:
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const {
icon, title } = context.props
const vnodes = []
if (icon) {
if (icon.includes('el-icon')) {
vnodes.push(<i class={
[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={
icon}/>)
}
}
if (title) {
vnodes.push(<span slot='title'>{
(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>
三、侧边栏实现总结
sidebar
主要包括 el-menu
容器组件,el-menu
中遍历 vuex
中的 routes
,生成 sidebar-item
组件。sidebar
主要配置如下所示:
activeMenu
:根据当前路由的 meta.activeMenu
属性控制侧边栏中高亮菜单
isCollapse
:根据 Cookie
的 sidebarStatus
控制侧边栏是否折叠
variables
:通过 @style/variables.css
填充 el-menu
的基本样式
sidebar-item
,分为两个部分,如下所示:
- 第一部分是当只需要展示一个
children
或者没有 children
时进行展示,展示的组件包括:
app-link
:动态组件,path
为链接时,显示为 a
标签,path
为路由时,显示为 router-link
组件
el-menu-item
:菜单项,当 sidebar-item
为非 nest
组件时,el-menu-item
会增加 submenu-title-noDropdown
的 class
item
:el-menu-item
里的内容,主要是 icon
和 title
,当 title
为空时,整个菜单项将不会展示
- 第二部分是当
children
超过两项时进行展示,展示的组件包括:
el-submenu
:子菜单组件容器,用于嵌套子菜单组件
sidebar-item
:el-submen
迭代嵌套了 sidebar-item
组件,在 sidebar-item
组件中的变化,设置了 is-nest
属性为 true
,根据 child.path
生成了 base-path
属性传入 sidebar-item
组件