Vue 权限菜单(动态路由)详解

Posted 桃小瑞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 权限菜单(动态路由)详解相关的知识,希望对你有一定的参考价值。

今天记录一下Vue权限菜单(动态路由),在我们写后台的时候用的比较多,Vue的权限菜单分两种,一种是通过本地进行,根据账号的权限进行筛选出可用的权限,组合菜单并在页面上渲染显示,另一种是根据登录的账号,后端直接回可用的权限菜单,前端进行整合渲染。第二种在日常中使用比第一种的情况多些,本篇文章讲述记录的也是第二种方式。我这里使用的是 element-ui的Admin 模板。

一、新建请求的js

我这里是新建了一个请求的js,你们可以直接将这个方法写在你们现在的请求js里面。
src目录下新建一个文件夹api,新建是一个名request的js文件。

request.js代码:

//这个request 是模板自带的,封装了axios的request,在utils目录下
import request from '@/utils/request'

export default 
	//...其他的请求
  // 获取权限菜单列表
  getRoleMenu()
    return request(
      url: `项目请求url`,
      method: 'get',
    )
  ,
   // ...其他的请求
  //

二、新建一个获取菜单的js

src目录下新建一个文件夹menu,在menu下新建一个menu.js

menu.js代码:

import Layout from '@/layout'//引入admin的layout布局
import router from '../router'//引入router
import request from '../api/request.js'//自定义封装的请求
import store from '../store'//Vuex

export default 
  // 获取路由菜单
  getMenu() 
    return new Promise((resolve, reject) => 
      return request.getRoleMenu().then(res => 
      	/*  回来的数据格式参考
			// [
			//   type:1,//1表示有子路由,0则没有
			//   path: '路径',
			//   component: Layout,
			//   children: [
			//     path: '',
			//     name: '名称',
			//     component: '页面路径',
			//     meta: 
			//       title: '页面标题',
			//       icon: '图标'
			//     ,
			//		children: []//如果有子路由则需要写
			//   ]
			// ]
		*/
      	//声明一个空数组,用来装处理好的菜单信息
        const result = []
        // 获取到路由菜单,进行递归数据处理
        this.parseRoute(res.data, result, 1)
        // 添加菜单
        this.addMenu(result)
        //缓存用户菜单,我这里使用的是sessionStorage,用localStorage也可以
        sessionStorage.setItem("route", JSON.stringify(res.data))
        // 输出成功
        resolve()
      ).catch(err =>
        reject()
      )
    )
  ,
  // 对路由菜单递归数据处理
  parseRoute(fullList, resultList, step) 
    return new Promise((resolve, reject) => 
      let result = []
      // 路由类型申明,0表示没有二级路由,1表示有一级路由
      const typeArray = [0, 1]
      // 循环进行数据处理
      for (let i = 0; i < fullList.length; i++) 
        const itemElement = fullList[i]
        let routerObject = null
        const childrenList = []

        // 路由类型过滤,如果回来的路由类型不存在,则直接结束当前循环进行下一次循环
        if (!typeArray.includes(itemElement.type)) 
          continue
        
		//对当前执行步骤进行判断,step 等于1,则属于主路由,反之则属于子路由
        if (step === 1) 
          routerObject = 
            path: itemElement['routePath'],
            component: Layout,
            children: [
              path: itemElement['routePath'],
              name: itemElement['name'],
              component: resolve => require([`@/views$itemElement.componentsPath`], resolve),
              meta: 
                title: itemElement['name'],
                icon: itemElement['icon']
              ,
              children: childrenList
            ]
          
         else 
          routerObject = 
            path: itemElement['routePath'],
            name: itemElement['name'],
            component: resolve => require([`@/views$itemElement.componentsPath`], resolve),
            meta: 
              title: itemElement['name'],
              icon: itemElement['icon']
            ,
            children: childrenList
          
        

        const itemResult = []
        //对主路由尽心判断,满足条件则进行递归
        if (itemElement['nextMenuList'] != null && itemElement['nextMenuList'].length) 
          this.parseRoute(itemElement['nextMenuList'], itemResult, step + 1)
        
		//将组装好的子路由信息插入数组
        for (let j = 0; j < itemResult.length; j++) 
          childrenList.push(itemResult[j])
        
		//判断子路由数组长度
        if (childrenList.length === 0) //不存在子路由
          if (step === 1) //如果是主路由则删除第一个子路由,如果不删除则会出现一个空的父菜单
            delete routerObject.children[0].children
           else //如果是子路由,则删除子路由下的子路由,删除的这个子路由是空的,必须需要删除,否则也会出现一个空的菜单
            delete routerObject.children
          
         else //存在子路由
          if (step === 1) 如果是主路由,则添加meta和子路由列表
            routerObject.meta = routerObject.children[0].meta
            routerObject.children = childrenList
            // console.log(routerObject)
          
        
		//插入组装好的数据
        resultList.push(routerObject)
        //插入最终完整的数据列表
        result.push(routerObject)
      
      //输出已经组装好并且能用的数据
      resolve(result)
    )
  ,
  // 添加菜单
  addMenu(data) 
    return new Promise((resolve, reject) => 
      // 在处理完的菜单列表数据后面插入404页面,404必须存在菜单列表的最后一项,否则会对所有页面进行拦截,并跳转404页面
      data.push(
        path: '*',
        redirect: '/404',
        hidden: true
      )
      // 打印菜单列表
      // console.log(data)
      // 将可用的路由权限列表存入Vuex
      store.dispatch('user/modifyMenu', data)
      // 添加菜单
      router.addRoutes(data)
      // 将路由元注入路由对象,必须添加
      router.options.routes.push(data)
      //输出成功
      resolve()
    )
  ,
  //

三、修改Vuex

找到在store目录下的modules/user.js打开这个js文件,
引入刚刚创建的menu.js和请求的request.js

import request from "../../api/request.js"
import menu from "../../menu/menu.js"

state里添加一个menu,也就是getDefaultState()

const getDefaultState = () => 
  return 
    token: getToken(),
    name: '',
    avatar: '',
    menu: []//添加一个数组用来装菜单列表
  

并在mutations里添加一个SET_MENU的方法用来修改state里的菜单列表

  SET_MENU: (state,menu) =>
    state.menu = menu
  

接下来就是修改actions里的login方法和添加一个方法用来修改mutations

	//1、修改login方法
	在login 方法的成功回调中添加一个这个方法,用来获取权限菜单
	menu.getMenu()
	//2、添加一个方法用来修改 mutations,这个方法是跟login方法同级的
	 modifyMenu( commit ,menu)
   		 return new Promise(resolve => 
    	  commit('SET_MENU',menu)
    	  resolve()
    	)
 	 

修改login方法 如图:

再在logout那个方法里清除缓存,像下面这段代码这样

logout( commit, state ) 
    return new Promise((resolve, reject) => 
      // 清除缓存的权限菜单
      sessionStorage.removeItem("route")
      //
      removeToken() // must remove  token  first
      // resetRouter()
      location.reload()
      commit('RESET_STATE')
    )
 

现在已经能够拿到菜单信息并能够在侧边栏菜单显示了,控制台还有个报错,只是说一刷新页面就会没有,只能够通过地址栏跳转,下一步我们先来做路由/菜单的持久化。

四、修改 router,持久化处理

找到router目录,并打开index.js。引入获取权限菜单的menu.jsVuex

	import store from '../store'
	import menu from '../menu/menu.js'

constantRoutes 中只留下登录404首页这三个页面的router路径其他的全部不需要。类似于下面图中那样。

createRouter() 后面添加路由监听,以实现菜单的持久化。一定要放在const router = createRouter() 后面。
就像下图那样

监听路由的代码:

	//路由判断
router.beforeEach((to, from, next) => 
	// 判断是否有token
  if (store.state.user.token) 
    // 判断是否是跳转到登录页面
    if (to.path === '/login') next()
    else //判断菜单列表
      if (!store.state.user.menu.length)  // 判断当前用户是否已拉取完权限菜单信息
          // 如果本地不存在权限菜单,则获取权限菜单,生成菜单列表
          if(!sessionStorage.getItem("route"))
        	//获取路由菜单
            menu.getMenu().then(response =>
            	//保险起见,组装一次数据
           		menu.parseRoute(JSON.parse(sessionStorage.getItem("route")), [], 			1).then(res =>
           			//添加路由并进行跳转
             		 menu.addMenu(res).then(e =>
                		next( ...to, replace: true ) // hack方法 确保addRoutes已完成
              		)
           		)
            ).catch(err =>//失败则直接跳转登录页面
              next(path: 'login')
            )
          else//从缓存中读取用户权限列表,并添加菜单到侧边栏和路由元
            menu.parseRoute(JSON.parse(sessionStorage.getItem("route")), [], 1).then(res =>
              menu.addMenu(res).then(e =>
                next( ...to, replace: true ) // hack方法 确保addRoutes已完成
              )
            )
          
      
      else next(path: 'login') //当有权限菜单存在时,说明所有可访问路由已生成 如访问没权限的全面会自动进入登录页面
    
   else next(path: 'login'); // 否则全部重定向到登录页
)

这样我们的菜单/路由持久化就🆗了,怎么刷新都不会丢失了,如果没有这个缓存了,还会自动的去获取并加载上去,然后完成页面的跳转。

五、处理问题/修改源码

打开我们的项目,找到并打开在src > layout > components > Sidebar 的index.vue。打开过后往下滑,在第34行左右的位置,把routes中的return那行改一下,侧边栏的菜单就可以正常显示了。如下代码:

	// 菜单
    routes() 
    	//修改的代码
      let menuRouter = this.$store.state.user.menu,
          routeMenu = [...this.$router.options.routes,...menuRouter]

      return routeMenu
      //源码中的代码
      // return this.$router.options.routes
    ,

经过上面的步骤我们的权限菜单(动态路由)就已经完成了,接下来我们需要去修改一下 element-ui的菜单源码,因为跟我们写的对不上,控制台还有个报错。

打开我们的项目,找到并打开在src > layout > components > Sidebar 的SidebarItem.vue。打开过后往下滑,在第41行左右的位置,把item中的type那行注释掉,就可以解决控制台报错的那个小问题了。如下图:

我们的 权限菜单(动态路由)就已经搞定了。以上就是操作 Vue 权限菜单(动态路由)的一个详细过程,有问题的小伙伴可以私信我,也可以评论留言,看到会回复

Vue权限控制——动态注册路由

需求:实现后台管理系统不同用户的权限控制


根据登录的用户的角色动态展示后台管理系统的左侧菜单栏的菜单列表内容,然后还要动态注册对应子菜单的路由
菜单列表内容应该通过后端接口返回:

  1. sort为1表示当前项有子菜单
  2. sort为2表示当前项没有子菜单,这个才是需要我们去动态注册的组件

    前端需要根据后端返回的菜单列表去动态的展示菜单列表:

并且为每个菜单列表项注册对应的路由:

  1. 在views/main文件夹下创建所有的页面(component)

  2. 在router/main文件夹下创建每个页面对应的路由对象(此时只是先配置好路由path和组件component的映射关系,还没有注册路由,后续会根据后端返回的用户菜单表数据(userMenus)动态的去注册路由

  3. 根据用户的角色role.id向后端发送请求,拿到当前登录用户的userMenus菜单

  1. 根据userMenus生成对应的routes
    1)先拿到项目中所有组件的路由对象route放到allRoutes数组中
    2)再递归遍历userMenus数组的每一项(menu),将满足menu.url === route.path条件的menu放到routes数组中

  2. 遍历routes数组,把数组中的每一个route通过router.addRoute('main', route),动态注册到main路由对象的children属性中

import type  RouteRecordRaw  from 'vue-router'

export function mapMenuToRoutes(userMenus: any[]): RouteRecordRaw[] 
  const routes: RouteRecordRaw[] = []
  // 1.先去加载默认所有的routes
  const allRoutes: RouteRecordRaw[] = []
  const routeFiles = require.context('../router/main', true, /\\.ts/)
  routeFiles.keys().forEach((key) => 
    console.log(key) // ./analysis/dashboard/dashboard.ts
    const route = require('../router/main' + key.split('.')[1])
    allRoutes.push(route.default)
  )
  console.log(allRoutes)
  // 2.根据菜单获取需要添加到routes
  const _recurseGetRoute = (menus: any[]) => 
    for (const menu of menus) 
      if (menu.type === 2) 
        const route = allRoutes.find((route) => 
          return route.path === menu.url
        )
        if (route) routes.push(route)
       else 
        _recurseGetRoute(menu.children)
      
    
  
  _recurseGetRoute(userMenus)
  return routes


<template>
  <div class="nav-menu">
    <div class="logo">
      <img src="~@/assets/img/logo.svg" alt="logo" />
      <span class="title" v-if="!collapse">后台管理系统</span>
    </div>
    <el-menu
      default-active="1"
      :collapse="collapse"
      class="el-menu-vertical"
      background-color="#0c2135"
      text-color="#b7bdc3"
      unique-opened
      active-text-color="#0a60bd"
    >
      <template v-for="item in userMenus" :key="item.id">
        <!-- 有二级菜单的一级菜单 -->
        <template v-if="item.type === 1">
          <!-- 一级菜单 -->
          <el-sub-menu :index="item.id + ''">
            <template #title>
              <el-icon><Setting /></el-icon>
              <!-- <i v-if="item.icon" :class="item.icon"></i> -->
              <span> item.name </span>
            </template>
            <template v-for="subItem in item.children" :key="subItem.id">
              <!-- 二级菜单  -->
              <el-menu-item
                :index="subItem.id + ''"
                @click="handleMenuItemClick(subItem)"
              >
                <i v-if="subItem.icon" :class="subItem.icon"></i>
                <span> subItem.name </span>
              </el-menu-item>
            </template>
          </el-sub-menu>
        </template>
        <!-- 没有二级菜单的一级菜单 -->
        <template v-else-if="item.type === 2">
          <!-- 一级菜单 -->
          <el-menu-item :index="item.id + ''">
            <i v-if="item.icon" :class="item.icon"></i>
            <span> item.name </span>
          </el-menu-item>
        </template>
      </template>
    </el-menu>
  </div>
</template>

<script lang="ts">
import  defineComponent, computed  from 'vue'
import  Setting  from '@element-plus/icons-vue'
import  useStore  from '@/store'
import  useRouter  from 'vue-router'
export default defineComponent(
  name: 'nav-menu',
  components:  Setting ,
  props: 
    collapse: 
      type: Boolean,
      default: false
    
  ,
  setup(props, context) 
    const store = useStore()
    const router = useRouter()
    const userMenus = computed(() => store.state.login.userMenus)
    const handleMenuItemClick = (item: any) => 
      // console.log(item)
      router.push(
        path: item.url ?? '/not-found'
      )
    
    return 
      userMenus,
      handleMenuItemClick
    
  
)
</script>

<style scoped lang="less">
.nav-menu 
  height: 100%;
  background-color: #001529;

.logo 
  display: flex;
  height: 28px;
  padding: 12px 10px 8px 10px;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  img 
    width: 40px;
    height: 40px;
  
  .title 
    font-size: 16px;
    font-weight: 700;
    color: #fff;
  

.el-menu-vertical 
  width: 100%;
  height: calc(100% - 48px);

.el-menu 
  border-right: none;

</style>

以上是关于Vue 权限菜单(动态路由)详解的主要内容,如果未能解决你的问题,请参考以下文章

Vue权限控制——动态注册路由

Vue实现动态路由

vue项目实现动态路由和动态菜单搭建插件式开发框架免费源码

vue项目 动态路由怎么做

vue项目权限管理

vue前后分离动态路由和权限管理方案