> 文档中心 > Vue+Element UI+Spring Boot+MyBatis+MySQL实现动态多级菜单

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_menuname是联合唯一键,因为同一个菜单下不能有同名的子菜单,superior_menusort_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;}

因为此处有多个实体类都有一些共同的字段,如idcreateTimeupdateTime。所以这里将这几个共有的字段抽取到一个抽象类中,并在这个类中实现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>

代码解释:判断菜单是否还有子菜单,如果有就递归判断

效果图

在这里插入图片描述