记一次vue-element-admin 的动态路由权限管理和菜单渲染的学习

23,439 阅读4分钟

三人行必有我师,今天来记录一次 对 vue-element-admin 中 动态路由和动态菜单渲染的理解,只是学习,不对之处,还请见谅

现实的工作中,尤其是写后台管理系统的时候,经常会涉及到不同的用户角色拥有不同的管理权限,那么久需要对应不同的管理者进行不同的动态路由设置和导航菜单渲染,这在后台管理中,变成了一个迫切需要解决的问题,今天久记录一次对vue-element-admin中这一块学习的的理解

以下代码都是抄的,不喜勿喷.

不会写文章:那就让代码来说明问题吧! 不喜欢看代码的见谅哈,发个地址: github.com/cgq001/admi…
大家相互学习哈,我也是模仿的 哈哈, 不需要星星,纯属娱乐 哈哈 无聊罢了

一.路由设计

1.route/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

import Layout from '@/layout' //布局页

import Home from '../views/Home.vue'

Vue.use(VueRouter)

// 通用页面, 这里的配置不需要权限
export const constRouter = [
  {
      path: '/login',
      component: () => import('@/views/login/Login'),
      hidden: true //导航菜单忽略选项
  },
  {
      path: '/',
      component: Layout, //应用布局页
      redirect: '/home',
      meta:{
        title: '布局',
        icon: 'wx'
      },
      children: [
          {
              path: 'home',
              component: () => {
                  import('@/views/Home.vue')
              },
              name: "Home",
              meta:{
                  title: "Home", //导航菜单项标题
                  icon: 'qq' //导航菜单图标
              }
          }
      ]
  }
]

// 动态路由
export const asyncRoutes = [
  {
    path: '/about',
    component: Layout,
    redirect: '/about/index',
    meta:{
      title: "关于",
      icon: 'wx'
    },
    children: [
      {
        path: 'index',
        component: () => import('@/views/about/About.vue'),
        name: 'about',
        meta: {
          title: "About",
          icon: 'qq',
          roles: ['admin','editor']  //角色权限配置
        }
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: constRouter
})

export default router

2.全局导航守卫

需要在main.js中引入全局守卫

1.main.js
// 全局路由守卫
import './router/permission'
2.router/permission
// 路由的全局首位

// 权限控制逻辑
import router from './index'
import store from '../store/index'

import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 从cookie获取令牌

const whiteList = ['/login'] //排除的路径

router.beforeEach(async (to,from,next) => {

    // 获取令牌判断用户是否登陆
    const hasToken = getToken()
    // 有令牌 表示已经登陆
    if(hasToken){
        if(to.path === '/login'){
            // 已登录重定向到首页
            next({path: '/'})
        }else{
            //若用户角色已附加则说明动态路由已经添加
            const hasRoles = store.getters.roles && store.getters.roles.length > 0

            if(hasRoles){
                //角色存在
                next() //继续即可
            } else {
                try {
                    //先请求获取用户角色
                    const { roles } = await store.dispatch('user/getInfo')
                
                    // 根据当前用户角色动态生成路由
                    const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
                   
                    // 添加这些路由至路由器
                    router.addRoutes(accessRoutes)

                    // 继续路由切换,确保addRoutes完成
                    next({...to,replace: true})
                } catch(error){
                    // 出错需要重置令牌并重新登陆(令牌过期,网络错误等原因)
                    await store.dispatch('user/resetToken')
                    Message.error(error || "网络错误")
                    next(`/login?redirect=${to.path}`)
                }
            }
        }
    }else{
        // 用户无令牌
        if(whiteList.indexOf(to.path) !== -1){
            //白名单路由放过
            next()
        } else {
            // 重定向至登录页
            next(`/login?redirect=${to.path}`)
        }
    }
})

3.1store/index

import Vue from 'vue'
import Vuex from 'vuex'
import permission from './modules/permission'
import user from './modules/user'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    permission,
    user
  },
  // 定义全局getters 方便访问user 模块的roles
  getters:{
    roles: state => state.user.roles,
    permission_routes: state => state.permission.routes
  }
})

3.1store/modules/user

import { getToken, setToken, removeToken } from '@/utils/auth'

// 村赤用户令牌和角色信息
const state ={
    token: getToken(),
    roles: [] //角色
}

const mutations ={
    SET_TOKEN: (state,token) => {
        state.token = token;
    },
    SET_ROLES: (state,roles) => {
        state.roles = roles;
    }
};

const actions = {
    // 用户登录
    login({ commit }, userInfo) {
        const { username } = userInfo;
        return new Promise((resolve,reject) => {
            setTimeout(() => {
                if(username === 'admin' || username === 'jerry'){
                    // 保存状态
                    commit('SET_TOKEN',username);
                    // 写入cookie
                    setToken(username)
                    resolve()
                }else{
                    reject('用户名或密码错误')
                }
            },1000)
        })
    },
    // 获取用户角色信息
    getInfo({ commit, state }){
        return new Promise((resolve) => {
            setTimeout(() => {
                const roles = state.token === 'admin' ? ['admin'] : ['editor']
                commit('SET_ROLES',roles)
                resolve({roles})
            },1000)
        })
    },
    // 重置令牌
    resetToken({ commit }){
        return new Promise(resolve => {
            commit('SET_TOKEN','')
            commit('SET_ROLES',[])
            removeToken();
            resolve()
        })
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}

3.1store/modules/permission

// 权限管理模块
import { asyncRoutes, constRouter } from '@/router'

/**
 * 根据路由meta.role 确定是否当前用户拥有访问权限
 * @roles 用户拥有角色
 * @route 待判定路由
 * 
 * 
 * 
*/
function hasPermission (roles,route){
    // 如果当前路由有roles 字段则需要判断用户访问权限
    if(route.meta && route.meta.roles){
        // 若用户拥有的角色中有被包含在待定路由角色表中的则拥有访问权限
        return roles.some(role => route.meta.roles.includes(role))
    } else{
        // 没有设置roles 则无需判定即可访问
        return true
    }
}

/**
 * 递归过滤AsyncRoutes路由表
 * @routes 待过滤路由表,首次传入的就是AsyncRoutes
 * @roles 用户拥有角色
 * 
*/
export function filterAsyncRoutes(routes,roles){
    const res = []
    routes.forEach(route => {
        // 复制一份
        const tmp = { ...route}
        // 如果用户有访问权限则加入结果路由表
        if(hasPermission(roles,tmp)){
            // 如果存在子路由则递归过滤之
            if(tmp.children){
                tmp.children = filterAsyncRoutes(tmp.children,roles)
            }
            res.push(tmp)
        }
    })
    return res;
}

const state = {
    routes: [], //完整路由表
    addRoutes: []  //用户可访问路由表
}

const mutations = {
    SET_ROUTES: (state, routes) => {
      
        // routes 用户可以访问的权限
        state.addRoutes = routes
        // 完整的路由表
        
        state.routes = constRouter.concat(routes)
       
    }
}

const actions = {
    generateRoutes({ commit }, roles) {
        return new Promise(resolve => {
            
            let accessedRoutes;
            // 用户是管理员则拥有完整访问权限
            if(roles.includes('admin')){
                accessedRoutes = asyncRoutes || []
            }else{
                //  否则需要根据用户角色做过滤处理
                accessedRoutes = filterAsyncRoutes(asyncRoutes,roles)
            }
           
            commit('SET_ROUTES',accessedRoutes)
       
            resolve(accessedRoutes)
        })
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}

4.登陆

<template>
    <div>
        <input type="text" v-model="username" />
        <div @click="login">登陆</div>
    </div>
   
</template>

<script>
export default {
    data(){
        return {
            username:undefined
        }
    },
    methods:{
        login(){
            this.$store
                .dispatch('user/login',{username: this.username})
                .then(()=>{
                    // 登陆成功后重定向
                    this.$router.push({
                        path: this.$route.query.redirect || '/'
                    })
                })
                .catch(err=>{
                    console.log(err)
                })
        }
    }
}
</script>

二.菜单渲染

1.Sidebar/index

<template>
    <div>
  
            <el-menu
                :default-active="activeMenu"
                :background-color="variables.menuBg"
                :text-color="variables.menuText"
                :unique-opened="false"
                :active-text-color="variables.menuActiveText"
                :collapse-transition="false"
                mode="vertical"
                >
                    <SidebarItem
                        v-for="route in permission_routes"
                        :key="route.path"
                        :item='route'
                        :base-path='route.path'
                        >
                    
                    </SidebarItem>
            </el-menu>

    </div>
</template>

<script>
import { mapGetters } from 'vuex';
import SidebarItem from './SidebarItem'
console.log('一层')
export default {

    components:{ SidebarItem },
    computed:{
        ...mapGetters(['permission_routes']),
        activeMenu(){
            const route = this.$route;
            const { meta, path } = route
            // 默认激活项
            if(meta.activeMenu){
                return meta.activeMenu
            }
            return path;
        },
        variables(){
            return {
                menuText: "#bfcbd9",
                menuActiveText: "#409EFF",
                menuBg: "#304156"
            }
        }
    },
    mounted(){
 
        console.log(this.permission_routes,12)
    }
}
</script>

1.Sidebar/SidebarItem

<template>
    <div v-if="!item.hidden" class="menu-wrapper">
        <!-- 仅有一个可显示的子路由,并且没有孙子路由 -->
        <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && item.alwaysShow">
            <router-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
                <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown' : isNest }">
                   
                    <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
                </el-menu-item>
            </router-link>
        </template>
        <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
            <template v-slot:title>
                <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
            </template>
            <sidebar-item
                v-for="child in item.children"
                :key="child.path"
                :is-nest='true'
                :item='child'
                :base-path="resolvePath(child.path)"
                class="nest-menu"
                />
        </el-submenu>
    </div>
</template>

<script>
import path from 'path'
import Item from './Item'

export default {
    name: "SidebarItem",
    components: { Item },
    props: {
        item: {
            type: Object,
            required: true
        },
        isNest: {
            type: Boolean,
            required: false
        },
        basePath: {
            type: String,
            default: ''
        }
    },
    data(){
        this.onlyOneChild = null
        return {
            
        }
    },
    mounted(){
        console.log(this.item)
    },
    methods:{
        hasOneShowingChild(children = [],parent){
           
            const showingChildren = children.filter(item =>{
                if(item.hidden){
                    return false
                } else {
                    // 如果只有一个子菜单时设置
                    this.onlyOneChild = item
                    return true
                }
            })
            // 当只有一个子路由,子路由默认展示
            if(showingChildren.length === 1){
                return true
            }
            // 没有子路由则显示父路由
            if(showingChildren.length === 0){
                this.onlyOneChild = {...parent, path: '', noShowingChildren: true }
                return true
            }
            console.log( this.onlyOneChild)
            return false
        },
        resolvePath(routePath){
            return path.resolve(this.basePath, routePath)
        }
    }    
}
</script>

1.Sidebar/index

<script>
export default {
    name: "MenuItem",
    functional: true,
    props:{
        icon: {
            type: String,
            default: ''
        },
        title: {
            type: String,
            default: ''
        }
    },
    render(h, context){
        const { icon, title } = context.props
        const vnodes = []
      
        if(icon){
            // vnodes.push(<svg-icon icon-class={icon} />)
            vnodes.push(<i class={icon}></i>)
        }
        if(title){
  
            vnodes.push(<span slot='title'>{title}</span>)
        }
          
        return vnodes
    }
}
</script>