结合Pinia实现动态菜单
图
在管理后台系统中,菜单是最基础的功能,通常搭建框架都会考虑做一个适合自己项目的架构,后来结合Router、ElementPlus、Pinia做了一个通用的菜单架构,支持多级菜单

Router

import {createRouter, createWebHistory} from 'vue-router'
import other from "./other"

const router = createRouter({
  // history模式:createWebHistory(), hash模式:createWebHashHistory()
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: () => import('../view/Home.vue'),
      redirect: '/home/indexShow',
      children: [
        /**
         * 说明:
         *    menuLevel 为导航级别,按顺序写
         */
        {
          path: 'indexShow',
          name: '数据概况',
          menuLevel: 1,
          component: () => import('../components/Index/IndexShow.vue'),
          icon: 'histogram',
        },
        {
          path: 'system',
          name: '系统管理',
          menuLevel: 1,
          icon: 'cpu',
        },
        {
          path: 'system/adminUser',
          name: '管理员管理',
          menuLevel: 2,
          component: () => import('../components/system/AdminUser.vue'),
        },
        // --------------------------分------割-------------------------------
        {
          path: 'breakRouter',
          menuLevel: 4,
        },
        ...other,
      ]
    },
    // 登录页面
    {
      path: '/',
      component: () => import('../view/Login.vue'),
    },
  ],
})

export default router

还有非菜单的路由

/**
 * 不属于菜单的路由
 */
export default [
  {
    path: '/trading/details',
    component: () => import('../components/HelloWorld.vue')
  },
]

Store

import {defineStore} from 'pinia'

export const useStore = defineStore('storeId', {
  // 推荐使用 完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
  // 开启数据持久化
  // persist: true
})

export const sysStore = defineStore('sysStore', {
  state: () => {
    return {
      // 菜单树
      menuTree: [],
      // 激活的菜单项
      menuIndex: '',
      // 是否折叠菜单
      menuCollapse: false,
    }
  },
  // 开启数据持久化
  // persist: true
})

菜单组件

MenuComponent.vue:

<!--菜单-->
<template>
  <div class="home-menu">
    <el-menu
        :default-active="sysStoreData.menuIndex"
        router
        background-color="#304156"
        text-color="#ddd"
        active-text-color="#409eff"
        :collapse="sysStoreData.menuCollapse">
      <MenuTree :menuList="sysStoreData.menuTree"></MenuTree>
    </el-menu>
  </div>
</template>

<script setup>
import router from '../../router'
import MenuTree from './MenuTree.vue'
import {sysStore} from '../../store'
const sysStoreData = sysStore()

menu()

// 菜单计算(将路由转为为菜单树)
function menu() {
  // 获取所有菜单列表
  let menus = router.options.routes[0].children
  let oneItem = {}
  let twoItem = {}
  let oneMenu = []
  let twoMenu = []
  let threeMenu = []
  for (let item of menus) {
    // 分割
    if (item.menuLevel === 4) {
      break
    }
    item.path = '/home/' + item.path
    if (item.menuLevel === 1) {
      if (threeMenu.length > 0) {
        twoItem.ch = threeMenu
        twoMenu.push(twoItem)
        twoItem = {}
        threeMenu = []
      }
      if (twoMenu.length > 0) {
        oneItem.ch = twoMenu
        oneMenu.push(oneItem)
        oneItem = {}
        twoMenu = []
      }
      if (item.component) {
        // 该级别可点击,校验是否有权限
        menuShowRule(item, () => {
          // 没有下级,直接添加进最后数据
          oneMenu.push(item)
        })
      } else {
        // 不可点击
        oneItem = item
      }
    } else if (item.menuLevel === 2) {
      if (threeMenu.length > 0) {
        twoItem.ch = threeMenu
        twoMenu.push(twoItem)
        twoItem = {}
        threeMenu = []
      }
      if (item.component) {
        // 该级别可点击,校验是否有权限
        menuShowRule(item, () => {
          // 没有下级,直接添加进数据
          twoMenu.push(item)
        })
      } else {
        // 不可点击
        twoItem = item
      }
      twoItem = item
    } else if (item.menuLevel === 3) {
      menuShowRule(item, () => {
        threeMenu.push(item)
      })
    }
  }
  if (threeMenu.length > 0) {
    twoItem.ch = threeMenu
    twoMenu.push(twoItem)
  }
  if (twoMenu.length > 0) {
    oneItem.ch = twoMenu
    oneMenu.push(oneItem)
  }
  // 保存菜单
  sysStoreData.menuTree = oneMenu
  console.log(oneMenu)
  // 首次展示
  if (sysStoreData.menuIndex === '') {
    sysStoreData.menuIndex = oneMenu[0].path
  }
}

/**
 * 定义是否显示的规则
 */
function menuShowRule(item, fun) {
  fun()
}
</script>

<style scoped lang="sass">
// 菜单高度
.el-menu-item
  height: 45px

.el-sub-menu
  height: 45px

// 去除菜单边框
.el-menu
  border-right: none
</style>

MenuTree.vue:

<template>
  <template v-for="menu in menuList">
    <!--没有子菜单-->
    <el-menu-item v-if="menu.component" :index="menu.path" @click="sysStoreData.menuIndex = menu.path">
      <el-icon>
        <component :is="menu.icon"></component>
      </el-icon>
      <span>{{ menu.name }}</span>
    </el-menu-item>
    <!--有子菜单-->
    <el-sub-menu
        v-else
        :index="menu.path">
      <template #title>
        <el-icon>
          <component :is="menu.icon"></component>
        </el-icon>
        <span>{{ menu.name }}</span>
      </template>
      <!--递归调用自身-->
      <MenuTree :menuList="menu.ch"></MenuTree>
    </el-sub-menu>
  </template>
</template>

<script setup>
import {sysStore} from '../../store'
const sysStoreData = sysStore()

defineProps({
  menuList: {
    type: Array,
    required: false,
  }
})
</script>

<style scoped>

</style>

菜单升级和BUG修复

出现问题:无法支持4级菜单,所以进行了升级,同时上面方式有个BUG,当菜单中出现相同名字时,无法进行跳转,是由于router的name不能重复造成,以下是升级和修复后的代码

router

import {createRouter, createWebHashHistory} from 'vue-router'
import other from "./other"
import api from '../http/api.js'
import {sysStore, adminStore} from '../store'

const router = createRouter({
  // history模式:createWebHistory(), hash模式:createWebHashHistory()
  history: createWebHashHistory(),
  routes: [
    {
      path: '/home',
      component: () => import('../view/Home.vue'),
      redirect: '/home/indexShow',
      children: [
        /**
         * 说明:
         *    menuLevel 为导航级别,按顺序写
         *
         * 说明:
         *    path          路径
         *    name          路由名,不可重复,与path一致
         *    menuName      菜单名
         *    menuLevel     导航级别,从上往下按顺序写
         *    component     页面组件
         *    icon          图标,仅一级导航有做显示
         *    auth          权限
         *    ch  组件内按钮权限,用在授权处
         */
        {
          path: 'indexShow',
          name: 'indexShow',
          menuName: '数据概况',
          menuLevel: 1,
          component: () => import('../components/index/IndexShow.vue'),
          icon: 'histogram',
        },
        {
          path: 'system',
          name: 'system',
          menuName: '系统管理',
          menuLevel: 1,
          icon: 'cpu',
        },
        {
          path: 'system/adminUser',
          name: 'system/adminUser',
          menuName: '管理员管理',
          menuLevel: 2,
          component: () => import('../components/system/AdminUser.vue'),
          auth: api.system_admin_user_list,
          ch: [
            {name: '添加管理员', auth: api.system_admin_user_add},
            {name: '编辑管理员', auth: api.system_admin_user_edit},
            {name: '删除管理员', auth: api.system_admin_user_del},
            {name: '密码重置', auth: api.system_admin_reset_password},
          ],
        },
        {
          path: 'system/adminRole',
          name: 'system/adminRole',
          menuName: '管理员角色及权限',
          menuLevel: 2,
          component: () => import('../components/system/AdminRole.vue'),
          auth: api.system_admin_role_list,
          ch: [
            {name: '添加角色', auth: api.system_admin_role_add},
            {name: '编辑角色', auth: api.system_admin_role_edit},
            {name: '删除角色', auth: api.system_admin_role_del},
          ],
        },
        {
          path: 'system/adminUserLog',
          name: 'system/adminUserLog',
          menuName: '管理员日志',
          menuLevel: 2,
          component: () => import('../components/system/AdminLog.vue'),
          auth: api.system_admin_log_list,
        },
        {
          path: 'reagent',
          name: 'reagent',
          menuName: '试剂耗材管理',
          menuLevel: 1,
          icon: 'Tickets',
        },
        {
          path: 'reagentCommon',
          name: 'reagentCommon',
          menuName: '普通试剂',
          menuLevel: 2,
        },
        {
          path: 'reagentCommonPut',
          name: 'reagentCommonPut',
          menuName: '管理人员入库',
          menuLevel: 3,
          component: () => import('../components/reagent/ReagentPutRecord.vue'),
          auth: api.system_admin_user_list,
          ch: [
            {name: '添加', auth: api.system_admin_user_add},
            {name: '编辑管理员', auth: api.system_admin_user_edit},
            {name: '删除管理员', auth: api.system_admin_user_del},
            {name: '密码重置', auth: api.system_admin_reset_password},
          ],
        },
        {
          path: 'chemistry',
          name: 'chemistry',
          menuName: '危险化学品',
          menuLevel: 2,
        },
        {
          path: 'chemistryPoison',
          name: 'chemistryPoison',
          menuName: '易制毒',
          menuLevel: 3,
        },
        {
          path: 'chemistryPoisonPut',
          name: 'chemistryPoisonPut',
          menuName: '管理人员入库',
          menuLevel: 4,
          component: () => import('../components/reagent/ReagentPutRecord2.vue'),
          auth: api.system_admin_user_list,
          ch: [
            {name: '添加', auth: api.system_admin_user_add},
            {name: '编辑管理员', auth: api.system_admin_user_edit},
            {name: '删除管理员', auth: api.system_admin_user_del},
            {name: '密码重置', auth: api.system_admin_reset_password},
          ],
        },
        
        // --------------------------分------割-------------------------------
        {
          path: 'breakRouter',
          menuLevel: 5,
        },
        ...other,
      ]
    },
    {
      path: '/',
      component: () => import('../view/Login.vue'),
    },
  ],
})

router.beforeEach((to, from, next) => {
  let sysStoreData = sysStore()
  let adminStoreData = adminStore()
  if (to.path === '/' || adminStoreData.token.length > 0) {
    next()
  } else {
    next('/')
  }
})

export default router

store

import {defineStore} from 'pinia'

export const useStore = defineStore('storeId', {
  // 推荐使用 完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
  // 开启数据持久化
  persist: true
})

// 动态菜单
export const sysStore = defineStore('sysStore', {
  state: () => {
    return {
      // 菜单树
      menuTree: [],
      // 激活的菜单项
      menuIndex: '',
      // 是否折叠菜单
      menuCollapse: false,
    }
  },
  // 开启数据持久化
  persist: true
})


// 动态菜单
export const adminStore = defineStore('adminStore', {
  state: () => {
    return {
      // 管理员ID
      id: '',
      // 登录Token
      token: '',
      // 权限值
      auth: [],
      // 昵称
      nick: '',
      // 账号类型:1-超级管理员、2-普通管理员
      type: '',
      // 账号级别
      level: '',
      // 是否超级管理员
      superAdmin: false,
    }
  },
  // 开启数据持久化
  persist: true
})

组件MenuComponent.vue

<!--菜单-->
<template>
  <div class="home-menu">
    <el-menu
      :default-active="sysStoreData.menuIndex"
      router
      background-color="#304156"
      text-color="#ddd"
      active-text-color="#409eff"
      :collapse="sysStoreData.menuCollapse">
      <MenuTree :menuList="sysStoreData.menuTree"></MenuTree>
    </el-menu>
  </div>
</template>

<script setup>
import router from '../../router'
import MenuTree from './MenuTree.vue'
import {sysStore} from '../../store'

const sysStoreData = sysStore()

menu()

// 菜单计算(将路由转为为菜单树)
function menu() {
  // 获取所有菜单列表
  let menus = router.options.routes[0].children
  let oneItem = {}
  let twoItem = {}
  let threeItem = {}
  let oneMenu = []
  let twoMenu = []
  let threeMenu = []
  let fourMenu = []
  // 遍历菜单
  for (let item of menus) {
    // 分割
    if (item.menuLevel === 5) {
      break
    }
    item.path = '/home/' + item.path
    if (item.menuLevel === 1) {
      if (fourMenu.length > 0) {
        threeItem.ch = fourMenu
        threeMenu.push(threeItem)
        threeItem = {}
        fourMenu = []
      }
      if (threeMenu.length > 0) {
        twoItem.ch = threeMenu
        twoMenu.push(twoItem)
        twoItem = {}
        threeMenu = []
      }
      if (twoMenu.length > 0) {
        oneItem.ch = twoMenu
        oneMenu.push(oneItem)
        oneItem = {}
        twoMenu = []
      }
      if (item.component) {
        // 该级别可点击,校验是否有权限
        menuShowRule(item, () => {
          // 没有下级,直接添加进最后数据
          oneMenu.push(item)
        })
      } else {
        // 不可点击,属于目录
        oneItem = item
      }
    } else if (item.menuLevel === 2) {
      if (fourMenu.length > 0) {
        threeItem.ch = fourMenu
        threeMenu.push(threeItem)
        threeItem = {}
        fourMenu = []
      }
      if (threeMenu.length > 0) {
        twoItem.ch = threeMenu
        twoMenu.push(twoItem)
        twoItem = {}
        threeMenu = []
      }
      if (item.component) {
        // 该级别可点击,校验是否有权限
        menuShowRule(item, () => {
          // 没有下级,直接添加进数据
          twoMenu.push(item)
        })
      } else {
        // 不可点击,属于目录
        twoItem = item
      }
    } else if (item.menuLevel === 3) {
      if (fourMenu.length > 0) {
        threeItem.ch = fourMenu
        threeMenu.push(threeItem)
        threeItem = {}
        fourMenu = []
      }
      if (item.component) {
        // 该级别可点击,校验是否有权限
        menuShowRule(item, () => {
          // 没有下级,直接添加进数据
          threeMenu.push(item)
        })
      } else {
        // 不可点击,属于目录
        threeItem = item
      }
    } else if (item.menuLevel === 4) {
      menuShowRule(item, () => {
        fourMenu.push(item)
      })
    }
  }
  if (fourMenu.length > 0) {
    threeItem.ch = fourMenu
    threeMenu.push(threeItem)
  }
  if (threeMenu.length > 0) {
    twoItem.ch = threeMenu
    twoMenu.push(twoItem)
  }
  if (twoMenu.length > 0) {
    oneItem.ch = twoMenu
    oneMenu.push(oneItem)
  }
  // 保存菜单
  sysStoreData.menuTree = oneMenu
  console.log(oneMenu)
  // 首次展示
  if (sysStoreData.menuIndex === '') {
    sysStoreData.menuIndex = oneMenu[0].path
  }
}

/**
 * 定义是否显示的规则
 */
function menuShowRule(item, fun) {
  fun()
}
</script>

<style scoped lang="sass">
// 菜单高度
.el-menu-item
  height: 45px

.el-sub-menu
  height: 45px

// 去除菜单边框
.el-menu
  border-right: none
</style>

组件MenuTree.vue

<template>
  <template v-for="menu in menuList">
    <!--没有子菜单-->
    <el-menu-item v-if="menu.component" :index="menu.path" @click="sysStoreData.menuIndex = menu.path">
      <el-icon>
        <component :is="menu.icon"></component>
      </el-icon>
      <span>{{ menu.menuName }}</span>
    </el-menu-item>
    <!--有子菜单-->
    <el-sub-menu
      v-else
      :index="menu.path">
      <template #title>
        <el-icon>
          <component :is="menu.icon"></component>
        </el-icon>
        <span>{{ menu.menuName }}</span>
      </template>
      <!--递归调用自身-->
      <MenuTree :menuList="menu.ch"></MenuTree>
    </el-sub-menu>
  </template>
</template>

<script setup>
import {sysStore} from '../../store'

const sysStoreData = sysStore()

defineProps({
  menuList: {
    type: Array,
    required: false,
  }
})
</script>

<style scoped>

</style>