一、后端采用springboot+mybatis-plus。改造自macrozheng/mall-tiny(https://github.com/macrozheng/mall-tiny,他这边是前端需要在router/index.js中配置路由信息,然后从数据库查询路由信息,进行匹配后,再显示出来,需要前后端匹配的。)
1、数据库表
菜单表对于的实体SysMenu:
package com.jc.pms.modules.sys.model;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import com.jc.pms.modules.sys.dto.SysMenuMeta;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
*
* 后台菜单表
*
*
* @author macro
* @since 2020-08-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_menu")
@ApiModel(value="SysMenu对象", description="后台菜单表")
public class SysMenu implements Serializable {
private static final long serialVersionUID=1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "父级ID")
private Long parentId;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "菜单名称")
private String title;
@ApiModelProperty(value = "菜单级数")
private Integer level;
@ApiModelProperty(value = "菜单排序")
private Integer sort;
@ApiModelProperty(value = "前端组件名称")
private String name;
@ApiModelProperty(value = "前端图标")
private String icon;
@ApiModelProperty(value = "前端隐藏")
private Integer hidden;
@ApiModelProperty(value = "访问路径")
private String path;
@ApiModelProperty(value = "组件路径")
private String component;
@ApiModelProperty(value = "菜单类型")
private Integer type;
@ApiModelProperty(value = "状态")
private Integer status;
//前端用以图标和面包屑
@TableField(exist = false)
private JSONObject meta;
public JSONObject getMeta() {
return Meta(this.getTitle(),this.getIcon());
}
//为meta构建JSONObejct对象值,前端用以图标和面包屑
public JSONObject Meta(String title, String icon){
SysMenuMeta menuMeta= new SysMenuMeta();
menuMeta.setIcon(icon);
menuMeta.setTitle(title);
return JSONUtil.parseObj(menuMeta, true, true);
}
}
上面用到的SysMenuMeta对象:
package com.jc.pms.modules.sys.dto;
import lombok.Data;
@Data
public class SysMenuMeta {
private String icon;
private String title;
}
********
下面用到的SysMenuNode子菜单对象:
package com.jc.pms.modules.sys.dto;
import com.jc.pms.modules.sys.model.SysMenu;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 后台菜单节点封装
* Created by macro on 2020/2/4.
*/
@Getter
@Setter
public class SysMenuNode extends SysMenu {
@ApiModelProperty(value = "子级菜单")
private List<SysMenuNode> children;
}
SysMenuServiceImpl中关于MenuTree获取的代码:
@Override
public List<SysMenuNode> treeList() {
List<SysMenu> menuList = list();//此处默认查所有的,可以改成根据用户id去查菜单,或者用户role去查询菜单,mapper里面配置自己写下
List<SysMenuNode> result = menuList.stream()
.filter(menu -> menu.getParentId().equals(0L))
.map(menu -> covertMenuNode(menu, menuList)).collect(Collectors.toList());
return result;
}
* 将UmsMenu转化为UmsMenuNode并设置children属性
*/
private SysMenuNode covertMenuNode(SysMenu menu, List<SysMenu> menuList) {
SysMenuNode node = new SysMenuNode();
BeanUtils.copyProperties(menu, node);
List<SysMenuNode> children = menuList.stream()
.filter(subMenu -> subMenu.getParentId().equals(menu.getId()))
.map(subMenu -> covertMenuNode(subMenu, menuList)).collect(Collectors.toList());
node.setChildren(children);
return node;
}
最终需要的menu数据json:
{
"code":200,"message":"操作成功","data":[{
"id":21,"parentId":0,"createTime":"2020-02-07T08:29:13.000+00:00","title":"系统管理","level":0,"sort":0,"name":"","icon":"ums","hidden":0,"path":"/ums","component":"Layout","type":1,"status":0,"meta":{
"icon":"ums","title":"系统管理"},"children":[{
"id":22,"parentId":21,"createTime":"2020-02-07T08:29:51.000+00:00","title":"用户列表","level":1,"sort":0,"name":"adminList","icon":"ums-admin","hidden":0,"path":"/ums/admin","component":"ums/admin/index","type":1,"status":0,"meta":{
"icon":"ums-admin","title":"用户列表"},"children":[]},{
"id":23,"parentId":21,"createTime":"2020-02-07T08:30:13.000+00:00","title":"角色列表","level":1,"sort":0,"name":"roleList","icon":"ums-role","hidden":0,"path":"/ums/role","component":"ums/role/index","type":1,"status":0,"meta":{
"icon":"ums-role","title":"角色列表"},"children":[]},{
"id":24,"parentId":21,"createTime":"2020-02-07T08:30:53.000+00:00","title":"菜单列表","level":1,"sort":0,"name":"menuList","icon":"ums-menu","hidden":0,"path":"/ums/menu","component":"ums/menu/index","type":1,"status":0,"meta":{
"icon":"ums-menu","title":"菜单列表"},"children":[]},{
"id":25,"parentId":21,"createTime":"2020-02-07T08:31:13.000+00:00","title":"资源列表","level":1,"sort":0,"name":"resourceList","icon":"ums-resource","hidden":0,"path":"/ums/resource","component":"ums/resource/index","type":1,"status":0,"meta":{
"icon":"ums-resource","title":"资源列表"},"children":[]}]}]}
二、前端是vue-element-template
1、跨域、api之类的配置省略。
2、store/modules/user.js增加一个存储菜单信息的menus字段和获取菜单json信息的函数。(获取用户信息跟菜单信息分开来)
// get router info
getAntRouter({
commit, state }) {
return new Promise((resolve, reject) => {
dongtRouter().then(response => {
const {
data } = response
if (!data) {
reject('验证失败,请重新登录')
}
const menus = data
console.log(menus)
menus.push({
path: '/404',
component: '404',
hidden: true
}, {
path: '*',
redirect: '/404',
hidden: true
})
commit('SET_MENUS', menus)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
3、src/permission.js 直接贴出来:
import router from './router'
import store from './store'
import {
Message } from 'element-ui'
import Layout from '@/layout'
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'] // no redirect whitelist
const _import = require('./router/_import_' + process.env.NODE_ENV) // 获取组件的方法
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()
} else {
// determine whether the user has obtained his permission roles through getInfo
// const hasRoles = store.getters.roles && store.getters.roles.length > 0
// if (hasRoles) {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
// // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
// const { roles } = await store.dispatch('user/getInfo')
//
// // generate accessible routes map based on roles
// const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
//
// // dynamically add accessible routes
// router.addRoutes(accessRoutes)
await store.dispatch('user/getInfo') // 请求获取用户信息
await store.dispatch('user/getAntRouter') // 请求获取路由信息
if (store.getters.menus.length < 1) {
global.antRouter = []
next()
}
const menus = filterAsyncRouter(store.getters.menus) // 1.过滤路由
console.log(menus)
router.addRoutes(menus) // 2.动态添加路由
global.antRouter = menus // 3.将路由数据传递给全局变量,做侧边栏菜单渲染工作
next({
...to,
replace: true
})
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
next({
...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error({
message: error || 'Has Error' })
next(`/login?redirect=${
to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${
to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
// // 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap) {
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') {
route.component = Layout
} else {
route.component = _import(route.component) // 导入组件
}
}
if (route.hidden) {
// eslint-disable-next-line eqeqeq
if (route.hidden == '1') {
route.hidden = false
// eslint-disable-next-line eqeqeq
} else if (route.hidden == '0') {
route.hidden = true
}
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
return accessedRouters
}
4、layout/components/sidebar/index.vue略改。
<template>
<div :class="{
'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<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>
div>
template>
<script>
import {
mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: {
SidebarItem, Logo },
computed: {
...mapGetters([
// 'permission_routes',
'sidebar'
]),
routes() {
// return this.$router.options.routes
return this.$router.options.routes.concat(global.antRouter) // 把路由concat进去
},
activeMenu() {
const route = this.$route
const {
meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
script>
ok了。后端菜单信息动态获取,生成前端菜单路由。不需要再在前端配置权限菜单的路由信息了。注意不要忘记要把菜单对应的文件建起来。