Vue+Element UI+Spring Boot+MyBatis+MySQL实现动态多级菜单
Vue+Spring Boot+MySQL实现动态多级菜单
通常在后台管理系统中,需要根据每个用户不同的权限来动态展示菜单;本文主要记录通过Vue
+Element UI
+Spring Boot
+MyBatis
+MySQL
实现一个动态多级菜单的功能
开发环境
名称 | 版本号 |
---|---|
JDK | 1.8.0_291 |
IntelliJ IDEA | 2021.3.2 |
WebStorm | 2021.3.2 |
DataGrip | 2021.3.4 |
MySQL | 8.0.25 |
Spring Boot | 2.6.4 |
mybatis | 2.2.2 |
Vue | 2.6.14 |
Element UI | 2.15.6 |
数据库
创建数据库表
CREATE TABLE `menu` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL COMMENT 'menu name', `icon` varchar(255) DEFAULT NULL COMMENT 'menu icon', `route` varchar(255) DEFAULT NULL COMMENT 'vue route url', `superior_menu` int NOT NULL DEFAULT 0 COMMENT 'superior menu', `sort_number` int NOT NULL COMMENT 'sort number', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'creation time of this record', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time of this record', PRIMARY KEY (`id`), UNIQUE KEY (`superior_menu`,`name`), UNIQUE KEY (`superior_menu`,`sort_number`))
*数据表解释
name
:菜单名称,icon
:菜单的图标,route
:menu-item的route属性,superior_menu
:上级菜单,sort_number
:排序号(用来指定菜单顺序的),id
为主键且自增长,superior_menu
和name
是联合唯一键,因为同一个菜单下不能有同名的子菜单,superior_menu
和sort_number
也是联合唯一键,因为同一个菜单下排序号不能重复*
插入测试数据
INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (1, '电商业务管理', 'el-icon-sell', null, 0, 3, '2022-04-01 14:34:42', '2022-04-04 10:23:52');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (2, '店铺管理', 'el-icon-s-shop', null, 1, 2, '2022-04-03 19:35:11', '2022-04-04 10:26:24');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (3, '添加店铺', null, '/management-center/add-store', 2, 1, '2022-04-03 19:35:11', '2022-04-04 09:07:08');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (4, '店铺列表', null, '/management-center/store-list', 2, 2, '2022-04-03 19:35:11', '2022-04-04 09:07:08');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (5, '动态分管理', 'el-icon-s-data', null, 1, 1, '2022-04-04 08:56:46', '2022-04-04 10:26:24');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (6, '录入动态分', null, '/management-center/enter-dsr', 5, 1, '2022-04-04 08:56:46', '2022-04-04 08:57:00');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (7, '更新动态分', null, '/management-center/update-dsr', 5, 2, '2022-04-04 08:56:46', '2022-04-04 08:57:00');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (8, '行政业务管理', 'el-icon-suitcase', null, 0, 1, '2022-04-04 08:58:24', '2022-04-04 10:23:52');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (9, '经费管理', 'el-icon-s-order', null, 8, 1, '2022-04-04 08:58:53', '2022-04-04 09:44:08');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (10, '录入经费支出数据', null, '/management-center/enter-expenditure', 9, 1, '2022-04-04 08:59:43', '2022-04-04 10:08:00');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (11, '组织机构管理', 'el-icon-user-solid', null, 8, 2, '2022-04-04 09:01:19', '2022-04-04 09:44:08');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (12, '组织机构图', null, '/management-center/organization-chart', 11, 1, '2022-04-04 09:03:31', '2022-04-04 09:28:18');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (13, '云仓业务管理', 'el-icon-house', null, 0, 2, '2022-04-04 09:36:37', '2022-04-04 10:23:52');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (14, '商家管理', 'el-icon-s-custom', null, 13, 1, '2022-04-04 09:41:52', '2022-04-04 09:42:26');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (15, '商家列表', null, '/management-center/merchant-list', 14, 1, '2022-04-04 09:42:26', '2022-04-04 10:08:27');INSERT INTO menu (id, name, icon, route, superior_menu, sort_number, create_time, update_time) VALUES (16, '添加商家', null, '/management-center/add-merchant', 14, 2, '2022-04-04 10:10:27', '2022-04-04 10:10:27');
JAVA后台
Menu实体类
package com.fenzhichuanmei.oa.security.pojo;import com.fenzhichuanmei.oa.common.pojo.BasePojo;import lombok.Data;import lombok.EqualsAndHashCode;import java.util.List;/** * @author Yi Dai 484201132@qq.com * @since 2022/3/31 18:59 */@Data@EqualsAndHashCode(callSuper = true)public class Menu extends BasePojo { private String name; private String icon; private String route; private Integer superiorMenu; private Integer sortNumber; private List<Menu> subMenus; private List<Permission> permissions;}
因为此处有多个实体类都有一些共同的字段,如id
,createTime
,updateTime
。所以这里将这几个共有的字段抽取到一个抽象类中,并在这个类中实现Serializable
接口,方便序列化等;这个类作为所有数据库表实体类的共同父类,减低代码冗余,代码如下:
package com.fenzhichuanmei.oa.common.pojo;import lombok.Data;import java.io.Serializable;import java.time.LocalDateTime;/** * @author Yi Dai 484201132@qq.com * @since 2022/4/1 10:29 */@Datapublic abstract class BasePojo implements Serializable { /** * the unique identification of this record in the database table */ protected Integer id; /** * creation time of this record */ protected LocalDateTime createTime; /** * update time of this record */ protected LocalDateTime updateTime;}
统一返回实体
package com.fenzhichuanmei.oa.common.response;import lombok.Getter;import lombok.ToString;/** * generic response body * * @author Yi Dai 484201132@qq.com * @since 2022-02-06 20:42 */@Getter@ToStringpublic class ResponseBody { /** * description request status */ private final Integer statusCode; /** * sub status code */ private Integer subStatusCode; /** * specific details */ private String message; /** * the data carried in response */ private Object data; public static ResponseBody build(Integer statusCode) { return new ResponseBody(statusCode); } public ResponseBody appendSubStatusCode(Integer subStatusCode) { this.subStatusCode = subStatusCode; return this; } public ResponseBody appendMessage(String message) { this.message = message; return this; } public ResponseBody appendData(Object data) { this.data = data; return this; } private ResponseBody(Integer statusCode) { this.statusCode = statusCode; }}
Mapper接口
package com.fenzhichuanmei.oa.security.mapper;import com.fenzhichuanmei.oa.security.pojo.Menu;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import java.util.List;/** * @author Yi Dai 484201132@qq.com * @since 2022/4/3 19:17 */@Mapperpublic interface MenuMapper { List<Menu> queryMenus(@Param("menu") Menu menu);}
XML映射文件
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.fenzhichuanmei.oa.security.mapper.MenuMapper"> <select id="queryMenus" parameterType="menu" resultType="menu"> select `id`, `name`, `icon`, `route`, `superior_menu`,`sort_number`, `create_time`, `update_time` from `menu` <where> <if test="menu!=null and menu.id!=null"> and `superior_menu`= #{menu.id} </if> <if test="menu==null or menu.superiorMenu==null"> and `superior_menu`= 0 </if> </where> </select></mapper>
主要实现代码
package com.fenzhichuanmei.oa.security.service.impl;import com.fenzhichuanmei.oa.common.response.ResponseBody;import com.fenzhichuanmei.oa.common.response.StatusCode;import com.fenzhichuanmei.oa.security.mapper.MenuMapper;import com.fenzhichuanmei.oa.security.pojo.Menu;import com.fenzhichuanmei.oa.security.service.MenuService;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.Comparator;import java.util.List;/** * @author Yi Dai 484201132@qq.com * @since 2022/4/3 19:14 */@Servicepublic class MenuServiceImpl implements MenuService { @Resource private MenuMapper menuMapper; @Override public ResponseBody queryMenus() { List<Menu> menus = menuMapper.queryMenus(null); menus.sort(Comparator.comparingInt(Menu::getSortNumber)); for (Menu menu : menus) { List<Menu> subMenus = querySubMenus(menu); menu.setSubMenus(subMenus); } return ResponseBody.build(StatusCode.SUCCESS).appendData(menus); } private List<Menu> querySubMenus(Menu menu) { List<Menu> subMenus = menuMapper.queryMenus(menu); if (subMenus.size() > 0) { subMenus.sort(Comparator.comparingInt(Menu::getSortNumber)); menu.setSubMenus(subMenus); for (Menu subMenu : subMenus) { List<Menu> menus = querySubMenus(subMenu); subMenu.setSubMenus(menus); } } return subMenus; }}
代码解释:先查询顶级菜单,然后递归查询其子菜单
前端
HomePage.vue
<template> <el-row> <el-col :span="24"> <el-row> <el-col> <el-menu class="top-menu" mode="horizontal"> <el-submenu index="1" class="submenu"><template slot="title">代毅</template><el-menu-item index="1-1">退出登录</el-menu-item><el-menu-item index="1-2">修改密码</el-menu-item> </el-submenu> </el-menu> </el-col> </el-row> <br> <el-row> <el-col :span="3"> <el-menu router class="side-menu"> <template v-for="subMenu of subMenus"><sub-menu v-if="subMenu.subMenus.length" :sub-menu="subMenu" :key="subMenu.id"></sub-menu><el-menu-item :index="getRandomIndex()"v-else-if="subMenu.route":route="subMenu.route":key="subMenu.id"> {{ subMenu.name }}</el-menu-item> </template> </el-menu> </el-col> <el-col :span="21"> <el-col :span="24"> <router-view></router-view> </el-col> </el-col> </el-row> </el-col> </el-row></template><script>import SubMenu from "@/components/SubMenu";import {nanoid} from "nanoid";import {queryMenus} from "@/network";export default { name: "HomePage", data() { return { subMenus: [] }; }, components: { SubMenu }, methods: { getRandomIndex() { return nanoid(); } }, async mounted() { this.subMenus = [...await queryMenus()]; }}</script><style scoped>.top-menu > .submenu { float: right;}.side-menu { height: 90vh; overflow: auto;}.top-menu, .side-menu { border-radius: 10px;}</style>
SubMenu.vue
<template> <el-submenu :index="getRandomIndex()"> <template v-if="subMenu.subMenus.length"> <template slot="title"> <i :class="subMenu.icon"></i> <span>{{ subMenu.name }}</span> </template> <template v-for="subMenu of subMenu.subMenus"> <sub-menu v-if="subMenu.subMenus.length" :sub-menu="subMenu" :key="subMenu.id"></sub-menu> <el-menu-item :index="getRandomIndex()" v-else-if="subMenu.route" :route="subMenu.route" :key="subMenu.id"> {{ subMenu.name }} </el-menu-item> </template> </template> </el-submenu></template><script>import {nanoid} from "nanoid";export default { name: "SubMenu", computed: {}, props: { /** * @param subMenu.subMenus */ subMenu: { type: Object, required: true } }, methods: { getRandomIndex() { return nanoid(); } }}</script>
代码解释:判断菜单是否还有子菜单,如果有就递归判断