vue-element-admin 动态菜单改造

发布时间 2023-04-15 16:53:45作者: soft1314

vue-element-admin 动态菜单改造

vue-element-admin 是一款优秀后台前端解决方案,它基于 vue 和 element-ui实现。开源后台管理系统解决方案项目 Boot-admin的前端模块就是基于vue-element-admin开发而来。

作为一款纯前端的后台界面解决方案,vue-element-admin是通过遍历路由进行渲染,从而得到菜单列表的,我们可以在 router.js 中看到相关代码,即是路由也是菜单。

改造思路:实现前后端分离要求,服务端控制菜单是否显示,前端控制路由信息定义。前端开发时不需要找服务端来新增路由信息,后端不需要关心前端路由的父/子关系、图标等定义信息。

第1步.定义路由

在 src/router/index.js 中将不需要后台控制的路由定义在 constantRoutes 中,如 /login /404 等;而需要后台控制是否显示的路由定义在 asyncRoutes 中。asyncRoutes 中每个节点都添加 srvName 属性,通过它来和服务端返回的菜单信息进行关联。

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '@/layout'

/* 外部路由文件 */
import sysManageRouter from './modules/sysmanage.js'
import codeGeneratorRouter from './modules/codegenerator.js'
import myWorkRouter from './modules/mywork.js'

/**
 * 同步路由
 * 不需要后台权限控制的路由,所有角色均可操作
 */
export const constantRoutes = [{
  path: '/redirect',
  component: Layout,
  hidden: true,
  children: [{
    path: '/redirect/:path(.*)',
    component: () => import('@/views/redirect/index')
  }]
},
{
  path: '/login',
  component: () => import('@/views/login/index'),
  hidden: true
},
{
  path: '/auth-redirect',
  component: () => import('@/views/login/auth-redirect'),
  hidden: true
},
{
  path: '/404',
  component: () => import('@/views/error-page/404'),
  hidden: true
},
{
  path: '/401',
  component: () => import('@/views/error-page/401'),
  hidden: true
},
{
  path: '/',
  component: Layout,
  redirect: '/dashboard',
  children: [{
    path: 'dashboard',
    component: () => import('@/views/dashboard/index'),
    name: 'Dashboard',
    meta: {
      title: '仪表板',
      icon: 'dashboard',
      affix: true
    }
  }]
},
]

/**
 * 异步路由
 * 基于后台动态控制的路由
 */
export const asyncRoutes = [
  /** 引入系统管理路由模块 **/
  sysManageRouter,
  /** 引入代码生成路由模块 **/
  codeGeneratorRouter,
  /** 引入工作流路由模块 **/
  myWorkRouter,

  // 404 page must be placed at the end !!!
  {
    path: '*',
    redirect: '/404',
    hidden: true
  }
]

const createRouter = () => new Router({
  scrollBehavior: () => ({
    y: 0
  }),
  routes: constantRoutes
})

const router = createRouter()
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

在 src/router/modules 目录下,新建路由子模块文件

  • 系统管理 sysmanage.js
  • 代码生成 codegenerator.js
  • 工作流 mywork.js

sysmanage.js内容如下:

import Layout from '@/layout'

const sysManageRouter = {
  path: '/manage',
  name: 'SysManage',
  component: Layout,
  redirect: '/manage/basemanage/dictionary',
  srvName: '/api/system/auth/manage',
  meta: {
    title: '系统管理',
    icon: 'example'
  },
  children: [{
    path: 'basemanage',
    name: 'BaseManage',
    srvName: '/api/system/auth/manage/basemanage',
    component: () => import('@/views/manage/basemanage/index'),
    redirect: '/manage/basemanage/dictionary',
    meta: {
      title: '基础管理',
      icon: 'tree'
    },
    children: [{
      path: 'dictionary',
      name: 'DicManage',
      srvName: '/api/system/auth/manage/basemanage/dictionary',
      component: () => import('@/views/manage/basemanage/dictionary/index'),
      meta: {
        title: '字典管理',
        icon: 'tree'
      }
    },
    {
      path: 'region',
      name: 'DivManage',
      srvName: '/api/system/auth/manage/basemanage/region',
      component: () => import('@/views/manage/basemanage/region/index'),
      meta: {
        title: '区域管理',
        icon: 'tree'
      }
    },
    {
      path: 'organization',
      name: 'OrgManage',
      srvName: '/api/system/auth/manage/basemanage/organization',
      component: () => import('@/views/manage/basemanage/organization/index'),
      meta: {
        title: '组织管理',
        icon: 'tree'
      }
    },
    {
      path: 'employee',
      name: 'EmpManage',
      srvName: '/api/system/auth/manage/basemanage/employee',
      component: () => import('@/views/manage/basemanage/employee/index'),
      meta: {
        title: '人员管理',
        icon: 'tree'
      }
    }
    ]
  },
  {
    path: 'authmanage',
    name: 'AuthManage',
    srvName: '/api/system/auth/manage/authmanage',
    component: () => import('@/views/manage/authmanage/index'),
    redirect: '/manage/authmanage/menu',
    meta: {
      title: '权限管理',
      icon: 'tree'
    },
    children: [{
      path: 'menu',
      name: 'MenuManage',
      srvName: '/api/system/auth/manage/authmanage/menu',
      component: () => import('@/views/manage/authmanage/menu'),
      meta: {
        title: '菜单管理',
        icon: 'table'
      }
    }, {
      path: 'resource',
      name: 'ResourceManage',
      srvName: '/api/system/auth/manage/authmanage/resource',
      component: () => import('@/views/manage/authmanage/resource'),
      meta: {
        title: '功能管理',
        icon: 'table'
      }
    }, {
      path: 'user',
      name: 'UserManage',
      srvName: '/api/system/auth/manage/authmanage/user',
      component: () => import('@/views/manage/authmanage/user'),
      meta: {
        title: '用户管理',
        icon: 'tree'
      }
    },
    {
      path: 'role',
      name: 'RoleManage',
      srvName: '/api/system/auth/manage/authmanage/role',
      component: () => import('@/views/manage/authmanage/role'),
      meta: {
        title: '角色管理',
        icon: 'tree'
      }
    }, {
      path: 'userofrole',
      name: 'UserOfRoleManage',
      srvName: '/api/system/auth/manage/authmanagele/userofrole',
      component: () => import('@/views/manage/authmanage/userofrole'),
      meta: {
        title: '角色-用户',
        icon: 'tree'
      }
    }, {
      path: 'resourceofrole',
      name: 'ResourceOfRoleManage',
      srvName: '/api/system/auth/manage/authmanage/resourceofrole',
      component: () => import('@/views/manage/authmanage/resourceofrole/index'),
      meta: {
        title: '角色-功能',
        icon: 'tree'
      }
    }
    ]
  },
  {
    path: 'operationmanage',
    name: 'OperationManage',
    srvName: '/api/system/auth/manage/operationmanage',
    component: () => import('@/views/manage/operationmanage/index'),
    meta: {
      title: '运行管理',
      icon: 'tree'
    },
    children: [{
      path: 'online',
      name: 'OnlineManage',
      srvName: '/api/system/auth/manage/operationmanage/online',
      component: () => import('@/views/manage/operationmanage/online/index'),
      meta: {
        title: '在线用户',
        icon: 'tree'
      }
    },
    {
      path: 'job',
      name: 'JobManage',
      srvName: '/api/system/auth/manage/operationmanage/job',
      component: () => import('@/views/manage/operationmanage/job/index'),
      meta: {
        title: '定时任务',
        icon: 'tree'
      }
    },
    {
      path: 'task',
      name: 'TaskManage',
      srvName: '/api/system/auth/manage/operationmanage/task',
      component: () => import('@/views/manage/operationmanage/task/index'),
      meta: {
        title: '流程任务',
        icon: 'tree'
      }
    },
    {
      path: 'histask',
      name: 'HisTaskManage',
      srvName: '/api/system/auth/manage/operationmanage/task/his',
      component: () => import('@/views/manage/operationmanage/histask/index'),
      meta: {
        title: '历史任务',
        icon: 'tree'
      }
    },
    {
      path: 'log',
      name: 'LogManage',
      srvName: '/api/system/auth/manage/operationmanage/log',
      component: () => import('@/views/manage/operationmanage/log/index'),
      meta: {
        title: '系统日志',
        icon: 'tree'
      }
    },
    {
      path: 'nacos',
      name: 'nacos',
      srvName: '/api/system/auth/manage/operationmanage/nacos',
      component: () => import('@/views/manage/operationmanage/nacos/index'),
      meta: {
        title: 'Nacos',
        icon: 'link'
      }
    },
    {
      path: 'admin',
      name: 'admin',
      srvName: '/api/system/auth/manage/operationmanage/admin',
      component: () => import('@/views/manage/operationmanage/admin/index'),
      meta: {
        title: 'Admin',
        icon: 'link'
      }
    }
    ]
  },
  {
    path: 'definitionmanage',
    name: 'DefManage',
    srvName: '/api/system/auth/manage/definitionmanage',
    component: () => import('@/views/manage/definitionmanage/index'),
    meta: {
      title: '定义管理',
      icon: 'tree'
    },
    children: [
      {
        path: 'model',
        name: 'ModelManage',
        srvName: '/api/system/auth/manage/definitionmanage/model',
        component: () => import('@/views/manage/definitionmanage/model/index'),
        meta: {
          title: '模型管理',
          icon: 'tree'
        }
      },
      {
        path: 'process',
        name: 'ProcessManage',
        srvName: '/api/system/auth/manage/definitionmanage/process',
        component: () => import('@/views/manage/definitionmanage/process/index'),
        meta: {
          title: '流程管理',
          icon: 'tree'
        }
      },
      {
        path: 'drools',
        name: 'DroolsManage',
        srvName: '/api/system/auth/manage/definitionmanage/drools',
        component: () => import('@/views/manage/definitionmanage/drools/index'),
        meta: {
          title: '规则管理',
          icon: 'tree'
        }
      }
    ]
  },
  {
    path: 'datamaintain',
    name: 'DataMaintain',
    srvName: '/api/system/auth/manage/datamaintain',
    component: () => import('@/views/manage/datamaintain/index'),
    meta: {
      title: '数据处理',
      icon: 'tree'
    },
    children: [{
      path: 'sqlinput',
      name: 'SqlInput',
      srvName: '/api/system/auth/manage/datamaintain/sqlinput',
      component: () => import('@/views/manage/datamaintain/sqlinput/index'),
      meta: {
        title: '提交',
        icon: 'tree'
      }
    }, {
      path: 'sqlexec',
      name: 'sqlexec',
      srvName: '/api/system/auth/manage/datamaintain/sqlexec',
      component: () => import('@/views/manage/datamaintain/sqlexec/index'),
      meta: {
        title: '执行',
        icon: 'tree'
      }
    }]
  }
  ]
}
export default sysManageRouter

第2步.服务端接口定义

服务端接口返回数据格式如下:

@Data
public class MenuDTO {
    private String id;
    private String srvName;
    private Boolean show;
    private String accessControlStyle;
}

节点中 srvName 和前端的路由进行匹配,通过 show 属性来确定显示或隐藏。
服务端无需关心菜单的子/父级关系,只需要将所有的菜单信息输出一个数组即可。

    @GetMapping("/auth/user/menu")
    public List<MenuDTO> getMenus() throws Exception{
        BaseUser baseUser = UserTool.getBaseUser();
        List<MenuDTO> menuDTOList = resourceDataGetter.getMyselfMenuList(baseUser);
        return menuDTOList;
    }

第3步.定义 api 请求接口

在 src/api/ 目录下创建 menus.js

import request from '@/utils/request'
export function getMenus(token) {
  return request({
    url: '/api/system/auth/user/menu',
    method: 'get',
    params: { token }
  })
}

第4步.配置 store 调用

新增文件 src/store/modules/menus.js

import {
  Message
} from 'element-ui'
import {
  getMenus
} from '@/api/menu'
import {
  getToken
} from '@/utils/auth'
import {
  asyncRoutes
} from '@/router/index'

const getDefaultState = () => {
  return {
    token: getToken(),
    menuList: []
  }
}

const state = getDefaultState()

const mutations = {
  SET_MENUS: (state, menus) => {
    state.menuList = menus
  }
}

// 动态菜单定义在前端,后台只会返回有权限的菜单列表,通过遍历服务端的菜单数据,没有的将对于菜单进行隐藏,前端新增页面无需先通过服务端进行菜单添加,遵循了前后端分离原则
export function generaMenu(routes, srvMenus) {
  for (let i = 0; i < routes.length; i++) {
    const routeItem = routes[i]
    var showItem = false
    for (let j = 0; j < srvMenus.length; j++) {
      const srvItem = srvMenus[j]

      // 前后端数据通过 srvName 属性来匹配
      if (routeItem.srvName !== undefined && routeItem.srvName === srvItem.srvName && srvItem.show === true) {
        showItem = true
        routes[i]['hidden'] = false
        break
      }
    }
    if (showItem === false) {
      routes[i]['hidden'] = true
    }

    if (routeItem['children'] !== undefined && routeItem['children'].length > 0) {
      generaMenu(routes[i]['children'], srvMenus)
    }
  }
}

const actions = {
  getMenus({
    commit
  }) {
    return new Promise((resolve, reject) => {
      getMenus(state.token).then(response => {
        if (response.code !== 100) {
          Message({
            message: response.message,
            type: 'error',
            duration: 5 * 1000
          })
          reject(response.message)
        }

        const {
          data
        } = response
        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const srvMenus = data
        var pushRouter = asyncRoutes
        generaMenu(pushRouter, srvMenus)
        commit('SET_MENUS', pushRouter)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  }
}

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

第5步.修改路由钩子,渲染动态菜单

修改src/permission.js文件

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
    } else {
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          const { roles } = await store.dispatch('user/getInfo')
          // 获取菜单
          await store.dispatch('menu/getMenus')
          // 生成动态路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 添加动态路由
          router.addRoutes(accessRoutes)

          next({ ...to, replace: true })
        } catch (error) {
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

关键代码:

// 在完成登录获取到用户信息后,开始从获取菜单
await store.dispatch('menu/getMenus')
// 生成动态路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加动态路由
router.addRoutes(accessRoutes)

改造完成,效果如下:

效果截图