vue-element-admin 的动态路由权限管理和菜单渲染

最近整理了一下关于vue后台管理项目动态路由权限管理及菜单的渲染:

环境:vue3.0+element-plus+vue-router4.0
思路:

  1. router/index.js文件:

配置路由,路由分为两部分:公共路由+动态路由(注意:配置不存在路由跳转到默认页面的代码,需要写在动态路由里,否则会造成刷新页面,跳转到默认页面

  1. router/permission.js文件

全局导航守卫,设置路由拦截(在main.js引入)

  1. store/modules/user.js 获取用户信息(角色)
    store/modules/permission.js 根据角色获取动态路由
  2. menu菜单组件中,通过store获取总路由并进行渲染

具体代码流程

1.router/index.js

{ path: ‘/:pathMatch(.*)’, redirect: ‘/dashboard’, hidden: true } 注意这个代码写的位置,是为了解决刷新后页面跳转到默认页面的

import { createRouter, createWebHashHistory } from 'vue-router'

// 通用路由,不需要配置权限
export const constRouter = [
  {
    path: '/login',
    name: 'login',
    component: () =>import('../views/login.vue'),
  },
  {
    path: '/',
    name: 'dashboard',
    redirect: '/dashboard',
    component: () =>import('../layout/index.vue'),
    children: [
        {
          path: '/dashboard',
          meta: { title: '首页',icon: 'el-icon-s-tools', },
          component: () => import('../views/dashboard/index.vue')
        },
    ]
  },
]

// 动态路由
export const asyncRoutes = [
  {
    path: '/book',
    name: 'layout',
    redirect: '/home',
    component: () =>import('../layout/index.vue'),
    meta: {
      title: '图书管理',
      icon: 'el-icon-user',
      roles: ['admin','editor']  //角色权限配置
    },
    children: [
        {
          path: 'list',
          name: 'book-list',
          meta: { 
            title: '图书列表',
            icon: 'el-icon-s-tools',
            roles: ['admin','editor']  //角色权限配置
          },
          component: () => import('@/views/books/book_list.vue')
        },
        {
            path: '/home',
            name: 'home',
            meta: { 
                title: 'Home',
                icon: 'el-icon-s-tools', 
                roles: ['editor']
             },
            component: () =>import('../views/Home.vue')
        },
    ]
  },
  { path: '/:pathMatch(.*)', redirect: '/dashboard', hidden: true } //这句代码要写在动态路由里面
]

const router = createRouter({
  history: createWebHashHistory(process.env.BASE_URL),
  routes: constRouter
})

export default router

2.router/permission.js

在main.js中引入
全局路由守卫,路由拦截;
这里要注意一下:vue-route4.0 删除了 router.addRoutes(数组); 方法,只能用 router.addRoute(对象); 这个方法

import router from './index';
import store from '../store';

import { getToken } from '../utils/auth';
// import { message } from 'element-plus';

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

router.beforeEach(async (to, from, next) => {
    // 判断是否有登录态
    const hasToken = getToken();
    // 如果已经登录
    if(hasToken){
        if(to.path === '/login'){
            next({path: '/'});
        }else{
            // 获取角色
            const hasRoles = store.state.user.roles && store.state.user.roles.length > 0;
            if(hasRoles){
                next();
            }else{
                try{
                    //请求获取角色
                    const { roles } = await store.dispatch('user/getUserInfo');
                    console.log(roles);
                    // 根据角色生成动态路由
                    const accessRoutes = await store.dispatch('permission/generateRoutes', roles);
                    // 
                    accessRoutes.forEach(ele=>{
                        router.addRoute(ele);
                    })
                    next({ ...to, replace: true });
                }catch{
                    // 出错需要重置令牌并重新登陆(令牌过期,网络错误等原因)
                    await store.dispatch('user/resetToken');
                    Message.error(error || 'Has Error');
                    next(`/login?redirect=${to.path}`);
                }
            }
        }
    }else{
        // 没有token
        if(whiteList.indexOf(to.path) !== -1){
            next();
        }else{
            next(`/login?redirect=${to.path}`);
        }
    }
    
})

3.store下的文件
在这里插入图片描述

3-1. store/index.js


import { createStore } from 'vuex'
import menus from './modules/menus';
import user from './modules/user';
import permission from './modules/permission';
import getters from './getters';

export default createStore({
  modules:{
    menus,
    user,
    permission,
  },
  getters
})

3-2. store/getters.js


const getters = {
    roles: state => state.user.roles,
    accessRoutes: state => state.permission.accessRoutes,
    routes: state => state.permission.routes,
  }
export default getters

3-3. store/modules/user.js


import { login, getUser } from '../../scripts/api';
import { setToken, getToken, removeToken } from '../../utils/auth';
const state = {
    userInfo: getToken(),
    token:'',
    roles:[],//角色
}

// 修改store
const mutations = {
    SET_TOKEN(state,token) {           
        state.token = token;      
    },
    SET_ROLES(state,roles) {           
        state.roles = roles;      
    },
    SET_USER(state,userInfo) {           
        state.userInfo = userInfo;      
    },
}
// 提交mutation
const actions = {
    // 用户登录
    login({commit}, data){
        return new Promise((resolve, reject)=>{
            login().then(res=>{
                console.log(res);
                commit('SET_TOKEN', data);
                setToken(data)
                resolve();
            }).catch(err=>{
                reject(err);
            })
        })   
    },
    // 获取角色
    getUserInfo({commit}){
        return new Promise((resolve, reject)=>{
            getUser().then(res=>{
                console.log(res);
                const userInfo = {
                    name:'leiqiuqiu',
                    roles:["admin"]
                }
                commit('SET_ROLES', userInfo.roles);
                commit('SET_USER', userInfo);
                resolve(userInfo);

            }).catch(err=>{
                reject(err);
            })
        }) 
    },
    // 删除 token
    resetToken({ commit }) {
      return new Promise(resolve => {
        commit('SET_TOKEN', '')
        commit('SET_ROLES', [])
        removeToken()
        resolve()
      })
    },
}

// 命名空间:namespaced: true:当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名,
// 也就是说,我们在调用这些方法时,需要加上这个文件的路径(比如我要访问这个文件中的state里边的某个属性:this.$store.state.modulesA。
// 后边这个modulesA就是多了个modulesA.js模块名,因为如果不加模块名,那我们会访问所有模块中的属性,那就乱了),
// 所以我们要加上命名空间,相当于独立的区块去使用,模块和模块之间互不干扰。
export default{
    namespaced: true,
    state,
    mutations,
    actions
}

3-4. store/modules/permission.js

import {constRouter, asyncRoutes} from '@/router';
const state = {
    accessRoutes: [],
    routes:[],
}

// 修改store
const mutations = {
    SET_ROUTES(state, accessRoutes) {           
        state.accessRoutes = accessRoutes;    
        state.routes = constRouter.concat(accessRoutes);    
    },
}
// 提交mutation
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 const hasPermission = (roles, route)=>{
    if(route.meta && route.meta.roles){
        return roles.some( ele => route.meta.roles.includes(ele));
    }else{
        return true;
    }
}
export const filterAsyncRoutes = (routes, roles)=>{
    const res = [];
    routes.forEach(route=>{
        let temp = {...route};
        if(hasPermission(roles, temp)){
            if(temp.children){
                temp.children = filterAsyncRoutes(temp.children, roles)
            }
            res.push(temp)
        }
    })
    return res;
}

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

4.store里需要引入的文件

这里是store里引用的文件:
1.auth.js: token的缓存处理
2 接口请求文件,这里不做展示了

4-1:utils/auth.js

import Cookies from 'js-cookie';

const TokenKey = 'admin-token';

export const setToken = (token)=>{
    return Cookies.set(TokenKey, token);
}

export const getToken = ()=>{
    return Cookies.get(TokenKey);
}

export const removeToken = ()=>{
    return Cookies.remove(TokenKey);
}

5.登录页面


<template>
    <div>
        <el-button @click="login">login</el-button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            redirect:null,
        }
    },
    watch:{
        $route:{
            handler: function(route){
                console.log(route)
                const query = route.query;
                this.redirect = query && query.redirect;
            },
            immediate:true
        }
    },
    methods: {
        login(){
            console.log(this.$store)
            this.$store.dispatch('user/login', 'leiqiuqiu123').then(res=>{
                alert('登录成功')
                this.$router.push(this.redirect)
            })
        }
    },
}
</script>


6.路由菜单渲染

这里不展示全部代码,只展示获取菜单数据的地方

6-1: sidebar/index.vue

<template>
     <div class="menus" :class="isCollapse ? 'on':''">
        <el-menu
            :uniqueOpened="true"
            :collapse="isCollapse"
            :default-active="$route.path"
            class="el-menu-vertical-demo"
            background-color="#304156"
            text-color="#fff"
            :collapse-transition="false"
            active-text-color="#ffd04b">
            <sidebar-item v-for="route in menus" :key="route.path" :item="route" :base-path="route.path" ></sidebar-item>
        </el-menu>
     </div>
</template>
<script>
import SidebarItem from './SidebarItem';
import { mapState } from 'vuex' ;
export default {
    components: { SidebarItem },
    data() {
        return {
            menus:[],
        }
    },
    computed:{
        ...mapState({
            opend: state => state.menus.opend,
        }),
        isCollapse(){
            return !this.opend;
        }
    },
    methods: {

    },
    created() {
        // 获取菜单路由
        this.menus = this.$store.getters.routes;
    },
}
</script>
<style>
    .menus{    
        position: fixed;
        left: 0;
        top: 0;
        bottom: 0;
    }
    .menus.on{
        width:auto;
    }
    .el-menu{
        height: 100%;
    }
</style>


6-2:sidebar/SidebarItem.vue

<template>
  <div v-if="!item.hidden" class="item">
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
            <i :class="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)"></i>
            <span>{{onlyOneChild.meta.title}}</span>
        </el-menu-item>
      </app-link>
    </template>

    <!-- <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> -->
    <el-submenu v-else :index="resolvePath(item.path)" popper-append-to-body>
      <template #title>
        <div v-if="item.meta">
            <i :class="item.meta && item.meta.icon"></i>
            <span>{{item.meta.title}}</span>
        </div>
      </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 { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
// import FixiOSBug from './FixiOSBug'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
//   mixins: [FixiOSBug],
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
    // TODO: refactor with render function
    this.onlyOneChild = null
    return {
      
    }
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      console.log(555555)
      console.log(children)
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // 如果只有一个子菜单时设置
          this.onlyOneChild = item
          return true
        }
      })
        console.log(showingChildren)
      // 当只有一个子路由,子路由默认展示
      if (showingChildren.length === 1) {
        return true
      }

      // 没有子路由则显示父路由
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>
<style>
/* 因为element的菜单组件不太支持里面穿插标签,但是现在有个标签<div class="item">,导致样式受到影响 */
/*隐藏文字*/
  .el-menu--collapse  .el-submenu__title span,
  .el-menu--collapse  .el-menu-item.submenu-title-noDropdown span,
  .el-menu--collapse  .el-submenu__title .el-submenu__icon-arrow{
    display: none;
  }
   .el-menu {
    height: 100%;
  }
  .el-menu:not(.el-menu--collapse) {
      width: 260px;
    }
</style>


  • 2
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值