1、添加store

        我们先添加几个store状态,后续需要用来共享使用

        首先,我们在store/modules下的app.js中添加一个menuRouteLoaded状态,来判断是否加载过路由

springBoot项目动态图表 springboot动态菜单_List

        然后在store、modules下新建一个menu.js,在index.js中引入,里面保存加载后的导航菜单数据。

menu.js代码如下:

export default {
state: {
navTree: [], // 导航菜单树
},
getters: {
},
mutations: {
setNavTree(state, navTree){ // 设置导航菜单树
state.navTree = navTree;
}
},
actions: {
}
}
在store/modules下新建一个user.js,在index.js中引入,里面保存加载后的用户权限数据。
user.js代码如下:
export default {
state: {
perms: [], // 用户权限标识集合
},
getters: {
},
mutations: {
setPerms(state, perms){ // 用户权限标识集合
state.perms = perms;
}
},
actions: {
}
}
index.js引入后的代码如下:
import Vue from 'vue'
 import vuex from 'vuex'Vue.use(vuex);
// 引入子模块
 import app from './modules/app'
 import user from './modules/user'
 import menu from './modules/menu'const store = new vuex.Store({
     modules: {
         app: app,
         user: user,
         menu: menu
     }
 })

export default store

2、登录页面

打开登录页面Login.vue, 在登录接口设置菜单加载状态,要求重新登录之后重新加载菜单,

springBoot项目动态图表 springboot动态菜单_springBoot项目动态图表_02

 3、导航守卫

        路由对象router给我们提供beforeEach方法,可以在每次路由之前进行一些相关处理,也叫导航守卫,我们这里就通过导航守卫实现动态菜单的加载。

        修改router/index.js文件,添加导航守卫,在每次路由时判断用户会话是否过期。如果登录有效且跳转到登录界面,就直接路由到主页;如果是非登录页面且会话过期,就跳到登录页面要求登录;否则,加载动态菜单和路由并路由到目标页面。

        下面是router下index.js代码

import Vue from 'vue'
 import Router from 'vue-router'
 import Login from '@/views/Login'
 import Home from '@/views/Home'
 import NotFound from '@/views/404'
 import api from '@/http/api'
 import store from '@/store'Vue.use(Router)
const router = new Router({
   routes: [
     {
       path: '/',
       name: '首页',
       component: Home,
       children: []
     },
     {
       path: '/login',
       name: '登录',
       component: Login
     },
     {
       path: '/404',
       name: 'notFound',
       component: NotFound
     }
   ]
 })router.beforeEach((to, from, next) => {
   // 登录界面登录成功之后,会把用户信息保存在会话
   // 存在时间为会话生命周期,页面关闭即失效。
   let userName = sessionStorage.getItem('user')
   if (to.path === '/login') {
     // 如果是访问登录界面,如果用户会话信息存在,代表已登录过,跳转到主页
     if(userName) {
       next({ path: '/' })
     } else {
       next()
     }
   } else {
     if (!userName) {
       // 如果访问非登录界面,且户会话信息不存在,代表未登录,则跳转到登录界面
       next({ path: '/login' })
     } else {
       // 加载动态菜单和路由
       addDynamicMenuAndRoutes(userName, to, from)
       next()
     }
   }
 })/**
 * 加载动态菜单和路由
 */
 function addDynamicMenuAndRoutes(userName, to, from) {
   if(store.state.app.menuRouteLoaded) {
     console.log('动态菜单和路由已经存在.')
     return
   }
   api.menu.findNavTree({'userName':userName})
   .then(res => {
     // 添加动态路由
     let dynamicRoutes = addDynamicRoutes(res.data)
     router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)
     router.addRoutes(router.options.routes)
     // 保存加载状态
     store.commit('menuRouteLoaded', true)
     // 保存菜单树
     store.commit('setNavTree', res.data)
   }).then(res => {
     api.user.findPermissions({'name':userName}).then(res => {
       // 保存用户权限标识集合
       store.commit('setPerms', res.data)
     })
   })
   .catch(function(res) {
   })
 }/**
 * 添加动态(菜单)路由
 * @param {*} menuList 菜单列表
 * @param {*} routes 递归创建的动态(菜单)路由
 */
 function addDynamicRoutes (menuList = [], routes = []) {
  var temp = []
  for (var i = 0; i < menuList.length; i++) {
    if (menuList[i].children && menuList[i].children.length >= 1) {
      temp = temp.concat(menuList[i].children)
    } else if (menuList[i].url && /\S/.test(menuList[i].url)) {
       menuList[i].url = menuList[i].url.replace(/^\//, '')
       // 创建路由配置
       var route = {
         path: menuList[i].url,
         component: null,
         name: menuList[i].name,
         meta: {
           icon: menuList[i].icon,
           index: menuList[i].id
         }
       }
       try {
         // 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储
         // 如url="sys/user",则组件路径应是"@/views/sys/user.vue",否则组件加载不到
         let array = menuList[i].url.split('/')
         let url = ''
         for(let i=0; i<array.length; i++) {
           url += array[i].substring(0,1).toUpperCase() + array[i].substring(1) + '/'
         }
         url = url.substring(0, url.length - 1)
         route['component'] = resolve => require([`@/views/${url}`], resolve)
       } catch (e) {}
       routes.push(route)
    }
  }
  if (temp.length >= 1) {
    addDynamicRoutes(temp, routes)
  } else {
    console.log('动态路由加载...')
    console.log(routes)
    console.log('动态路由加载完成.')
  }
  return routes
 }

export default router

4、导航树组件

        在components目录下新建一个导航树组件MenuTree,如下图:

springBoot项目动态图表 springboot动态菜单_List_03

 代码如下:

<template>
<el-submenu v-if="menu.children && menu.children.length >= 1" :index="'' + menu.id">
<template slot="title">
<i :class="menu.icon" ></i>
<span slot="title">{{menu.name}}</span>
</template>
<MenuTree v-for="item in menu.children" :key="item.id" :menu="item"></MenuTree>
</el-submenu>
<el-menu-item v-else :index="'' + menu.id" @click="handleRoute(menu)">
<i :class="menu.icon"></i>
<span slot="title">{{menu.name}}</span>
</el-menu-item>
</template>
<script>
import { getIFrameUrl, getIFramePath } from '@/utils/iframe'
export default {
name: 'MenuTree',
props: {
menu: {
type: Object,
required: true
}
},
methods: {
handleRoute (menu) {
// 通过菜单URL跳转至指定路由
this.$router.push("/" + menu.url)
}
}
}
</script>
<style scoped lang="scss">
</style>
5、在navBar.vue中添加导航菜单
添加后代码如下:
<template>
  <div class="menu-bar-container">
<!-- logo -->
<div class="logo" :style="{'background':themeColor}" @click="$router.push('/')"
:class="collapse?'menu-bar-collapse-width':'menu-bar-width'">
<img v-if="collapse" src="@/assets/logo.png"/> <div>{{collapse?'':appName}}</div>
</div>
<!-- 导航菜单 -->
<el-menu ref="navmenu" default-active="1" :class="collapse?'menu-bar-collapse-width':'menu-bar-width'"
:collapse="collapse" :collapse-transition="false" :unique-opened="true "
@open="handleopen" @close="handleclose" @select="handleselect">
<!-- 导航菜单树组件,动态加载菜单 -->
<menu-tree v-for="item in navTree" :key="item.id" :menu="item"></menu-tree>
</el-menu>
  </div>
</template>
<script>
import { mapState } from 'vuex'
import MenuTree from "@/components/MenuTree"
export default {
components:{
MenuTree
},
computed: {
...mapState({
appName: state=>state.app.appName,
themeColor: state=>state.app.themeColor,
collapse: state=>state.app.collapse,
navTree: state=>state.menu.navTree
})
},
methods: {
handleopen() {
console.log('handleopen')
},
handleclose() {
console.log('handleclose')
},
handleselect(a, b) {
console.log('handleselect')
}
}
}
</script>
<style scoped lang="scss">
.menu-bar-container {
position: fixed;
top: 0px;
left: 0;
bottom: 0;
z-index: 1020;
.el-menu {
position:absolute;
top: 60px;
bottom: 0px;
text-align: left;
// background-color: #2968a30c;
}
.logo {
position:absolute;
top: 0px;
height: 60px;
line-height: 60px;
background: #545c64;
cursor:pointer;
img {
width: 40px;
height: 40px;
border-radius: 0px;
margin: 10px 10px 10px 10px;
float: left;
}
div {
font-size: 25px;
color: white;
text-align: left;
padding-left: 20px;
}
}
.menu-bar-width {
width: 200px;
}
.menu-bar-collapse-width {
width: 65px;
}
}
</style>

好了,到此我们已经完成了动态菜单,下面我们进行测试

启动项目后,我们访问:http://localhost:8080/#/login,点击登录

springBoot项目动态图表 springboot动态菜单_加载_04

 

springBoot项目动态图表 springboot动态菜单_导航菜单_05

 可以看到我们菜单已经加载好了

好啦,到此我们完成了动态菜单加载