# 权限控制
权限控制是后台管理系统中最常见的功能,其实现基础逻辑可以总结为一句话,通过不同的角色动态生成不同权限进行访问控制,这其中最基础的点就是通过什么进行权限控制,一切都是围绕如何控制访问来执行,其他的操作都是来辅助这点的。
# 页面权限
后台管理系统的权限控制大概分为两种:
1、菜单权限控制
菜单权限控制也就是页面访问控制,某个用户或权限能访问哪些页面,能看到哪些页面。
2、操作权限控制
菜单权限是限制某些页面能否访问,操作权限则是针对某页面中的操作进行控制,比如修改、删除等操作。
操作权限要基于菜单权限才能实现,先控制能否访问页面后才会进行操作权限的判断,一般操作权限是附带于菜单权限的,所以我们权限控制主要是控制菜单权限。
# 实现方式
权限控制一般就分为前端实现和后端实现,后端实现方式会简单一点,前端实现比较复杂但对页面的控制度会比较灵活。这里我们主要以前端实现来讲解。
1、后端实现
后端实现是通过菜单管理页面添加与本地对应的菜单栏和操作权限,添加好后通过接口获取进行渲染就可以了。
2、前端实现
前端设置权限比较麻烦,前端需要自定义路由表,根据这个路由表和用户信息来动态匹配对应的路由。
# 权限控制流程
在Vue中控制页面访问,我们首先想到的就是路由导航守卫,通过router.beforeEach正好可以实现我们对路由的监听、控制访问、页面跳转等操作。
如下图所示,是路由监听的整个流程,所有操作都是围绕着从开始到结束进行,其他的分支只是辅助该流程完成,不论怎么判断,每次进入页面前都会重新走一遍该流程,其大概步骤可分为:
- 通过token判断是否登录
- 判断是否有权限列表(生成的路由表)
- 如果没有则根据角色/权限过滤生成路由表
- 动态添加路由
- 判断是否是导航页渲染页面
- 根据操作权限控制操作
根据上面的流程图,我们以前端的实现方式在src目录下创建一个 permission.ts 文件来实现路由权限判断,然后再在main.ts中引入。
/**
** 路由权限控制 **
* 监听每次路由跳转,进行过滤和筛选路由
*/
import storage from 'store'
import router from '@/router'
import { useEmpowerStore } from '@/stores/modules/empower'
// 基础路由
import { constantRouterMap } from '@/router/router.config'
import { ACCESS_TOKEN } from '@/constants/app'
import defaultSetting from '@/config/settings'
import Notification from '@arco-design/web-vue/es/notification'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'
import type { RouteRecordName } from 'vue-router'
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
const whiteList: RouteRecordName[] = constantRouterMap.filter(item => item.meta && item.meta.isWhite).map(el => el.name as RouteRecordName) // 白名单
const empowerList: RouteRecordName[] = ['Empower'] // 授权页面
const loginRoutePath = '/empower'
router.beforeEach((to, from, next) => {
NProgress.start()
// 获取store
const empowerStore = useEmpowerStore()
// 判断是否在白名单内
if (whiteList.includes(to.name as RouteRecordName)) {
next()
} else {
// 判断是否登录
if (storage.get(ACCESS_TOKEN)) {
if (empowerList.includes(to.name as RouteRecordName)) {
next({ path: '/' })
NProgress.done()
} else {
// 判断是否有页面权限列表
// 这一步获取区别前端和后端获取
if (!empowerStore.menuPermList.length) {
// 获取用户信息
empowerStore.getUserInfo()
.then(() => {
// 根据用户信息进行过滤返回对应路由
empowerStore.filterRoutes()
.then(() => {
// 动态添加可访问路由表
empowerStore.routerList.forEach(item => {
router.addRoute(item)
})
// 必要,要不然会死循环
next({ ...to, replace: true })
})
})
.catch(() => {
Notification.error({
title: '错误',
content: '请求用户信息失败,请重试'
})
// 失败时,清空历史保留信息
storage.clearAll()
window.location.reload()
})
} else {
next()
}
}
} else {
// 是授权页面,直接进入
if (empowerList.includes(to.name as RouteRecordName)) {
next()
} else {
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
NProgress.done()
}
}
}
})
router.afterEach(() => {
NProgress.done()
})
# 自定义路由表
要想前端实现对流程的控制,我们需要自定义一个路由表来存放需要判断的信息,这个路由表是根据用户权限定义的所有页面的路由列表,是所有页面的信息,后面不论是判断、过滤、渲染都是根据此表。下面就是如何定义我们的路由表。
1、是否需要登录
路由表可以分为需要登录的和不需要登录的,不需要登录的放在基础路由constantRouterMap列表里,如登录、注册、活动页等,需要登录的放在权限路由asyncRouterMap列表里。
2、白名单
在基础路由里添加白名单页面,不需要登录就可以访问,为了区分登录注册等页面,可以在meta中添加 isWhite 字段来表名此页面为白名单页面。
3、权限控制
在需要登录的路由列表里,通过在meta中 permission 字段给路由添加唯一标识,前端在分配权限时把这个标识符传个后端,后端在用户信息接口中返回,用来在路由过滤时判断。当然并不是每个页面都需要权限控制,我们还可以添加一个 isAuth 字段判断该路由是否要进行权限控制。
4、导航栏
需要在导航栏显示的路由统一放在根路由下的子路由中,通过嵌套路由和基础布局BasicLayout来实现。当然,也可以放在其他路由中实现多个不同的导航布局页面,这里为了统一都放在根路由下。不在导航栏的页面放在跟路由以外的页面走其他布局,比如个人中心直接全屏显示,或者在根路由中走导航的基础布局,但不显示,可以通过添加 hidden 字段控制,比如详情页。
5、缓存
嵌套子路由时使用RouteView组件来在当前页面进行显示子路由。并通过添加 keepAlive 字段来控制页面是否进行缓存。
import { shallowRef } from 'vue'
import BasicLayout from '@/layouts/BasicLayout.vue'
import RouteView from '@/layouts/RouteView.vue'
import { type RouteRecordRaw } from 'vue-router'
/**
* @desc 权限路由表
* 路由分为需要登录的不需要登录的
* 不需要登录的放在基础路由里,如登录、注册、活动页,需要登录的放在权限路由里,
* 白名单页面放在基础路由里,如活动页,通过meta字段isWhite区分登录注册页
* 需要登录的又分为需要权限控制和不需要权限控制的,通过meta字段isAuth进行判断需不需要权限判断
* 在导航栏显示的路由统一放在根路由下走基础布局
* 不在导航栏显示的可以放在根路由外面走其他布局,比如个人中心
* 或者在根路由走基础布局,通过hidden自动控制,比如详情页
* keepAlive是否缓存该组件,缓存必须在每一层router-view加keep-alive才会生效
* permission为权限id,全局必须保持唯一
* */
const Empower = () => import(/* webpackChunkName: 'empower' */ '@/views/empower/index.vue')
const Promotion = () => import(/* webpackChunkName: 'promotion' */ '@/views/promotion/index.vue')
const Exception = () => import(/* webpackChunkName: 'exception' */ '@/views/exception/index.vue')
const HomeComponent = () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
const MineComponent = () => import(/* webpackChunkName: 'mine' */ '@/views/mine/index.vue')
const Workbench = () => import(/* webpackChunkName: 'workbench' */ '@/views/workbench/index.vue')
const MapCharts = () => import(/* webpackChunkName: 'map-charts' */ '@/views/map-charts/index.vue')
const TableList = () => import(/* webpackChunkName: 'table-list' */ '@/views/table-list/index.vue')
const UserCenter = () => import(/* webpackChunkName: 'user-center' */ '@/views/user-center/index.vue')
// 权限路由
export const asyncRouterMap: RouteRecordRaw[] = [
{
path: '/',
name: 'Index',
component: shallowRef(BasicLayout),
meta: { hidden: false, keepAlive: false, isAuth: false },
children: [
{
path: '/home',
name: 'Home',
component: HomeComponent,
meta: { title: '主页', icon: 'icon-apps', hidden: false, keepAlive: true, isAuth: true, permission: 'home' }
},
{
path: '/dashboard',
name: 'Dashboard',
component: shallowRef(RouteView),
meta: { title: '仪表盘', icon: 'icon-dashboard', hidden: false, keepAlive: true, isAuth: true, permission: 'dashboard' },
children: [
{
path: '/workbench',
name: 'Workbench',
component: Workbench,
meta: { title: '工作台', icon: 'icon-common', hidden: false, keepAlive: false, isAuth: true, permission: 'workbench' }
},
{
path: '/map-charts',
name: 'MapCharts',
component: MapCharts,
meta: { title: '实时监控', icon: 'icon-computer', hidden: false, keepAlive: true, isAuth: true, permission: 'map_charts' }
}
]
},
{
path: '/table-list',
name: 'TableList',
component: TableList,
meta: { title: '列表页', icon: 'icon-list', hidden: false, keepAlive: true, isAuth: true, permission: 'table_list' }
},
{
path: 'https://arco.design/vue/docs/pro/faq',
name: 'Faq',
redirect: '',
meta: { title: '常见问题', icon: 'icon-apps', hidden: false, keepAlive: false, isOpen: true, isAuth: false, permission: '' }
},
{
path: '/user-center',
name: 'UserCenter',
component: UserCenter,
meta: { title: '个人中心', icon: '', hidden: true, keepAlive: true, isAuth: false, permission: 'account' }
}
]
},
{
path: '/mine',
name: 'Mine',
component: MineComponent,
meta: { title: '我的', icon: 'icon-heart-fill', hidden: true, keepAlive: false, isAuth: false, permission: '' }
}
]
// 基础路由
export const constantRouterMap: RouteRecordRaw[] = [
{
path: '/promotion',
name: 'Promotion',
component: Promotion,
meta: { title: '推广页', isWhite: true }
},
{
path: '/empower',
name: 'Empower',
component: Empower
},
// 异常处理
{
path: '/exception',
name: 'Exception',
component: Exception
}
]
# 动态获取权限表
在permission.ts整个流程中我们只进行流程判断,其他的操作都是辅助,所以在调用和过滤权限时,我们统一把获取用户信息和路由过滤提取出来都放在store中进行处理,正好store的action中也可以进行异常处理。我们把权限列表存在store中而不是缓存中,这是为了防止用户刷新页面,每次刷新都重新走一遍权限判断流程。
在store中通过用户信息接口获取用户角色或者权限,这个具体根据项目来,只是判断方式不同,我们把获取的角色或权限保存在store中进行权限判断。然后根据获取的角色或权限使用filterAsyncRouter 方法来过滤用户可以访问的页面路由,获取好后使用路由方法添加真实的vue路由中,并保存到store,后面用这个进行渲染导航栏。
// store
import storage from 'store'
import { defineStore } from 'pinia'
import { ACCESS_TOKEN } from '../../../constants/app'
import { loginApi, infoApi } from '@/api/empower'
import { asyncRouterMap } from '@/router/router.config'
import type { RouteRecordRaw } from 'vue-router'
import type { EmpowerState } from './types'
import type { LoginReq, LoginRes } from '@/api/empower/types'
export const useEmpowerStore = defineStore('empower', {
state: (): EmpowerState => ({
userInfo: {} as UserInfo,
routerList: [], // 路由列表
menuPermList: [], // 路由权限列表
operatePermList: [] // 操作权限列表
}),
getters: {},
actions: {
// 获取用户信息
getUserInfo() {
return new Promise((resolve, reject) => {
infoApi().then(res => {
const data = res.data
this.userInfo = data
// 获取权限列表
const { id, menu_perm, operate_perm } = data
this.menuPermList = menu_perm
this.operatePermList = operate_perm
if (menu_perm.length) {
resolve(menu_perm)
} else {
reject(new Error('角色必须是非空数组!'))
}
}).catch(error => {
reject(error)
})
})
},
// 根据权限列表过滤对应路由
filterRoutes() {
return new Promise((resolve, reject) => {
const routerList = this.filterAsyncRouter(asyncRouterMap, this.menuPermList)
console.log(routerList)
routerList.push({
path: '/:catchAll(.*)',
redirect: '/exception'
})
if (routerList.length > 1) {
this.routerList = routerList
resolve(routerList)
} else {
reject(new Error('无法获取该用户角色,请重新登录!'))
}
})
},
// 根据权限列表过滤路由
filterAsyncRouter(routerMap: RouteRecordRaw[], menu_perm: string[]) {
const routerList = routerMap.filter(route => {
if (route.meta) {
const { isAuth, permission } = route.meta
if (!isAuth || menu_perm.includes(permission as string)) {
if (route.children && route.children.length) {
route.children = this.filterAsyncRouter(route.children, menu_perm)
// 如果有子路由重定向到第一个
if (route.children.length) route.redirect = route.children[0].path
}
return true
}
}
return false
})
return routerList
}
}
})
# 导航栏渲染
通过权限控制获取到真正的路由后,就可以进行页面渲染了,后台管理页面的导航栏布局一般都是头部导航栏和左侧导航栏两种布局,我们可以创建多个布局页面来进行显示,可以在自定义路由表中切换,或者通过配置动态切换。
在基础布局中,通过store中获取的路由列表根路由下的子路由和hidden字段过滤出导航栏列表,然后使用UI布局的menu组件进行渲染展示。然后使用路由router-view组件在某个布局中显示子路由页面。
<template>
<a-spin dot :loading="publicStore.pageLoad" :style="{ width: '100%' }" tip="加载中...">
<a-layout class="basic">
<!-- 侧边导航栏 start -->
<a-layout-sider hide-trigger :width="220" collapsible :collapsed="collapsed">
<div class="logo" @click="backHome">
<img src="@/assets/images/logo.png" alt="logo" />
<div class="logo-title">{{appStore.appName}}</div>
</div>
<a-menu :selected-keys="state.selectedKeys" :open-keys="state.openKeys" :auto-scroll-into-view="true" :auto-open="true" :accordion="true" @sub-menu-click="subMenuClick" @menuItemClick="onClickMenuItem">
<template v-for="(item, index) in state.menuList" :key="index">
<a-menu-item :key="item.path" v-if="!item.children">
<template #icon>
<ArcoIcon :icon="(item?.meta?.icon as string)"></ArcoIcon>
</template>
<span>{{ item?.meta?.title }}</span>
</a-menu-item>
<a-sub-menu v-if="item.children && item.children.length" :key="item.path">
<template #icon>
<ArcoIcon :icon="(item?.meta?.icon as string)"></ArcoIcon>
</template>
<template #title>
<span>{{ item?.meta?.title }}</span>
</template>
<a-menu-item v-for="subItem in item.children" :key="subItem.path">
<template #icon>
<ArcoIcon :icon="(subItem?.meta?.icon as string)"></ArcoIcon>
</template>
<span>{{ subItem?.meta?.title }}</span>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</a-layout-sider>
<!-- 侧边导航栏 end -->
<a-layout>
<!-- 头部 start -->
<a-layout-header>
<GlobalHeader @COLLAPSE_EVENT="onCollapse"></GlobalHeader>
</a-layout-header>
<!-- 头部 end -->
<a-layout class="basic-layout">
<MultiTab></MultiTab>
<!-- <Breadcrumb></Breadcrumb> -->
<div class="layout-content">
<!-- 路由缓存,只针对当前子路由进行缓存 -->
<!-- RouteView 子路由模板名称,防止刷新子路由 -->
<RouterView v-slot="{ Component }">
<!-- <Transition name="fade" mode="out-in" appear> -->
<KeepAlive :include="['RouteView', ...publicStore.cacheList]">
<component :is="Component"></component>
</KeepAlive>
<!-- </Transition> -->
</RouterView>
</div>
<!-- <a-layout-footer>Footer</a-layout-footer> -->
</a-layout>
</a-layout>
</a-layout>
</a-spin>
</template>
<script setup lang="ts">
/**
* @description 侧栏菜单布局
* */
import { ref, reactive } from 'vue'
import { useRouter, useRoute, onBeforeRouteUpdate, type RouteRecordRaw } from 'vue-router'
import { useAppStore } from '@/stores/modules/app'
import { usePublicStore } from '@/stores/modules/public'
import { useEmpowerStore } from '@/stores/modules/empower'
import regExp from '@/utils/regExp'
import GlobalHeader from '@/components/GlobalHeader.vue'
import MultiTab from '@/components/MultiTab.vue'
// import Breadcrumb from '@/components/Breadcrumb.vue'
import ArcoIcon from '@/components/ArcoIcon'
const appStore = useAppStore()
const publicStore = usePublicStore()
const empowerStore = useEmpowerStore()
const router = useRouter()
const route = useRoute()
const collapsed = ref(false) // 折叠导航栏
interface State {
openKeys: string[]
menuList: RouteRecordRaw[]
selectedKeys: string[]
}
const state = reactive<State>({
openKeys: [],
menuList: [],
selectedKeys: []
})
// 获取路由列表
const getMeunList = (routerList: RouteRecordRaw[] = []) => {
const menuList = routerList.filter(item => {
if (!item?.meta?.hidden) {
if (item.children && item.children.length) {
item.children = getMeunList(item.children)
}
return true
}
return false
})
return menuList
}
const routerList = getMeunList(empowerStore.routerList[0].children)
state.menuList = routerList
state.selectedKeys = [route.path]
// 路由跳转获取展开key
const getOpenKeys = (path: string) => {
state.menuList.forEach(item => {
if (item.children && item.children.length) {
const bool = item.children.map(sub => sub.path).includes(path)
if (bool) state.openKeys = [item.path]
}
})
}
getOpenKeys(route.path)
// 监听当前路由更改
onBeforeRouteUpdate((to, from, next) => {
getOpenKeys(to.path)
state.selectedKeys = [to.path]
next()
})
// 折叠展开导航栏
const onCollapse = () => {
collapsed.value = !collapsed.value
}
// 展开子菜单
const subMenuClick = (key: string, openKeys: string[]) => {
state.openKeys = openKeys
}
// 路由跳转
const onClickMenuItem = (key: string) => {
state.selectedKeys = [key]
if (regExp.urlReg.test(key)) {
window.open(key)
} else {
router.push({
path: key
})
}
}
// 点击logo返回主页
const backHome = () => {
router.push({
path: '/'
})
}
</script>
<style lang="less" scoped>
.basic {
width: 100%;
height: 100vh;
background-color: #fff;
:deep(.arco-layout-sider) {
height: 100%;
overflow-y: auto;
.logo {
display: flex;
align-items: center;
width: 100%;
height: 64px;
padding: 0 10px;
overflow: hidden;
box-shadow: 1px 1px 1px #ccc;
transition: all 0.3s;
cursor: pointer;
img {
width: 30px;
height: 30px;
}
.logo-title {
font-size: 16px;
font-weight: 600;
margin-left: 10px;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
:deep(.arco-layout-header) {
height: 64px;
line-height: 64px;
background: var(--color-bg-3);
}
.basic-layout {
width: 100%;
overflow: hidden;
.layout-content {
width: 100%;
height: 100%;
padding: 20px 20px;
font-size: 14px;
background-color: #f2f2f2;
overflow-y: auto;
}
}
}
:deep(.arco-badge-dot) {
right: -5px;
// right: -10px;
// top: -1px;
}
</style>
# 缓存页面
缓存页面通过keepAlive判断是否开启,然后使用keep-alive组件进行判断是否缓存,这也是多标签页中同时使用的功能。
操作权限控制
页面通过动态获取权限列表进行权限限制访问,操作权限则需要进入页面进行细粒度控制,对操作按钮进行控制和逻辑处理进行判断。我们在页面中可以拿到操作权限进行判断,但是每个页面都获取权限和写判断方法太麻烦,对应按钮控制我们可以自定义指令来绑定到按钮上来控制,逻辑判断可以抽离成公共方法放在全局上,使用时直接传入对应的权限。
1、自定义权限全局指令
/**
* 全局权限指令
* @description 传入权限,进行判断是否显示,具体根据后端返回判断
* @author changz
* @param {String} - 权限
* @example
* <a-button v-permission="'role_auth_per'">添加</a-button>
* */
// 或者在util中定义一个公共函数,传入权限通过v-if判断是否显示当前操作
import type { DirectiveBinding, VNode } from 'vue'
import { useEmpowerStore } from '@/stores/modules/empower'
export default {
mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
// 当前用户拥有的权限列表
const empowerStore = useEmpowerStore()
const { menuPermList, operatePermList } = empowerStore
const permList = menuPermList.concat(operatePermList)
const permission = binding.value // 获取权限值
if (!permList.includes(permission)) {
el.parentElement?.removeChild(el) // 不拥有该权限移除dom元素
}
}
}
2、定义全局权限方法 定义一个全局方法判断权限,在HTML和JS中都可以进行逻辑判断。
// utils/permission.ts
/**
* 全局权限判断方法
* @description 传入需要判断的权限,判断是否存在改权限
* @author changz
* @param {String} [per] - 页面权限
* @return {boolean} - true/false
* @example
* <a-button v-if="hasPer('home')">添加</a-button>
* */
import { useEmpowerStore } from '@/stores/modules/empower'
export default function hasPer(per: string): boolean {
// 当前用户拥有的权限列表
const empowerStore = useEmpowerStore()
const { menuPermList, operatePermList } = empowerStore
const permList = menuPermList.concat(operatePermList)
return permList.includes(per)
}
在main.ts中引入并挂在在全局上,挂载全局一定要先使用ComponentCustomProperties进行类型声明才可以。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import directive from './directive'
import hasPer from '@/utils/permissions'
import './permission'
// 配置mitt公共组件传参
import mitt from 'mitt'
const emitter = mitt()
// TS注册
// 必须要扩展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof emitter
$hasPer: typeof hasPer
}
}
const app = createApp(App)
app.use(store)
app.use(router)
app.use(directive)
// 挂载到全局属性上
app.config.globalProperties.$Bus = emitter
app.config.globalProperties.$hasPer = hasPer
app.mount('#app')
在组件中使用:
<template>
<div class="index">
<a-button v-if="$hasPer('home')" type="outline" >主题</a-button>
<a-button type="primary" @click="openDetail">详情</a-button>
</div>
</template>
<script setup lang="ts" name="Home">
import { ref, getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const openDetail = () => {
if (instance?.proxy?.$hasPer('home')) {
console.log(111)
}
}
</script>
# 分配权限
前端控制权限最后一步就是对角色进行权限分配,分配权限需要先从后端获取某个角色的权限,然后我们需要维护一份json数据来渲染分配页面,这个数据和自定义路由表的权限一样,只是用来根据后端返回的权限做页面渲染和权限设置,最后再把设置好的权限列表传给后端。json数据可以根据页面显示自行设计,大概如下。
// json/permission.json.ts
/**
* 权限配置JSON表
* @description 用来给每个用户分配权限,对应页面路由权限和操作权限
* permList 路由权限。
* operateList 为页面中的操作权限,通过判断让元素是否显示
* 可以通过 v-permission指令或者$hasPer在js中判断
* */
import { ref } from 'vue'
export interface OperateList {
title: string
perm: string
isCheck: boolean
isShow: boolean
}
export interface PermList {
title: string
perm: string
}
export interface PermissionList {
title: string
permList: PermList[]
isCheck: boolean
indeterminate: boolean
isDisable: boolean
operateList: OperateList[]
}
export const permissionList = ref<PermissionList[]>([
{
title: '主页',
permList: [
{ title: '主页', perm: 'home' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: []
},
{
title: '工作台',
permList: [
{ title: '工作台', perm: 'workbench' },
{ title: '仪表盘', perm: 'dashboard' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: []
},
{
title: '实时监控',
permList: [
{ title: '实时监控', perm: 'map_charts' },
{ title: '仪表盘', perm: 'dashboard' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: []
},
{
title: '用户组织',
permList: [
{ title: '用户组织', perm: 'org_manage' },
{ title: '角色组织', perm: 'role_org' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: []
},
{
title: '角色权限',
permList: [
{ title: '角色权限', perm: 'role_manage' },
{ title: '角色组织', perm: 'role_org' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: [
{
title: '角色管理',
perm: 'role_per',
isCheck: false,
isShow: true
}
]
},
{
title: '系统设置',
permList: [
{ title: '系统设置', perm: 'system_setting' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: []
},
{
title: '消息中心',
permList: [
{ title: '消息中心', perm: 'message_center' }
],
isCheck: false,
indeterminate: false,
isDisable: false,
operateList: [
// {
// title: '资产管理',
// perm: 'asset_jcgl_per',
// isCheck: false,
// isShow: true
// }
]
}
])