Vue+Element UI+Spring Boot+MyBatis+MySQL实现动态多级菜单

Vue+Spring Boot+MySQL实现动态多级菜单

通常在后台管理系统中,需要根据每个用户不同的权限来动态展示菜单;本文主要记录通过Vue+Element UI+Spring Boot+MyBatis+MySQL实现一个动态多级菜单的功能

开发环境

名称版本号
JDK1.8.0_291
IntelliJ IDEA2021.3.2
WebStorm2021.3.2
DataGrip2021.3.4
MySQL8.0.25
Spring Boot2.6.4
mybatis2.2.2
Vue2.6.14
Element UI2.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
 */

@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>

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

效果图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值