最近整理了一下关于vue后台管理项目动态路由权限管理及菜单的渲染:
环境:vue3.0+element-plus+vue-router4.0
思路:
- router/index.js文件:
配置路由,路由分为两部分:公共路由+动态路由(注意:配置不存在路由跳转到默认页面的代码,需要写在动态路由里,否则会造成刷新页面,跳转到默认页面)
- router/permission.js文件
全局导航守卫,设置路由拦截(在main.js引入)
- store/modules/user.js 获取用户信息(角色)
store/modules/permission.js 根据角色获取动态路由 - 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>