前面章节我们实现的菜单功能都是展示的固定数据,跳转路由也是配置的固定路由,生产实践中非常不便,所以要实现菜单通过业务功能实现维护。菜单信息存储到数据库中,和传统应用差别不大,前端路由通过菜单信息预先生成动态路由,实现菜单跳转。
系统菜单展示后台实现
创建菜单表
CREATE TABLE `sys_menu` (
`id` BIGINT NOT NULL,
`parent_id` BIGINT DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` VARCHAR(50) DEFAULT NULL COMMENT '菜单名称',
`url` VARCHAR(200) DEFAULT NULL COMMENT '菜单URL',
`perms` VARCHAR(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔)',
`type` CHAR(6) DEFAULT NULL COMMENT '类型 [0:目录、1:菜单、2:按钮]',
`icon` VARCHAR(50) DEFAULT NULL COMMENT '菜单图标',
`order_num` INT DEFAULT NULL COMMENT '排序',
`status` CHAR(6) DEFAULT '0' DEFAULT NULL COMMENT '启用状态:[0:启用,1:停用]',
`create_by` BIGINT DEFAULT NULL COMMENT '创建人ID',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT DEFAULT NULL COMMENT '修改人ID',
`update_time` DATETIME DEFAULT NULL COMMENT '修改时间',
`deleted` INT DEFAULT '0' COMMENT '是否被删除(0:未删除;1:已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='菜单管理'
代码生成拷贝至项目工程。
自定义sql
xml文件种添加自定义sql。遍历菜单资源树,前端展现菜单树。
<select id="queryListByUserId" resultType="com.example.sys.vo.SysMenuVO">
SELECT *
FROM sys_menu u
WHERE status = #{status}
<if test="delField != null and delField != ''">
AND #{delField} = #{delVal}
</if>
order by ORDER_NUM
</select>
这里暂时查询全部菜单数据,后续添加权限,会很根据权限展示相应菜单。
delField、delVal为逻辑删除字段和字段值,Mapper接口使用mybaties-plus内置变量传入,最好不要手动在sql种手动编写容易出错。这里查询显示所有菜单只是单表查询,用mybaties-plus自由接口即可实现,完全不需要自定义sql。这里之所以使用自定义sql因为后续这个sql会比较复杂,会关联查询用户权限内的菜单。
Mapper接口
List<SysMenuVO> queryListByUserId(String status, String delField, String delVal);
service层接口
private GlobalConfig gc = GlobalConfigUtils.defaults();
@Override
public List<SysMenuVO> getCurrentUserMenus() {
List<SysMenuVO> list = sysMenuMapper.queryListByUserId( "0", gc.getDbConfig().getLogicDeleteField(), gc.getDbConfig().getLogicNotDeleteValue());
List<SysMenuVO> r_list = list.stream()
.filter(item -> item.getParentId() == 0L)
.collect(Collectors.toList());
r_list.forEach(item -> getChildMenu(item, list));
return r_list;
}
private void getChildMenu(SysMenuVO parentMenu, List<SysMenuVO> menuVOs) {
List<SysMenuVO> childList = menuVOs.stream().filter(item -> item.getParentId().equals(parentMenu.getId())).collect(Collectors.toList());
if (childList!=null&&childList.size()>0) {
parentMenu.setList(childList);
childList.forEach(item -> getChildMenu(item, menuVOs));
}
}
controller接口
@ApiOperation(value = "导航菜单")
@GetMapping("/myMenu")
public R<List<SysMenuVO>> myMenu() throws Exception {
List<SysMenuVO> menuList = sysMenuService.getCurrentUserMenus();
return R.ok().info(menuList);
}
使用swagger测试可以看到该接口
但是此时测试此接口会提示报错,后台报错提示找不到咱们的xml文件,所以我们需要配置xml路径。
POM文件配置
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
再次测试成功。
前端导航菜单实现
动态路由实现
路由加载前读取数据库中的菜单树信息,将菜单树信息保存到store中,并根据菜单信息初始化相关路由信息。
路由index.js
const router = new Router({
routes: routes.concat(mainRoutes)
})
router.beforeEach((to, from, next) => {
JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]')
if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, routers) === 'global') {
next()
} else {
http({
url: http.adornUrl("/sys/menu/myMenu"),
method: 'get'
}).then(({data}) => {
if (data && data.success == true) {
//取出动态路由传入
let routArr = JSON.parse(sessionStorage.getItem('dynamicRoutes')) || [];
routArr.forEach((item => {
let url = item.name.replace('-', '/');
item.component=_import(`${url}`) || null
}));
fnAddDynamicMenuRoutes(data.info, routArr);
router.options.isAddDynamicMenuRoutes = true
sessionStorage.setItem('menuList', JSON.stringify(data.info || '[]'))
next({ ...to, replace: true })
} else {
sessionStorage.setItem('menuList', '[]')
next()
}
}).catch((e) => {
console.log('路由异常',e)
Message.error(tips.error);
router.push({ name: 'login'},()=>{},()=>{})
})
}
})
/**
* 判断当前路由类型, global: 全局路由, main: 主入口路由
* @param {*} route 当前路由
*/
function fnCurrentRouteType (route, globalRoutes = []) {
var temp = []
for (var i = 0; i < globalRoutes.length; i++) {
if (route.path === globalRoutes[i].path) {
return 'global'
} else if (globalRoutes[i].children && globalRoutes[i].children.length >= 1) {
temp = temp.concat(globalRoutes[i].children)
}
}
return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main'
}
/**
* 添加动态(菜单)路由
* @param {*} menuList 菜单列表
* @param {*} routes 递归创建的动态(菜单)路由
*/
function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].list && menuList[i].list.length >= 1) {
temp = temp.concat(menuList[i].list)
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '')
var route = {
path: menuList[i].url.replace('/', '-'),
component: null,
name: menuList[i].url.replace('/', '-'),
meta: {
menuId: menuList[i].id,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
}
}
// url以http[s]://开头, 通过iframe展示
if (isURL(menuList[i].url)) {
route['path'] = `i-${menuList[i].id}`
route['name'] = `i-${menuList[i].id}`
route['meta']['iframeUrl'] = menuList[i].url
} else {
try {
route['component'] = _import(`${menuList[i].url}`) || null
} catch (e) {
}
}
routes.push(route)
}
}
if (temp.length >= 1) {
fnAddDynamicMenuRoutes(temp, routes)
} else {
mainRoutes.name = 'main-dynamic'
mainRoutes.children = routes
router.addRoutes([
mainRoutes,
{ path: '*', redirect: { name: '404' } }
])
sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '[]'))
}
}
export default router
菜单树展现
读取store中缓存的菜单信息展示导航菜单
子组件sub-menu
<template>
<el-submenu
v-if="menu.list && menu.list.length >= 1"
:index="menu.id + ''"
:popper-class="'site-sidebar--' + sidebarLayoutSkin + '-popper'">
<template slot="title">
<i :class="menu.icon"></i>
<span>{{ menu.name }}</span>
</template>
<sub-menu
v-for="item in menu.list"
:key="item.id"
:menu="item"
:dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
</el-submenu>
<el-menu-item v-else :index="menu.id + ''" @click="gotoRouteHandle(menu)">
<i :class="menu.icon"></i>
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
<script>
import SubMenu from './main-sidebar-sub-menu'
import { isURL } from '@/utils/validate'
export default {
name: 'sub-menu',
props: {
menu: {
type: Object,
required: true
},
dynamicMenuRoutes: {
type: Array,
required: true
}
},
components: {
SubMenu
},
computed: {
sidebarLayoutSkin: {
get () { return this.$store.state.common.sidebarLayoutSkin }
}
},
methods: {
// 通过menuId与动态(菜单)路由进行匹配跳转至指定路由
gotoRouteHandle (menu) {
if(menu.isJump==1&&isURL(menu.url)) {//跳出新窗口标识,并且以http、https的url
//跳出新窗口
window.open(menu.url)
}else{
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.id)
if (route.length >= 1) {
this.$router.push({name: route[0].name})
}
}
if (this.$store.state.common.clientType=='phone')this.$store.commit('common/updateSidebarFold', true);// 手机模式点击完菜单自动关闭
}
}
}
</script>
菜单树数据保存在$store.state.common.menuList 中,NavMenu导航菜单使用$store.state.common.menuList数据进行展示。
<sub-menu
v-show="showSidebar"
v-for="menu in menuList"
:key="menu.id"
:menu="menu"
:dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>
添加测试数据
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('1','0','系统管理',NULL,NULL,'0','el-icon-setting','0','0','1','2022-07-14 14:05:11',NULL,NULL,'0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('2','1','菜单管理','sys/menu-list',NULL,'1',NULL,'1','0','1','2022-07-14 14:05:16','1','2022-07-14 15:06:07','0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('3','1','用户管理','sys/user-list',NULL,'1',NULL,'0','0','1','2022-07-14 14:05:13','1','2022-07-14 15:06:01','0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('1547513127237103617','1','角色管理','sys/role-list',NULL,'1','','3','0','1','2022-07-14 17:27:36',NULL,NULL,'0');
效果展示