在vue項(xiàng)目中,特別是后臺(tái)管理系統(tǒng),通常會(huì)有用戶權(quán)限控制,每個(gè)用戶的權(quán)限是不同的,所以展示的菜單不同,通常的做法是生成不同的路由表,然后我們通過(guò)這個(gè)路由表去生成菜單,如果菜單的層級(jí)結(jié)構(gòu)固定,那可以采用普通的寫法,那如果菜單層級(jí)不確定的話,就得采用遞歸組件來(lái)實(shí)現(xiàn)。下面超人鴨講解自己寫的一個(gè)demo,是模仿vue-element-admin這個(gè)模板的邏輯寫的,加以簡(jiǎn)化。
菜單所使用的ui組件是element-ui的el-menu組件。
菜單往大了說(shuō)其實(shí)就兩種狀態(tài),一種是點(diǎn)擊進(jìn)行跳轉(zhuǎn)的,通常是菜單的最后一級(jí);另一種是菜單目錄,點(diǎn)擊之后下面還有子菜單,一般可以進(jìn)行收縮。所以基本思路就是將某一個(gè)路由信息傳遞進(jìn)組件,組件里面去判斷這個(gè)路由信息,看是要生成最后一級(jí)的菜單還是菜單目錄,如果是菜單目錄,再去遞歸。
element-ui中的el-menu組件就有符合這兩種狀態(tài)的組件:
- 如果是菜單的最后一級(jí),就用el-menu-item
- 如果是菜單目錄下面還有子菜單的,就用el-submenu,該組件下面可以再嵌套el-submenu和el-menu-item
上面就是基本的思路,下面是實(shí)現(xiàn)
根據(jù)權(quán)限生成路由表這部分不再主題內(nèi),就省略掉,先看看路由信息:
export const routes = [
{
path: '/login',
component: Main,
hidden: true
},
{
path: '/',
component: Main,
redirect: '/one',
children: [
{
path: 'one',
component: Test,
meta: { title: '菜單一', icon: 'el-icon-setting' }
}
]
},
{
path: '/two',
component: Main,
meta: { title: '菜單二', icon: 'el-icon-setting' },
children: [
{
path: 'index',
component: Test,
meta: { title: '菜單二-1' }
}
]
},
{
path: '/there',
component: Main,
meta: { title: '菜單三', icon: 'el-icon-setting' },
children: [
{
path: 'one',
component: Test,
meta: { title: '菜單三-1' }
},
{
path: 'two',
component: Test,
meta: { title: '菜單三-2' },
children: [
{
path: 'one',
component: Test,
meta: { title: '菜單三-1' }
},
{
path: 'two',
component: Test,
meta: { title: '菜單三-2' }
}
]
}
]
},
{
path: '/four',
component: Main,
meta: { title: '菜單四', icon: 'el-icon-setting' },
children: [
{
path: 'one',
component: Test,
meta: { title: '菜單四-1' }
},
{
path: 'there',
component: Test,
meta: { title: '菜單四-3' },
hidden: true
}
]
},
{
path: '/five',
component: Main,
alwaysShow: true,
meta: { title: '菜單五', icon: 'el-icon-setting' },
children: [
{
path: 'one',
component: Test,
meta: { title: '菜單五-1' }
}
]
}
]
其中的component屬性都是測(cè)試用的寫得不規(guī)范可以忽略,其中有幾個(gè)屬性這里說(shuō)明一下:
- hidden:代表該條路由不在菜單展示,一般為登錄頁(yè),404頁(yè)等
-
alwaysShow: 代表該路由信息要作為菜單目錄顯示,就是可以收縮,下面還有子路由,通常當(dāng)一個(gè)菜單下面只有一個(gè)子菜單的時(shí)候就不做分級(jí)展示,所以在組件里面是通過(guò)alwaysShow這個(gè)屬性控制的。
先看看頁(yè)面展示效果:
image.png
結(jié)合上面的路由信息,關(guān)注菜單四這項(xiàng),菜單四的路由信息有兩個(gè)子路由,但是其中一個(gè)hidden屬性為true,不展示在菜單上,所以它只有一個(gè)子菜單,同時(shí)沒(méi)有alwaysShow屬性,就不作分級(jí)展示,直接顯示一級(jí)菜單。而菜單五因?yàn)橛衋lwaysShow為true,所以盡管只有一個(gè)子菜單,但還是進(jìn)行分級(jí)展示。
超人鴨描述能力很差,希望到這你們能看懂我描述的功能。^ - ^
下面是具體的實(shí)現(xiàn)代碼,我會(huì)結(jié)合注釋講解。
首先element-ui的菜單組件,里面的屬性可以到element官網(wǎng)看看解釋,無(wú)關(guān)這個(gè)主題。下面是菜單的入口組件:
<template>
<div class="side-bar-index">
<el-scrollbar style="height:100%">
<el-menu
:default-active="$route.path"
background-color="#304156"
text-color="#bfcbd9"
:unique-opened="false"
active-text-color="#409EFF"
:collapse-transition="false"
mode="vertical"
>
<!-- 下面是主要實(shí)現(xiàn)功能的組件 -->
<sidebar-item
v-for="route in isShowRoutes"
:key="route.path"
:item="route"
:base-path="route.path"
></sidebar-item>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import SidebarItem from './SidebarItem'
import { routes } from '@/router/index'
export default {
components: {
SidebarItem
},
computed: {
// 過(guò)濾hidden的路由
isShowRoutes () {
return routes.filter((item) => {
return !item.hidden
})
}
}
}
</script>
這個(gè)組件就是用了element的菜單組件,并將路由過(guò)濾了一遍,主要實(shí)現(xiàn)在SidebarItem這個(gè)組件里面,這里使用v-for將每一個(gè)路由對(duì)象傳遞進(jìn)去,上面也說(shuō)到,菜單只有兩個(gè)狀態(tài),對(duì)應(yīng)的組件是element-ui的el-menu-item、el-submenu,而且el-submenu組件下面能在嵌套el-submenu和el-menu-item組件,所以在SidebarItem這個(gè)組件里面的基本邏輯就是:
<el-menu-item v-if="....."></el-menu-item>
<el-submenu v-else>
<!-- 遞歸 -->
<sidebar-item></<sidebar-item>
</el-submenu>
// 邏輯處理
然后在組件的create函數(shù)里面判斷傳遞進(jìn)來(lái)的路由對(duì)象是渲染成el-menu-item還是el-submenu,下面放上SidebarItem組件的完整代碼:
<template>
<!-- 限制:路由定義的meta必須要有title字段 -->
<div>
<!-- 該組件進(jìn)來(lái)只會(huì)走一種情況,走了菜單邏輯就不再遞歸 -->
<!-- 菜單 -->
<template v-if="isShowOneMenu">
<el-menu-item :index="resolvePath(data.path)">
<i v-if="data.meta.icon" :class="data.meta.icon"></i>
<span @click="handleClick(resolvePath(data.path))">{{data.meta.title}}</span>
</el-menu-item>
</template>
<!-- 菜單目錄(可收縮) -->
<el-submenu v-else :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<i :class="item.meta.icon"></i>
<span>{{item.meta.title}}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(child.path)"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
export default {
name: 'SidebarItem',
props: {
item: {
type: Object
},
basePath: {
type: String,
default: ''
}
},
data () {
return {
data: {}, // 當(dāng)前組件處理過(guò)后的菜單信息
isShowOneMenu: false // 判斷渲染el-menu-item還是el-submenu的標(biāo)識(shí)
}
},
methods: {
/**
* 區(qū)分路由是否是最后一層或者只有一個(gè)子路由
* 當(dāng)路由只有一個(gè)子路由時(shí)有兩種情況,一種是不展示收縮了,直接將唯一的子路由作為菜單;另一種是展示收縮,展開(kāi)下面只有一個(gè)子菜單的。由在路由信息中的屬性alwaysShow定義。
*/
handleRoute () {
// 當(dāng)遞歸到最后一層,路由已經(jīng)沒(méi)有children了
if (!this.item.children) {
this.isShowOneMenu = true
this.data = { ...this.item, path: '' }
return
}
// 過(guò)濾掉children中hidden的
const showingChildren = this.item.children.filter(item => {
return !item.hidden
})
// 此時(shí)要給item的children賦值,賦值為過(guò)濾完hidden的
this.item.children = showingChildren
// 當(dāng)只有一個(gè)子路由并且不展示收縮時(shí)做菜單處理,并且這個(gè)子路由沒(méi)有children屬性時(shí)
if (showingChildren.length === 1 && !showingChildren[0].children && !this.item.alwaysShow) {
this.isShowOneMenu = true
this.data = showingChildren[0]
// 如果該路由有meta信息與子路由的meta信息做結(jié)合,已子路由的meta信息優(yōu)先
if (this.item.meta) {
this.data.meta.title = this.data.meta.title ? this.data.meta.title : this.item.meta.title
this.data.meta.icon = this.data.meta.icon ? this.data.meta.icon : this.item.meta.icon
}
}
},
resolvePath (routePath) {
return path.resolve(this.basePath, routePath)
},
handleClick (path) {
this.$router.push(path)
}
},
created () {
this.handleRoute()
}
}
</script>
其中大部分邏輯處理我都寫在代碼注釋里,最主要的還是handleRoute這個(gè)方法里面的邏輯,通過(guò)處理this.isShowOneMenu這個(gè)變量來(lái)控制SidebarItem組件是渲染哪一種組件,而且只能是兩種組件中的一種,如果是做菜單目錄處理,就渲染el-submenu,里面再去嵌套SidebarItem組件,不斷的重復(fù)上面的邏輯。其中有個(gè)需要注意的點(diǎn),就是當(dāng)遞歸到最后一層的時(shí)候,是直接將路由信息賦值給當(dāng)前組件的data,需要把path清空,因?yàn)閭鬟f進(jìn)來(lái)的basePath已經(jīng)是最后一層路由的path了,這樣做拼接的時(shí)候才不會(huì)出錯(cuò)。
到這里使用遞歸組件實(shí)現(xiàn)無(wú)限級(jí)菜單就實(shí)現(xiàn)了,當(dāng)然這是按我的邏輯來(lái)實(shí)現(xiàn),其中有些不完美的地方,比如路由的meta中我要求必須要有title字段,當(dāng)路由設(shè)置了alwaysShow屬性,那當(dāng)層的路由對(duì)象也必須要有meta對(duì)象。當(dāng)然這些是可以根據(jù)你們具體的設(shè)置來(lái)修改。
如果之前沒(méi)有接觸過(guò)類似邏輯的組件,可能看著會(huì)比較亂,可以把超人鴨的代碼復(fù)制進(jìn)你們的demo嘗試一下,隨便修修改改一下就能理清的。
作者微信: Promise_fulfilled
