常见的后台系统中,会存在不同的用户角色,如超级管理员、普通用户,这些用户角色的权限是不同的,所以可以操作或显示的模块也不一致。这里采用如下后台管理系统框架作为模板
地址:https://github.com/PanJiaChen/vue-admin-template
简单说下需求,后台菜单如下图,要求系统中存在两类用户,管理员和普通员工,管理员登录系统可见所有菜单选项,普通用户仅可见首页菜单项。
项目左侧菜单Sidebar组件代码如下,用v-for遍历当前路由实例路由配置this.$router.options.routes传递给<sidebar-item/>,进行展示,这里不做详细介绍,
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes;
},
}
}
由此可知要实现菜单栏的动态展示,在路由配置文件中,就不能将项目中所有的路由配置对象写在一起,这里将项目中的路由配置拆分成三类,如下所示。分别是constantRoutes常量路由,存放所有用户都可见的路由包括首页,登录页,404页;asyncRoutes异步路由,存放不同的用户需要过滤出筛选的路由;anyRoutes任意路由,当路径出现错误的时候重定向到404页面。这样就可以灵活处理路由配置信息了。
router/index.js
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
]
export const asyncRoutes = [
{
name: 'Acl',
path: '/acl',
component: Layout,
redirect: '/acl/user/list',
meta: {
title: '权限管理',
icon: 'el-icon-lock'
},
children: [
{
name: 'User',
path: 'user/list',
component: () => import('@/views/acl/user/list'),
meta: {
title: '用户管理',
},
},
......
]
},
{
path:'/product',
component:Layout,
name:'Product',
meta: { title: '商品管理', icon: 'el-icon-goods' },
children:[
{
path:'tradeMark',
name:'TradeMark',
component:() => import('@/views/product/tradeMark'),
meta:{title:'品牌管理'}
},
......
]
},
];
export const anyRoutes = [
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
];
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
在用户登录系统后,浏览器会根据用户的token信息获取用户的详细信息,里面除了用户的基本信息外,还包括该用户可以访问的菜单列表(Array),以及用户能够操作的按钮列表。
请求行为在vuex的actions中进行,在登录页面派发该actons,在aciions中将响应的用户信息提交mutations。
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
routes:[], // 服务器返回的菜单标记信息
buttons:[], // 服务器返回的按钮标记信息
role:[], // 服务器返回的角色信息
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
USER_INFO: (state,info) => {
state.name = info.name;
state.avator = info.avator;
state.routes = info.routes; // 菜单权限标记
state.buttons = info.buttons; // 按钮权限标记
state.role = info.role; // 用户角色
},
const actions = {
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
//
commit('USER_INFO',data);
resolve(data)
}).catch(error => {
reject(error)
})
})
},
}
拿到了服务器返回的可显示的异步路由标记信息后,接下去要做的就是将其和项目中所有的异步路由信息进行对比,得到最终需要展示的异步路由信息,这里在登录后再提交一个mutations,命名为SEY_RESULTASYNCROUTES,传给mutations一个方法computedAsyncRoutes,在这个方法中做路由的信息对比,将当前用户要显示的异步路由作为返回值返回。
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
console.log(response.data);
if (!data) {
return reject('Verification failed, please Login again.')
}
//
commit('USER_INFO',data);
commit('SEY_RESULTASYNCROUTES',computedAsyncRoutes(asyncRoutes,data.routes));
resolve(data)
}).catch(error => {
reject(error)
})
})
},
定义computedAsyncRoutes方法如下,将异步路由asyncRoutes引入,遍历asyncRoutes数组,将每一个路由的name和服务器传来的标记信息routes挨个对比(indexOf方法),过滤出服务器标记数组中包含的路由信息;
这里值得注意一点,由于路由存在二级甚至三级路由,这些路由是写在路由配置对象的children属性中,它们一样也要遍历并对比判断它们是否需要显示,需要递归调用computedAsyncRoutes方法,对每个以及路由的children再次进行过滤。
import { resetRouter,asyncRoutes,anyRoutes,constantRoutes } from '@/router'
// 两个数组进行对比,计算出当前用户显示哪些异步路由
const computedAsyncRoutes = (asyncRoutes,routes) => {
return asyncRoutes.filter((item) => {
// 服务器返回的数组中包含的路由信息
if(routes.indexOf(item.name) !== -1) {
// 如果存在多级路由,递归
if(item.children && item.children.length) {
item.children = computedAsyncRoutes(item.children,routes);
}
return true;
}
})
}
这样提交到的SEY_RESULTASYNCROUTES这个mutations中就可以拿到上述处理完后的路由配置信息asyncRoutes了,但还需要做路由的合并操作,就是将先前定义的常量路由、任意路由和刚刚对比过滤完的异步路由三者进行合并(数组的concat方法),形成一个最终完整的路由信息,并给当前项目添加这个路由规则router.addRoutes(state.resultAllRoutes)
import router from '@/router'
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
routes:[], // 服务器返回的菜单标记信息
buttons:[], // 服务器返回的按钮标记信息
role:[], // 服务器返回的角色信息
resultAsyncRoutes:[],
resultAllRoutes:[]
}
}
const state = getDefaultState();
mutations:{
// 最终计算出的异步路由
SEY_RESULTASYNCROUTES(state,asyncRoutes){
// vuex保存当前用户的异步路由
state.resultAsyncRoutes = asyncRoutes;
// 计算出当前用户的所有路由
state.resultAllRoutes = constantRoutes.concat(state.resultAsyncRoutes,anyRoutes);
// 给路由添加新的路由
router.addRoutes(state.resultAllRoutes);
}
}
此时登陆后依旧无法正确显示左侧的菜单。
虽然此时正确的路由已经存在于router实例中,但左侧导航栏<Sidebar>组件遍历的依旧只有首页菜单,那是因为在routes/index.js中注册是常量路由如下所示,而修改操作router.addRoutes(state.resultAllRoutes)是异步操作以后的结果,在sidebar组件中通过 this.$router.options.routes只能获取初始的常量路由
router/index.js:
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
sidebar.vue
export default {
components: { SidebarItem, Logo },
computed: {
routes() {
return this.$router.options.routes;
},
}
}
只需将获取方式改为仓库中最新的路由配置信息this.$store.state.user.resultAllRoutes
sidebar.vue
routes() {
return this.$store.state.user.resultAllRoutes;
},
这样就实现了不同身份的用户登录后显示不同的菜单的效果。
此外,要在项目中实现按钮权限,即同一个按钮,有的用户显示按钮,有的用户不显示,只要拿到上述的buttons数组,在对应的按钮标签上使用v-show进行判断即可
<button v-show="$store.state.xxxModule.buttons.indexOf('对应的按钮标签名')">删除</button>