vue中使用遞歸組件實(shí)現(xiàn)無(wú)限級(jí)菜單

在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)的組件:

  1. 如果是菜單的最后一級(jí),就用el-menu-item
  2. 如果是菜單目錄下面還有子菜單的,就用el-submenu,該組件下面可以再嵌套el-submenuel-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-submenuel-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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容