使用VUE开发后台管理系统 完全由后端控制左边菜单项思路
在传统开发后台管理系统时,都会涉及权限控制这一功能需求 即:根据不同登录的角色账号来使用该账号拥有的功能,也就是说系统左边的菜单栏不是固定不变的,而是根据登录账号的权限去动态控制的,现在主流的两种模式
即:1.前后端配合控制 2.完全由后端来控制
本章着重来介绍 第二种模式 :由后端数据控制前端的菜单
- 借助Ant Design Pro Vue 来讲解实现思路 (Ant-Design-Pro Vue 开箱即用的企业后台管理系统)
- 码云地址 :https://gitee.com/sendya/ant-design-pro-vue.git
一.实现思路
-
后端提供路由接口 前端请求数据然后二次处理
-
前端请求到的数据
-
前端处理数据的逻辑
// eslint-disable-next-line
import * as loginService from '@/api/login'
// eslint-disable-next-line
import { BasicLayout, BlankLayout, PageView, RouteView } from '@/layouts'// 前端路由表
const constantRouterComponents = {// 基础页面 layout 必须引入BasicLayout: BasicLayout,BlankLayout: BlankLayout,RouteView: RouteView,PageView: PageView,'403': () => import(/* webpackChunkName: "error" */ '@/views/exception/403'),'404': () => import(/* webpackChunkName: "error" */ '@/views/exception/404'),'500': () => import(/* webpackChunkName: "error" */ '@/views/exception/500'),// 你需要动态引入的页面组件'Workplace': () => import('@/views/dashboard/Workplace'),'Analysis': () => import('@/views/dashboard/Analysis'),// form'BasicForm': () => import('@/views/form/basicForm'),'StepForm': () => import('@/views/form/stepForm/StepForm'),'AdvanceForm': () => import('@/views/form/advancedForm/AdvancedForm'),// // list'TableList': () => import('@/views/list/TableList'),// 'StandardList': () => import('@/views/list/StandardList'),'CardList': () => import('@/views/list/CardList'),'SearchLayout': () => import('@/views/list/search/SearchLayout'),'SearchArticles': () => import('@/views/list/search/Article'),'SearchProjects': () => import('@/views/list/search/Projects'),'SearchApplications': () => import('@/views/list/search/Applications'),'ProfileBasic': () => import('@/views/profile/basic'),'ProfileAdvanced': () => import('@/views/profile/advanced/Advanced'),// result'ResultSuccess': () => import(/* webpackChunkName: "result" */ '@/views/result/Success'),'ResultFail': () => import(/* webpackChunkName: "result" */ '@/views/result/Error'),// exception'Exception403': () => import(/* webpackChunkName: "fail" */ '@/views/exception/403'),'Exception404': () => import(/* webpackChunkName: "fail" */ '@/views/exception/404'),'Exception500': () => import(/* webpackChunkName: "fail" */ '@/views/exception/500'),// account'AccountCenter': () => import('@/views/account/center'),'AccountSettings': () => import('@/views/account/settings/Index'),'BaseSettings': () => import('@/views/account/settings/BaseSetting'),'SecuritySettings': () => import('@/views/account/settings/Security'),'CustomSettings': () => import('@/views/account/settings/Custom'),'BindingSettings': () => import('@/views/account/settings/Binding'),'NotificationSettings': () => import('@/views/account/settings/Notification'),'TestWork': () => import(/* webpackChunkName: "TestWork" */ '@/views/dashboard/TestWork')
}// 前端未找到页面路由(固定不用改)
const notFoundRouter = {path: '*', redirect: '/404', hidden: true
}// 根级菜单
const rootRouter = {key: '',name: 'index',path: '',component: 'BasicLayout',redirect: '/dashboard',meta: {title: '首页'},children: []
}/*** 动态生成菜单* @param token* @returns {Promise<Router>}*/
export const generatorDynamicRouter = (token) => {return new Promise((resolve, reject) => {loginService.getCurrentUserNav(token).then(res => {console.log('res', res) //后端请求到的数据const { result } = resconst menuNav = []const childrenNav = []// 后端数据, 根级树数组, 根级 PIDlistToTree(result, childrenNav, 0) //路由数据 第一次处理rootRouter.children = childrenNav menuNav.push(rootRouter)console.log('menuNav', menuNav)const routers = generator(menuNav) //路由数据 第二次处理routers.push(notFoundRouter)console.log('routers', routers)resolve(routers)}).catch(err => {reject(err)})})
}/*** 格式化树形结构数据 生成 vue-router 层级路由表** @param routerMap* @param parent* @returns {*}*///路由数据第二次处理 匹配加载组件 生成路由path
export const generator = (routerMap, parent) => { return routerMap.map(item => {const { title, show, hideChildren, hiddenHeaderContent, target, icon } = item.meta || {}const currentRouter = {// 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplacepath: item.path || `${parent && parent.path || ''}/${item.key}`,// 路由名称,建议唯一name: item.name || item.key || '',// 该路由对应页面的 组件 :方案1// component: constantRouterComponents[item.component || item.key],// 该路由对应页面的 组件 :方案2 (动态加载)component: (constantRouterComponents[item.component || item.key]) || (() => import(`@/views/${item.component}`)),// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)meta: {title: title,icon: icon || undefined,hiddenHeaderContent: hiddenHeaderContent,target: target,permission: item.name}}// 是否设置了隐藏菜单if (show === false) {currentRouter.hidden = true}// 是否设置了隐藏子菜单if (hideChildren) {currentRouter.hideChildrenInMenu = true}// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠if (!currentRouter.path.startsWith('http')) {currentRouter.path = currentRouter.path.replace('//', '/')}// 重定向item.redirect && (currentRouter.redirect = item.redirect)// 是否有子菜单,并递归处理if (item.children && item.children.length > 0) {// RecursioncurrentRouter.children = generator(item.children, currentRouter)}return currentRouter})
}/*** 数组转树形结构* @param list 源数组* @param tree 树* @param parentId 父ID*/// 路由数据第一次 处理 将list结构转化为 tree
const listToTree = (list, tree, parentId) => {list.forEach(item => {// 判断是否为父级菜单if (item.parentId === parentId) {const child = {...item,key: item.key || item.name,children: []}// 迭代 list, 找到当前菜单相符合的所有子菜单listToTree(list, child.children, item.id)// 删掉不存在 children 值的属性if (child.children.length <= 0) {delete child.children}// 加入到树中tree.push(child)}})
}
最终生成的 路由数据
{path: '/',name: 'index',component: BasicLayout,meta: { title: 'menu.home' },redirect: '/dashboard/workplace',children: [// dashboard{path: '/dashboard',name: 'dashboard',redirect: '/dashboard/workplace',component: RouteView,meta: { title: 'menu.dashboard', keepAlive: true, icon: bxAnaalyse, permission: [ 'dashboard' ] },children: [{path: '/dashboard/analysis/:pageNo([1-9]\\d*)?',name: 'Analysis',component: () => import('@/views/dashboard/Analysis'),meta: { title: 'menu.dashboard.analysis', keepAlive: false, permission: [ 'dashboard' ] }},// 外部链接{path: 'https://www.baidu.com/',name: 'Monitor',meta: { title: 'menu.dashboard.monitor', target: '_blank' }},{path: '/dashboard/workplace',name: 'Workplace',component: () => import('@/views/dashboard/Workplace'),meta: { title: 'menu.dashboard.workplace', keepAlive: true, permission: [ 'dashboard' ] }}]},// forms{path: '/form',redirect: '/form/base-form',component: RouteView,meta: { title: '表单页', icon: 'form', permission: [ 'form' ] },children: [{path: '/form/base-form',name: 'BaseForm',component: () => import('@/views/form/basicForm'),meta: { title: '基础表单', keepAlive: true, permission: [ 'form' ] }},{path: '/form/step-form',name: 'StepForm',component: () => import('@/views/form/stepForm/StepForm'),meta: { title: '分步表单', keepAlive: true, permission: [ 'form' ] }},{path: '/form/advanced-form',name: 'AdvanceForm',component: () => import('@/views/form/advancedForm/AdvancedForm'),meta: { title: '高级表单', keepAlive: true, permission: [ 'form' ] }}]},// list{path: '/list',name: 'list',component: RouteView,redirect: '/list/table-list',meta: { title: '列表页', icon: 'table', permission: [ 'table' ] },children: [{path: '/list/table-list/:pageNo([1-9]\\d*)?',name: 'TableListWrapper',hideChildrenInMenu: true, // 强制显示 MenuItem 而不是 SubMenucomponent: () => import('@/views/list/TableList'),meta: { title: '查询表格', keepAlive: true, permission: [ 'table' ] }},{path: '/list/basic-list',name: 'BasicList',component: () => import('@/views/list/BasicList'),meta: { title: '标准列表', keepAlive: true, permission: [ 'table' ] }},{path: '/list/card',name: 'CardList',component: () => import('@/views/list/CardList'),meta: { title: '卡片列表', keepAlive: true, permission: [ 'table' ] }},{path: '/list/search',name: 'SearchList',component: () => import('@/views/list/search/SearchLayout'),redirect: '/list/search/article',meta: { title: '搜索列表', keepAlive: true, permission: [ 'table' ] },children: [{path: '/list/search/article',name: 'SearchArticles',component: () => import('../views/list/search/Article'),meta: { title: '搜索列表(文章)', permission: [ 'table' ] }},{path: '/list/search/project',name: 'SearchProjects',component: () => import('../views/list/search/Projects'),meta: { title: '搜索列表(项目)', permission: [ 'table' ] }},{path: '/list/search/application',name: 'SearchApplications',component: () => import('../views/list/search/Applications'),meta: { title: '搜索列表(应用)', permission: [ 'table' ] }}]}]},// profile{path: '/profile',name: 'profile',component: RouteView,redirect: '/profile/basic',meta: { title: '详情页', icon: 'profile', permission: [ 'profile' ] },children: [{path: '/profile/basic',name: 'ProfileBasic',component: () => import('@/views/profile/basic'),meta: { title: '基础详情页', permission: [ 'profile' ] }},{path: '/profile/advanced',name: 'ProfileAdvanced',component: () => import('@/views/profile/advanced/Advanced'),meta: { title: '高级详情页', permission: [ 'profile' ] }}]},// result{path: '/result',name: 'result',component: RouteView,redirect: '/result/success',meta: { title: '结果页', icon: 'check-circle-o', permission: [ 'result' ] },children: [{path: '/result/success',name: 'ResultSuccess',component: () => import(/* webpackChunkName: "result" */ '@/views/result/Success'),meta: { title: '成功', keepAlive: false, hiddenHeaderContent: true, permission: [ 'result' ] }},{path: '/result/fail',name: 'ResultFail',component: () => import(/* webpackChunkName: "result" */ '@/views/result/Error'),meta: { title: '失败', keepAlive: false, hiddenHeaderContent: true, permission: [ 'result' ] }}]},// Exception{path: '/exception',name: 'exception',component: RouteView,redirect: '/exception/403',meta: { title: '异常页', icon: 'warning', permission: [ 'exception' ] },children: [{path: '/exception/403',name: 'Exception403',component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/403'),meta: { title: '403', permission: [ 'exception' ] }},{path: '/exception/404',name: 'Exception404',component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404'),meta: { title: '404', permission: [ 'exception' ] }},{path: '/exception/500',name: 'Exception500',component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/500'),meta: { title: '500', permission: [ 'exception' ] }}]},// account{path: '/account',component: RouteView,redirect: '/account/center',name: 'account',meta: { title: '个人页', icon: 'user', keepAlive: true, permission: [ 'user' ] },children: [{path: '/account/center',name: 'center',component: () => import('@/views/account/center'),meta: { title: '个人中心', keepAlive: true, permission: [ 'user' ] }},{path: '/account/settings',name: 'settings',component: () => import('@/views/account/settings/Index'),meta: { title: '个人设置', hideHeader: true, permission: [ 'user' ] },redirect: '/account/settings/base',hideChildrenInMenu: true,children: [{path: '/account/settings/base',name: 'BaseSettings',component: () => import('@/views/account/settings/BaseSetting'),meta: { title: '基本设置', hidden: true, permission: [ 'user' ] }},{path: '/account/settings/security',name: 'SecuritySettings',component: () => import('@/views/account/settings/Security'),meta: { title: '安全设置', hidden: true, keepAlive: true, permission: [ 'user' ] }},{path: '/account/settings/custom',name: 'CustomSettings',component: () => import('@/views/account/settings/Custom'),meta: { title: '个性化设置', hidden: true, keepAlive: true, permission: [ 'user' ] }},{path: '/account/settings/binding',name: 'BindingSettings',component: () => import('@/views/account/settings/Binding'),meta: { title: '账户绑定', hidden: true, keepAlive: true, permission: [ 'user' ] }},{path: '/account/settings/notification',name: 'NotificationSettings',component: () => import('@/views/account/settings/Notification'),meta: { title: '新消息通知', hidden: true, keepAlive: true, permission: [ 'user' ] }}]}]}
前端页面显示