Vue3 根据用户权限设置动态菜单

本文采用的技术栈有:

前端:Vue3 + VueRouter@4 + VueX + Element-ui

后端:Express

在中后台管理系统中,我们知道可以有多种用户实体。以学生管理系统为例,老师教务主任就是两个拥有不同职责的实体对象。

当不同权限的用户登录管理系统时,他们所需要的功能也就不同。比如老师管理学生信息,而教务主任不仅可以管理学生,也可以处理一些老师的信息。由于职责不同,(通常来说在左侧)的用户功能菜单也就不一样。

需求:不同的用户在登录后可以看到不同的菜单。

一、通过路由守卫附加请求

在实现功能需求前首先需要明确一点,在登录页面输入账号密码进行登录后,在没有direct的情况下一般是跳转到首页。在这个过程中,仔细想想,常见的处理大致是:

  1. 通过表单确认,向后台登录接口发起一个post请求
  2. 根据后台验证结果,进行消息提示判断是否通过路由跳转
  3. 将后台返回的token数据,以及用户相关数据保存起来

1.1 获取用户信息

需要注意的是,登录操作是在login页面完成的,而用户信息数据是在首页展现,这里采用路由守卫来实现这一功能。

import {
    
     ElLoading } from 'element-plus'
import router from '@/router'
import store from '@/store'
// 通过这个TOKEN_KEY拿到具体的token数据
import TOKEN_KEY from '@/store/modules/app' 
import Test from './modules/test'
import User from './modules/user'
import Article from './modules/article'

// Test页面中存在:“由于用户权限不一致,最终渲染的菜单也许不一致”的需求
export const asyncRoutes = [...Test]

// 表示不管是什么用户,登陆后都能看到该菜单
export const fixedRoutes = [...User, ...Article]

const router = createRouter({
    
    
  history: createWebHashHistory(),
  routes: [{
    
    
      path: '/',
      redirect: '/home',
    },
    // 这里并未添加 asyncRoutes 数组中的路由配置
    // 具体原因将在2.1中介绍
    // 现在仅需要知道的是:页面中可以直接访问挂载在 fixedRoutes 中的路由对象,无法访问asyncRoutes中的路由
    ...fixedRoutes
  ],
  scrollBehavior(to, from, savedPosition) {
    
    
    if (savedPosition) {
    
    
      return savedPosition
    } else {
    
    
      return {
    
    
        left: 0,
        top: 0
      }
    }
  },
})

// vue-router4的路由守卫不再是通过next放行
// 而是通过return返回true或false或者一个路由地址
router.beforeEach(async to => {
    
      
    // 如果路由配置中存在meta属性,则配置页面title  
    document.title = (!!to.meta && to.meta.title)  
    // 如果没有token信息 
    // 比如用户在未登录状态下通过URI链接访问固定页面 
    // 此时拦截跳转至登录页面,同时声明direct信息,让用户在登录后可以直接跳转到上次访问的页面  			
    if(window.localStorage[TOKEN_KEY]){
    
        
        return {
    
          
            name: 'login',     
            query: {
    
           
                direct: to.fullPath      
            }  
        }  
    }  
    else{
    
        
        // 由于用户权限数据保存在用户信息中   
        let {
    
     userInfo } = store.state.account      
        // 第一次登录没有用户信息,需要发起相关请求    
        if(!userInfo){
    
          
            const loadingInstance = ElLoading.service({
    
           
                lock: true,                
                text: '加载数据中...\_(ツ)_/',               
                background: 'rgba(0, 0, 0, 0.7)',     
            })     
            // 异步获取用户信息     
            try{
    
            
                // 在这里请求userInfo相关的信息
			   // 成功将用户数据保存在vuex中
                userInfo = await store.dispatch('account/getUserInfo')       
                loadingInstance.close()            
            } catch (err) {
    
                 
                loadingInstance.close()    
                return false        
            }   
        } 
    }
})

export default router

在目标页面中,直接将userInfo数据读取出来即可使用:

const userinfo = computed(() => state.account.userinfo)

1.2 获取菜单信息

在路由守卫中我们已经成功地实现了:在页面跳转的过程中获取到用户信息并保存。

接下来思考如何获取菜单信息,由于不同用户拥有的菜单不同,我们可以将标识用户权限的字段添加在userInfo中,比如auth:'admin | visitor'。由于菜单的动态性,我们需要在请求菜单数据的时候将用户信息一并传入接口中:

// 生成菜单
// 发起菜单的请求
if (store.state.menu.menus.length <= 0) {
    
          
    const loadingInstance = ElLoading.service({
    
           
        lock: true,       
        text: '正在加载数据,请稍候~',   
        background: 'rgba(0, 0, 0, 0.7)',    
    })     
    try {
    
       
        // 关于用户信息的部分放在user模块中
	    // 菜单部分则放在menu模块中 
        await store.dispatch('menu/generateMenus', userinfo)       
        loadingInstance.close()        
        return to.fullPath // 添加动态路由后,必须加这一句触发重定向,否则会404 
    } catch (err) {
    
         
        loadingInstance.close()   
        return false  
    }   
}

二、匹配动态菜单

根据前面的分析,我们需要在vuex中的menu模块下进行菜单内容的请求。

这样做的好处就在于,具体菜单信息的逻辑处理部分交由后端来实现,如果说某个用户权限需要更改,直接更改后台字段即可,前台只需要传入用户类型,就能收到该用户对应的权限菜单,进而渲染在页面中。

// store/modules/menu
export default {
    
    
  namespaced: true,
  state: {
    
    
    menus: [],
  },
  mutations: {
    
    
    SET_MENUS(state, data) {
    
    
      state.menus = data
    },
  },
  actions: {
    
    
    async generateMenus({
     
      commit }, userinfo) {
    
    
      // 假设所有用户看到的都是同样的菜单,即:只有固定菜单
      // const menus = getFilterMenus(fixedRoutes)
      // commit('SET_MENUS', menus)

      // 假设存在不同用户权限,需要设置动态菜单
      // 从后台获取菜单
      // 用户权限信息保存在userinfo中
      const {
    
     code, data } = await axios.get('/api/menus', {
    
     role: userinfo.role })
      
      // 获得返回数据
      if (+code === 200) {
    
    
        // 过滤出需要添加的动态路由
  	    // ...
      }
    },
  },
}

2.1 过滤菜单数据

现在根据用户权限获取到了对应的菜单功能,假设后台返回的data数据形式如下:

[
    name: 'test',
    title: '测试页面',
    children: [
        {
    
    
            name: 'stuList',
            title: '学生列表'
        },
        {
    
    
            name: 'stuAdd',
            title: '添加'
        },
        {
    
    
            name: 'stuEdit',
            title: '修改'
        }
	]
]

接下来需要做的就是将与返回数据相对应的路由挂载在页面中。

现在有一个问题是,我们在配置路由文件的时候,代码的格式为:

[
    {
    
    
        path: '/test',
        component: Layout,
        name: 'test',
        meta: {
    
    
            title: '测试页面',
        },
        icon: 'el-icon-location',
        children: [
            {
    
    
                path: '',
                name: 'stuList',
                component: List,
                meta: {
    
    
                    title: '学生列表',
                },
            },
            {
    
    
                path: 'stuAdd',
                name: 'stuAdd',
                component: Add,
                meta: {
    
    
                    title: '添加',
                },
                hidden: true, // 不在菜单中显示
            }
        ],
    },
]

在路由配置文件里面的对象数组记录的,是所有可能需要展示的菜单;而后台返回的数据则是用户需要使用到的菜单。

那么我们是不是可以根据后台返回的数据,和路由配置文件作比较,将符合条件的数据作为最终路由数据渲染到页面上呢?

回到store/modules/menu模块中:

if (+code === 200) {
    
    
    // 过滤出需要添加的动态路由
    // 创建 getFilterRoutes 方法过滤路由数据
    // 这里的 asuncRoutes 是从路由配置文件中导出的对象数组,记录着路由配置信息;
    // data则是后台返回的权限数据
    const filterRoutes = getFilterRoutes(asyncRoutes, data)
    
    // 动态添加路由
    // 回到1.1,这里解答当时留下的问题
    // 动态路由需要通过和后台数据匹配后才能挂载到router对象身上
    // 因此只能在后期通过router.addRoute方法添加
    filterRoutes.forEach(route => router.addRoute(route))

    // 生成菜单
    const menus = getFilterMenus([...fixedRoutes, ...filterRoutes])
    // 将数据保存至state中,方便菜单组件部分通过循环渲染,将数据渲染到页面上
    commit('SET_MENUS', menus)
}

2.2 实现过滤操作

基本的思路已经介绍完毕,现在来编写一下getFilterRoutesgetFilterMenus方法,实现核心过滤操作。

回顾以下后台数据结构:

在这里插入图片描述

  1. 循环遍历后台返回数据(因为具体展示哪些菜单由后台数据决定)
  2. 判断能否在路由配置的数组中,找到和后台数据匹配的路由
  3. 如果可以找到,由于二级路由的存在,我们需要对children子路由进行判断
  4. 递归遍历二级路由
const getFilterRoutes = (targetRoutes, ajaxRoutes) => {
    
    
    const filterRoutes = []
    ajaxRoutes.forEach(item => {
    
    
        const target = targetRoutes.find(target => target.name === item.name)
        if (target) {
    
    
            // 将children子路由信息单独取出来
		   // 剩下的部分全部属于一级路由,通过...rest收集起来
            const {
    
     children: targetChildren, ...rest } = target
            // 将...rest中的数据直接添加到路由对象中
            const route = {
    
    
                ...rest,
            }
            if (item.children) {
    
    
                // 递归遍历
                route.children = getFilterRoutes(targetChildren, item.children)
            }
            filterRoutes.push(route)
        }
    })
    return filterRoutes
}

现在我们成功实现了路由的过滤,现在来过滤菜单。

具体的操作和过滤路由时差不多:

  1. 遍历已经处理完毕的路由规则
  2. 如果我们没有设置某项菜单不可见,则创建具体的菜单item对象信息
  3. 如果存在二级菜单,我们在设置路由时需要注意为子菜单添加上一级路由路径(在配置路由时不需要为二级路由添加一级路由路径)
const getFilterMenus = (arr, parentPath = '') => {
    
    
    const menus = []
    arr.forEach(item => {
    
    
        if (!item.hidden) {
    
    
            // 某一级的菜单可能有多层子菜单数据
            const menu = {
    
    
                url: generateUrl(item.path, parentPath),
                title: item.meta.title,
                icon: item.icon,
            }
            if (item.children) {
    
    
                if (item.children.filter(child => !child.hidden).length <= 1) {
    
    
                    menu.url = generateUrl(item.children[0].path, menu.url)
                } else {
    
    
                    menu.children = getFilterMenus(item.children, menu.url)
                }
            }
            menus.push(menu)
        }
    })

    return menus
}

const generateUrl = (path, parentPath) => {
    
    
  return path.startsWith('/')
    ? path
    : path
    ? `${
      
      parentPath}/${
      
      path}`
    : parentPath
}

至此已成功实现vue3权限菜单(动态路由)的设置。

猜你喜欢

转载自blog.csdn.net/flow_camphor/article/details/125589248
今日推荐