本文介绍无限级菜单的实现,跟无限极评论相似。通过递归将菜单与子菜单进行封装。
前台级菜单动图
后台菜单
下面讲代码实现
本文案例采用 MyBatis Plus 作为 ORM 框架。
Menu.java
这里面的几个注解都是 MyBatis Plus 的注解,大家可以根据自己的需要来去留。childMenus 不是数据库字段,用于存该菜单下的子菜单列表。这里讲一下几个重要的字段
menuPid:菜单父节点id,如果是一级菜单,默认为0;如果是二级菜单,则为一级菜单id
menuSort:菜单排序编号,并非数据库中排序使用。主要是用于同一等级的菜单之间的排序,比如”Java基础“和”Java进阶“哪个放前面,可以根据这个字段来排序。数字越大,越靠前。
menuTarget:是在当前页面打开,还是新页面打开
menuType:菜单类型,根据菜单显示的位置,分为前台主要菜单0,前台顶部菜单1(以后可能也会加上后台菜单2)
menuLevel:辅助字段,记录该菜单是第几级,用于在后台显示的时候加上对应的——
MenuMapper.java
MenuMapper.xml
MenuService.java
MenuServiceImpl.java
这里的 @Cacheable 等注解是用于缓存,大家根据自己的需求选择去留
MenuUtil.java
以上就可以从service层获得对应的封装类型的数据了
前台的菜单树结构应该是这样的
后台的菜单列表结构如下
到这里基本就实现了。
下面简单讲一下前台菜单的视图层怎么写
通常情况下,我们只需要展示两级菜单就差不多了,有时候也需要三级。
通过三层for循环展示,方法虽然蠢但是可以实现。
如果用for循环可能实现不了
commonTag是自定义的 FreeMarker 标签,macro 是 FreeMarker 标签
一、效果预览
前台级菜单动图
后台菜单
下面讲代码实现
二、后端代码实现
本文案例采用 MyBatis Plus 作为 ORM 框架。
1.实体
Menu.java
- package com.liuyanzhao.sens.entity;
- import com.baomidou.mybatisplus.annotations.TableField;
- import com.baomidou.mybatisplus.annotations.TableId;
- import com.baomidou.mybatisplus.annotations.TableName;
- import com.baomidou.mybatisplus.enums.IdType;
- import lombok.Data;
- import java.io.Serializable;
- import java.util.List;
- /**
- * <pre>
- * 菜单
- * </pre>
- *
- * @author : saysky
- * @date : 2018/1/24
- */
- @Data
- @TableName("sens_menu")
- public class Menu implements Serializable {
- private static final long serialVersionUID = -7726233157376388786L;
- /**
- * 编号 自增
- */
- @TableId(type = IdType.AUTO)
- private Long menuId;
- /**
- * 菜单Pid
- */
- private Long menuPid = 0L;
- /**
- * 菜单名称
- */
- private String menuName;
- /**
- * 菜单路径
- */
- private String menuUrl;
- /**
- * 排序编号
- */
- private Integer menuSort = 1;
- /**
- * 图标,可选
- */
- private String menuIcon;
- /**
- * 打开方式
- */
- private String menuTarget;
- /**
- * 菜单类型(0前台主要菜单,1前台顶部菜单)
- */
- private Integer menuType;
- /**
- * 菜单层级
- */
- private Integer menuLevel = 1;
- /**
- * 子菜单列表
- */
- @TableField(exist = false)
- private List<Menu> childMenus;
- }
这里面的几个注解都是 MyBatis Plus 的注解,大家可以根据自己的需要来去留。childMenus 不是数据库字段,用于存该菜单下的子菜单列表。这里讲一下几个重要的字段
menuPid:菜单父节点id,如果是一级菜单,默认为0;如果是二级菜单,则为一级菜单id
menuSort:菜单排序编号,并非数据库中排序使用。主要是用于同一等级的菜单之间的排序,比如”Java基础“和”Java进阶“哪个放前面,可以根据这个字段来排序。数字越大,越靠前。
menuTarget:是在当前页面打开,还是新页面打开
menuType:菜单类型,根据菜单显示的位置,分为前台主要菜单0,前台顶部菜单1(以后可能也会加上后台菜单2)
menuLevel:辅助字段,记录该菜单是第几级,用于在后台显示的时候加上对应的——
2. Mapper 类 和 xml
MenuMapper.java
- package com.liuyanzhao.sens.mapper;
- import com.baomidou.mybatisplus.mapper.BaseMapper;
- import com.liuyanzhao.sens.entity.Menu;
- import org.apache.ibatis.annotations.Mapper;
- import java.util.List;
- /**
- * @author liuyanzhao
- */
- @Mapper
- public interface MenuMapper extends BaseMapper<Menu> {
- /**
- * 根据类型查询
- * @param menuType 菜单类型
- * @return List
- */
- List<Menu> findByMenuType(Integer menuType);
- /**
- * 根据菜单Pid获得菜单
- *
- * @param menuId 菜单ID
- * @return List
- */
- List<Menu> findByMenuPid(Long menuId);
- }
MenuMapper.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.liuyanzhao.sens.mapper.MenuMapper">
- <resultMap id="BaseResultMap" type="com.liuyanzhao.sens.entity.Menu">
- <id column="menu_id" jdbcType="INTEGER" property="menuId"/>
- <result column="menu_pid" jdbcType="INTEGER" property="menuPid"/>
- <result column="menu_name" jdbcType="VARCHAR" property="menuName"/>
- <result column="menu_url" jdbcType="VARCHAR" property="menuUrl"/>
- <result column="menu_sort" jdbcType="INTEGER" property="menuSort"/>
- <result column="menu_icon" jdbcType="VARCHAR" property="menuIcon"/>
- <result column="menu_target" jdbcType="VARCHAR" property="menuTarget"/>
- <result column="menu_type" jdbcType="INTEGER" property="menuType"/>
- <result column="menu_level" jdbcType="INTEGER" property="menuLevel"/>
- </resultMap>
- <sql id="all_columns">
- menu_id, menu_pid, menu_name, menu_url, menu_sort,
- menu_icon, menu_target, menu_type, menu_level
- </sql>
- <sql id="tb">`sens_menu`</sql>
- <sql id="all_values">
- #{menuId}, #{menuPid}, #{menuName}, #{menuUrl}, #{menuSort},
- #{menuIcon}, #{menuTarget}, #{menuType}, #{menuLevel}
- </sql>
- <select id="findByMenuType" resultMap="BaseResultMap">
- SELECT
- <include refid="all_columns"/>
- FROM
- <include refid="tb"/>
- WHERE menu_type = #{value} ORDER BY menu_sort DESC
- </select>
- <select id="findByMenuPid" resultType="com.liuyanzhao.sens.entity.Menu">
- SELECT
- <include refid="all_columns"/>
- FROM
- <include refid="tb"/>
- WHERE menu_pid = #{value} ORDER BY menu_sort DESC
- </select>
- </mapper>
3.service 层
MenuService.java
- package com.liuyanzhao.sens.service;
- import com.liuyanzhao.sens.entity.Menu;
- import java.util.List;
- /**
- * <pre>
- * 菜单业务逻辑接口
- * </pre>
- *
- * @author : saysky
- * @date : 2018/1/24
- */
- public interface MenuService {
- /**
- * 根据菜单Pid获得菜单
- *
- * @return List
- */
- List<Menu> findByMenuPid(Long menuId);
- /**
- * 根据编号查询菜单
- *
- * @param menuId menuId
- * @return Optional
- */
- Menu findByMenuId(Long menuId);
- /**
- * 根据类型查询,以树形展示,用于前台
- *
- * @param menuType 菜单类型
- * @return List
- */
- List<Menu> findMenuTree(Integer menuType);
- /**
- * 根据类型查询,以列表显示展示,用于后台管理
- *
- * @param menuType 菜单类型
- * @return 菜单
- */
- List<Menu> findMenuList(Integer menuType);
- /**
- * 新增/修改菜单
- *
- * @param menu menu
- * @return Menu
- */
- Menu saveByMenu(Menu menu);
- /**
- * 删除菜单
- *
- * @param menuId menuId
- * @return Menu
- */
- void removeByMenuId(Long menuId);
- }
MenuServiceImpl.java
- package com.liuyanzhao.sens.service.impl;
- import com.liuyanzhao.sens.entity.Menu;
- import com.liuyanzhao.sens.mapper.MenuMapper;
- import com.liuyanzhao.sens.model.enums.MenuTypeEnum;
- import com.liuyanzhao.sens.service.MenuService;
- import com.liuyanzhao.sens.utils.MenuUtil;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cache.annotation.CacheEvict;
- import org.springframework.cache.annotation.Cacheable;
- import org.springframework.stereotype.Service;
- import java.util.List;
- /**
- * <pre>
- * 菜单业务逻辑实现类
- * </pre>
- *
- * @author : saysky
- * @date : 2018/1/24
- */
- @Service
- public class MenuServiceImpl implements MenuService {
- private static final String MENUS_CACHE_NAME = "menus";
- @Autowired(required = false)
- private MenuMapper menuMapper;
- @Override
- @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_id_'+#menuId")
- public Menu findByMenuId(Long menuId) {
- return menuMapper.selectById(menuId);
- }
- @Override
- @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_pid_'+#menuId")
- public List<Menu> findByMenuPid(Long menuId) {
- return menuMapper.findByMenuPid(menuId);
- }
- @Override
- @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_tree_type_'+#menuType")
- public List<Menu> findMenuTree(Integer menuType) {
- List<Menu> menuList = menuMapper.findByMenuType(menuType);
- //以层级(树)关系显示
- return MenuUtil.getMenuTree(menuList);
- }
- @Override
- @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_list_type_'+#menuType")
- public List<Menu> findMenuList(Integer menuType) {
- List<Menu> menuList = menuMapper.findByMenuType(menuType);
- menuList.forEach(menu -> {
- String str = "";
- for (int i = 1; i < menu.getMenuLevel(); i++) {
- str += "——";
- }
- menu.setMenuName(str + menu.getMenuName());
- });
- //以一级菜单/二级菜单/三级菜单的顺序输出
- return MenuUtil.getMenuList(menuList);
- }
- @Override
- @CacheEvict(value = MENUS_CACHE_NAME, allEntries = true, beforeInvocation = true)
- public void removeByMenuId(Long menuId) {
- menuMapper.deleteById(menuId);
- }
- @Override
- @CacheEvict(value = MENUS_CACHE_NAME, allEntries = true, beforeInvocation = true)
- public Menu saveByMenu(Menu menu) {
- //1.设置MenuLevel
- if (menu.getMenuPid() == 0 || menu.getMenuPid() == null) {
- menu.setMenuLevel(1);
- } else {
- Menu parentMenu = this.findByMenuId(menu.getMenuPid());
- if (parentMenu != null && parentMenu.getMenuLevel() != null) {
- menu.setMenuLevel(parentMenu.getMenuLevel() + 1);
- }
- }
- //2.添加/更新菜单
- if (menu != null && menu.getMenuId() != null) {
- menuMapper.updateById(menu);
- } else {
- menuMapper.insert(menu);
- }
- return menu;
- }
- }
这里的 @Cacheable 等注解是用于缓存,大家根据自己的需求选择去留
4.菜单工具类
MenuUtil.java
- package com.liuyanzhao.sens.utils;
- import com.liuyanzhao.sens.entity.Menu;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- /**
- * <pre>
- * 拼装菜单,
- * </pre>
- *
- * @author : saysky
- * @date : 2018/7/12
- */
- public class MenuUtil {
- /**
- * 获取组装好的菜单
- * 以树的形式显示
- *
- * @param menusRoot menusRoot
- * @return List
- */
- public static List<Menu> getMenuTree(List<Menu> menusRoot) {
- List<Menu> menusResult = new ArrayList<>();
- for (Menu menu : menusRoot) {
- if (menu.getMenuPid() == 0) {
- menusResult.add(menu);
- }
- }
- for (Menu menu : menusResult) {
- menu.setChildMenus(getChildTree(menu.getMenuId(), menusRoot));
- }
- return menusResult;
- }
- /**
- * 获取菜单的子菜单
- *
- * @param id 菜单编号
- * @param menusRoot menusRoot
- * @return List
- */
- private static List<Menu> getChildTree(Long id, List<Menu> menusRoot) {
- List<Menu> menusChild = new ArrayList<>();
- for (Menu menu : menusRoot) {
- if (menu.getMenuPid() != 0) {
- if (menu.getMenuPid().equals(id)) {
- menusChild.add(menu);
- }
- }
- }
- for (Menu menu : menusChild) {
- if (menu.getMenuPid() != 0) {
- menu.setChildMenus(getChildTree(menu.getMenuId(), menusRoot));
- }
- }
- if (menusChild.size() == 0) {
- return null;
- }
- return menusChild;
- }
- /**
- * 获取组装好的菜单,
- *
- * @param menusRoot menusRoot
- * @return List
- */
- public static List<Menu> getMenuList(List<Menu> menusRoot) {
- List<Menu> menusResult = new ArrayList<>();
- for (Menu menu : menusRoot) {
- if (menu.getMenuPid() == 0) {
- menusResult.add(menu);
- menusResult.addAll(getChildList(menu.getMenuId(), menusRoot));
- }
- }
- return menusResult;
- }
- /**
- * 获取菜单的子菜单
- *
- * @param id 菜单编号
- * @param menusRoot menusRoot
- * @return List
- */
- private static List<Menu> getChildList(Long id, List<Menu> menusRoot) {
- List<Menu> menusChild = new ArrayList<>();
- for (Menu menu : menusRoot) {
- if (menu.getMenuPid() != 0) {
- if (menu.getMenuPid().equals(id)) {
- menusChild.add(menu);
- List<Menu> tempList = getChildList(menu.getMenuId(), menusRoot);
- tempList.sort((a, b) -> b.getMenuSort() - a.getMenuSort());
- menusChild.addAll(tempList);
- }
- }
- }
- if (menusChild.size() == 0) {
- return Collections.emptyList();
- }
- return menusChild;
- }
- }
以上就可以从service层获得对应的封装类型的数据了
前台的菜单树结构应该是这样的
后台的菜单列表结构如下
到这里基本就实现了。
下面简单讲一下前台菜单的视图层怎么写
三、FreeMarker渲染前台菜单
1.最多展示三级
通常情况下,我们只需要展示两级菜单就差不多了,有时候也需要三级。
- <ul id="menu-mainmenu" class="down-menu nav-menu">
- <#list frontMainMenus as menu>
- <li id="menu-item-${menu.menuId}"
- class="menu-item menu-item-type-custom menu-item-object-custom">
- <a href="${(menu.menuUrl)!}">
- <i class="${(menu.menuIcon)!}"></i>
- <span class="font-text">${(menu.menuName)!}</span>
- </a>
- <ul class="sub-menu">
- <#list menu.childMenus as menu2>
- <li id="menu-item-${menu2.menuId}"
- class="menu-item menu-item-type-custom menu-item-object-custom">
- <a href="${(menu2.menuUrl)!}">
- <i class="${(menu2.menuIcon)!}"></i>
- <span class="font-text">${(menu2.menuName)!}</span>
- </a>
- <ul class="sub-menu">
- <#list menu2.childMenus?if_exists as menu3>
- <li id="menu-item-${menu3.menuId}"
- class="menu-item menu-item-type-custom menu-item-object-custom">
- <a href="${(menu2.menuIcon)!}"">
- <span class="font-text">${(menu3.menuName)!}</span>
- </a>
- </li>
- </#list>
- </ul>
- </li>
- </#list>
- </ul>
- </li>
- </#list>
- </ul>
通过三层for循环展示,方法虽然蠢但是可以实现。
2.展示无限级菜单
如果用for循环可能实现不了
- <#macro childMenus menus>
- <ul class="sub-menu">
- <#list menus as menu>
- <li id="menu-item-${menu.menuId}"
- class="menu-item menu-item-type-custom menu-item-object-custom">
- <a href="${(menu.menuIcon)!}"">
- <span class="font-text">${(menu.menuName)!}</span>
- </a>
- <#if menu.childMenus?? && menu.childMenus?size gt 0>
- <@childMenus menu.childMenus></@childMenus>
- </#if>
- </li>
- </#list>
- </ul>
- </#macro>
- <ul id="menu-mainmenu" class="down-menu nav-menu">
- <@commonTag method="menus">
- <#list frontMainMenus as menu>
- <li id="menu-item-${menu.menuId}"
- class="menu-item menu-item-type-custom menu-item-object-custom">
- <a href="${(menu.menuUrl)!}">
- <i class="${(menu.menuIcon)!}"></i>
- <span class="font-text">${(menu.menuName)!}</span>
- </a>
- <#if menu.childMenus?? && menu.childMenus?size gt 0>
- <@childMenus menu.childMenus></@childMenus>
- </#if>
- </li>
- </#list>
- </@commonTag>
- </ul>
commonTag是自定义的 FreeMarker 标签,macro 是 FreeMarker 标签
2020年11月18日 22:00:51
frontMainMenus 是哪里来的?没找到
2020年11月23日 09:57:00
从后台传的,可以通过FreeMarker全局标签或者Controller传