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
*/
@Data
public 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
@ToString
public 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
*/
@Mapper
public 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
*/
@Service
public 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>
代码解释:判断菜单是否还有子菜单,如果有就递归判断