SpringBoot+FreeMarker实现无限级菜单

本文介绍无限级菜单的实现,跟无限极评论相似。通过递归将菜单与子菜单进行封装。

一、效果预览

前台级菜单动图

SpringBoot+FreeMarker实现无限级菜单

 

后台菜单

SpringBoot+FreeMarker实现无限级菜单

 

 

下面讲代码实现

二、后端代码实现

本文案例采用 MyBatis Plus 作为 ORM 框架。

1.实体

Menu.java

  1. package com.liuyanzhao.sens.entity;
  2. import com.baomidou.mybatisplus.annotations.TableField;
  3. import com.baomidou.mybatisplus.annotations.TableId;
  4. import com.baomidou.mybatisplus.annotations.TableName;
  5. import com.baomidou.mybatisplus.enums.IdType;
  6. import lombok.Data;
  7. import java.io.Serializable;
  8. import java.util.List;
  9. /**
  10.  * <pre>
  11.  *     菜单
  12.  * </pre>
  13.  *
  14.  * @author : saysky
  15.  * @date : 2018/1/24
  16.  */
  17. @Data
  18. @TableName("sens_menu")
  19. public class Menu implements Serializable {
  20.     private static final long serialVersionUID = -7726233157376388786L;
  21.     /**
  22.      * 编号 自增
  23.      */
  24.     @TableId(type = IdType.AUTO)
  25.     private Long menuId;
  26.     /**
  27.      * 菜单Pid
  28.      */
  29.     private Long menuPid = 0L;
  30.     /**
  31.      * 菜单名称
  32.      */
  33.     private String menuName;
  34.     /**
  35.      * 菜单路径
  36.      */
  37.     private String menuUrl;
  38.     /**
  39.      * 排序编号
  40.      */
  41.     private Integer menuSort = 1;
  42.     /**
  43.      * 图标,可选
  44.      */
  45.     private String menuIcon;
  46.     /**
  47.      * 打开方式
  48.      */
  49.     private String menuTarget;
  50.     /**
  51.      * 菜单类型(0前台主要菜单,1前台顶部菜单)
  52.      */
  53.     private Integer menuType;
  54.     /**
  55.      * 菜单层级
  56.      */
  57.     private Integer menuLevel = 1;
  58.     /**
  59.      * 子菜单列表
  60.      */
  61.     @TableField(exist = false)
  62.     private List<Menu> childMenus;
  63. }

这里面的几个注解都是 MyBatis Plus 的注解,大家可以根据自己的需要来去留。childMenus 不是数据库字段,用于存该菜单下的子菜单列表。这里讲一下几个重要的字段

menuPid:菜单父节点id,如果是一级菜单,默认为0;如果是二级菜单,则为一级菜单id

menuSort:菜单排序编号,并非数据库中排序使用。主要是用于同一等级的菜单之间的排序,比如”Java基础“和”Java进阶“哪个放前面,可以根据这个字段来排序。数字越大,越靠前。

menuTarget:是在当前页面打开,还是新页面打开

menuType:菜单类型,根据菜单显示的位置,分为前台主要菜单0,前台顶部菜单1(以后可能也会加上后台菜单2)

menuLevel:辅助字段,记录该菜单是第几级,用于在后台显示的时候加上对应的——

 

2. Mapper 类 和 xml

MenuMapper.java

  1. package com.liuyanzhao.sens.mapper;
  2. import com.baomidou.mybatisplus.mapper.BaseMapper;
  3. import com.liuyanzhao.sens.entity.Menu;
  4. import org.apache.ibatis.annotations.Mapper;
  5. import java.util.List;
  6. /**
  7.  * @author liuyanzhao
  8.  */
  9. @Mapper
  10. public interface MenuMapper extends BaseMapper<Menu> {
  11.     /**
  12.      * 根据类型查询
  13.      * @param menuType 菜单类型
  14.      * @return List
  15.      */
  16.     List<Menu> findByMenuType(Integer menuType);
  17.     /**
  18.      * 根据菜单Pid获得菜单
  19.      *
  20.      * @param menuId 菜单ID
  21.      * @return List
  22.      */
  23.     List<Menu> findByMenuPid(Long menuId);
  24. }

 

MenuMapper.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.liuyanzhao.sens.mapper.MenuMapper">
  4.   <resultMap id="BaseResultMap" type="com.liuyanzhao.sens.entity.Menu">
  5.     <id column="menu_id" jdbcType="INTEGER" property="menuId"/>
  6.     <result column="menu_pid" jdbcType="INTEGER" property="menuPid"/>
  7.     <result column="menu_name" jdbcType="VARCHAR" property="menuName"/>
  8.     <result column="menu_url" jdbcType="VARCHAR" property="menuUrl"/>
  9.     <result column="menu_sort" jdbcType="INTEGER" property="menuSort"/>
  10.     <result column="menu_icon" jdbcType="VARCHAR" property="menuIcon"/>
  11.     <result column="menu_target" jdbcType="VARCHAR" property="menuTarget"/>
  12.     <result column="menu_type" jdbcType="INTEGER" property="menuType"/>
  13.     <result column="menu_level" jdbcType="INTEGER" property="menuLevel"/>
  14.   </resultMap>
  15.   <sql id="all_columns">
  16.    menu_id, menu_pid, menu_name, menu_url, menu_sort,
  17.    menu_icon, menu_target, menu_type, menu_level
  18.     </sql>
  19.   <sql id="tb">`sens_menu`</sql>
  20.   <sql id="all_values">
  21.         #{menuId}, #{menuPid}, #{menuName}, #{menuUrl}, #{menuSort},
  22.         #{menuIcon}, #{menuTarget}, #{menuType}, #{menuLevel}
  23.     </sql>
  24.   <select id="findByMenuType" resultMap="BaseResultMap">
  25.     SELECT
  26.     <include refid="all_columns"/>
  27.     FROM
  28.     <include refid="tb"/>
  29.     WHERE menu_type = #{value} ORDER BY menu_sort DESC
  30.   </select>
  31.   <select id="findByMenuPid" resultType="com.liuyanzhao.sens.entity.Menu">
  32.     SELECT
  33.     <include refid="all_columns"/>
  34.     FROM
  35.     <include refid="tb"/>
  36.     WHERE menu_pid = #{value} ORDER BY menu_sort DESC
  37.   </select>
  38. </mapper>

 

3.service 层

MenuService.java

  1. package com.liuyanzhao.sens.service;
  2. import com.liuyanzhao.sens.entity.Menu;
  3. import java.util.List;
  4. /**
  5.  * <pre>
  6.  *     菜单业务逻辑接口
  7.  * </pre>
  8.  *
  9.  * @author : saysky
  10.  * @date : 2018/1/24
  11.  */
  12. public interface MenuService {
  13.     /**
  14.      * 根据菜单Pid获得菜单
  15.      *
  16.      * @return List
  17.      */
  18.     List<Menu> findByMenuPid(Long menuId);
  19.     /**
  20.      * 根据编号查询菜单
  21.      *
  22.      * @param menuId menuId
  23.      * @return Optional
  24.      */
  25.     Menu findByMenuId(Long menuId);
  26.     /**
  27.      * 根据类型查询,以树形展示,用于前台
  28.      * 
  29.      * @param menuType 菜单类型
  30.      * @return List
  31.      */
  32.     List<Menu> findMenuTree(Integer menuType);
  33.     /**
  34.      * 根据类型查询,以列表显示展示,用于后台管理
  35.      *
  36.      * @param menuType 菜单类型
  37.      * @return 菜单
  38.      */
  39.     List<Menu> findMenuList(Integer menuType);
  40.     /**
  41.      * 新增/修改菜单
  42.      *
  43.      * @param menu menu
  44.      * @return Menu
  45.      */
  46.     Menu saveByMenu(Menu menu);
  47.     /**
  48.      * 删除菜单
  49.      *
  50.      * @param menuId menuId
  51.      * @return Menu
  52.      */
  53.     void removeByMenuId(Long menuId);
  54. }

 

MenuServiceImpl.java

  1. package com.liuyanzhao.sens.service.impl;
  2. import com.liuyanzhao.sens.entity.Menu;
  3. import com.liuyanzhao.sens.mapper.MenuMapper;
  4. import com.liuyanzhao.sens.model.enums.MenuTypeEnum;
  5. import com.liuyanzhao.sens.service.MenuService;
  6. import com.liuyanzhao.sens.utils.MenuUtil;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.cache.annotation.CacheEvict;
  9. import org.springframework.cache.annotation.Cacheable;
  10. import org.springframework.stereotype.Service;
  11. import java.util.List;
  12. /**
  13.  * <pre>
  14.  *     菜单业务逻辑实现类
  15.  * </pre>
  16.  *
  17.  * @author : saysky
  18.  * @date : 2018/1/24
  19.  */
  20. @Service
  21. public class MenuServiceImpl implements MenuService {
  22.     private static final String MENUS_CACHE_NAME = "menus";
  23.     @Autowired(required = false)
  24.     private MenuMapper menuMapper;
  25.     @Override
  26.     @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_id_'+#menuId")
  27.     public Menu findByMenuId(Long menuId) {
  28.         return menuMapper.selectById(menuId);
  29.     }
  30.     @Override
  31.     @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_pid_'+#menuId")
  32.     public List<Menu> findByMenuPid(Long menuId) {
  33.         return menuMapper.findByMenuPid(menuId);
  34.     }
  35.     @Override
  36.     @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_tree_type_'+#menuType")
  37.     public List<Menu> findMenuTree(Integer menuType) {
  38.         List<Menu> menuList = menuMapper.findByMenuType(menuType);
  39.         //以层级(树)关系显示
  40.         return MenuUtil.getMenuTree(menuList);
  41.     }
  42.     @Override
  43.     @Cacheable(value = MENUS_CACHE_NAME, key = "'menus_list_type_'+#menuType")
  44.     public List<Menu> findMenuList(Integer menuType) {
  45.         List<Menu> menuList = menuMapper.findByMenuType(menuType);
  46.         menuList.forEach(menu -> {
  47.             String str = "";
  48.             for (int i = 1; i < menu.getMenuLevel(); i++) {
  49.                 str += "——";
  50.             }
  51.             menu.setMenuName(str + menu.getMenuName());
  52.         });
  53.         //以一级菜单/二级菜单/三级菜单的顺序输出
  54.         return MenuUtil.getMenuList(menuList);
  55.     }
  56.     @Override
  57.     @CacheEvict(value = MENUS_CACHE_NAME, allEntries = true, beforeInvocation = true)
  58.     public void removeByMenuId(Long menuId) {
  59.         menuMapper.deleteById(menuId);
  60.     }
  61.     @Override
  62.     @CacheEvict(value = MENUS_CACHE_NAME, allEntries = true, beforeInvocation = true)
  63.     public Menu saveByMenu(Menu menu) {
  64.         //1.设置MenuLevel
  65.         if (menu.getMenuPid() == 0 || menu.getMenuPid() == null) {
  66.             menu.setMenuLevel(1);
  67.         } else {
  68.             Menu parentMenu = this.findByMenuId(menu.getMenuPid());
  69.             if (parentMenu != null && parentMenu.getMenuLevel() != null) {
  70.                 menu.setMenuLevel(parentMenu.getMenuLevel() + 1);
  71.             }
  72.         }
  73.         //2.添加/更新菜单
  74.         if (menu != null && menu.getMenuId() != null) {
  75.             menuMapper.updateById(menu);
  76.         } else {
  77.             menuMapper.insert(menu);
  78.         }
  79.         return menu;
  80.     }
  81. }

这里的 @Cacheable 等注解是用于缓存,大家根据自己的需求选择去留

 

4.菜单工具类

MenuUtil.java

  1. package com.liuyanzhao.sens.utils;
  2. import com.liuyanzhao.sens.entity.Menu;
  3. import java.util.ArrayList;
  4. import java.util.Collections;
  5. import java.util.List;
  6. /**
  7.  * <pre>
  8.  *     拼装菜单,
  9.  * </pre>
  10.  *
  11.  * @author : saysky
  12.  * @date : 2018/7/12
  13.  */
  14. public class MenuUtil {
  15.     /**
  16.      * 获取组装好的菜单
  17.      * 以树的形式显示
  18.      *
  19.      * @param menusRoot menusRoot
  20.      * @return List
  21.      */
  22.     public static List<Menu> getMenuTree(List<Menu> menusRoot) {
  23.         List<Menu> menusResult = new ArrayList<>();
  24.         for (Menu menu : menusRoot) {
  25.             if (menu.getMenuPid() == 0) {
  26.                 menusResult.add(menu);
  27.             }
  28.         }
  29.         for (Menu menu : menusResult) {
  30.             menu.setChildMenus(getChildTree(menu.getMenuId(), menusRoot));
  31.         }
  32.         return menusResult;
  33.     }
  34.     /**
  35.      * 获取菜单的子菜单
  36.      *
  37.      * @param id        菜单编号
  38.      * @param menusRoot menusRoot
  39.      * @return List
  40.      */
  41.     private static List<Menu> getChildTree(Long id, List<Menu> menusRoot) {
  42.         List<Menu> menusChild = new ArrayList<>();
  43.         for (Menu menu : menusRoot) {
  44.             if (menu.getMenuPid() != 0) {
  45.                 if (menu.getMenuPid().equals(id)) {
  46.                     menusChild.add(menu);
  47.                 }
  48.             }
  49.         }
  50.         for (Menu menu : menusChild) {
  51.             if (menu.getMenuPid() != 0) {
  52.                 menu.setChildMenus(getChildTree(menu.getMenuId(), menusRoot));
  53.             }
  54.         }
  55.         if (menusChild.size() == 0) {
  56.             return null;
  57.         }
  58.         return menusChild;
  59.     }
  60.     /**
  61.      * 获取组装好的菜单,
  62.      *
  63.      * @param menusRoot menusRoot
  64.      * @return List
  65.      */
  66.     public static List<Menu> getMenuList(List<Menu> menusRoot) {
  67.         List<Menu> menusResult = new ArrayList<>();
  68.         for (Menu menu : menusRoot) {
  69.             if (menu.getMenuPid() == 0) {
  70.                 menusResult.add(menu);
  71.                 menusResult.addAll(getChildList(menu.getMenuId(), menusRoot));
  72.             }
  73.         }
  74.         return menusResult;
  75.     }
  76.     /**
  77.      * 获取菜单的子菜单
  78.      *
  79.      * @param id        菜单编号
  80.      * @param menusRoot menusRoot
  81.      * @return List
  82.      */
  83.     private static List<Menu> getChildList(Long id, List<Menu> menusRoot) {
  84.         List<Menu> menusChild = new ArrayList<>();
  85.         for (Menu menu : menusRoot) {
  86.             if (menu.getMenuPid() != 0) {
  87.                 if (menu.getMenuPid().equals(id)) {
  88.                     menusChild.add(menu);
  89.                     List<Menu> tempList = getChildList(menu.getMenuId(), menusRoot);
  90.                     tempList.sort((a, b) -> b.getMenuSort() - a.getMenuSort());
  91.                     menusChild.addAll(tempList);
  92.                 }
  93.             }
  94.         }
  95.         if (menusChild.size() == 0) {
  96.             return Collections.emptyList();
  97.         }
  98.         return menusChild;
  99.     }
  100. }

 

以上就可以从service层获得对应的封装类型的数据了

前台的菜单树结构应该是这样的

SpringBoot+FreeMarker实现无限级菜单

 

后台的菜单列表结构如下

SpringBoot+FreeMarker实现无限级菜单

 

到这里基本就实现了。

下面简单讲一下前台菜单的视图层怎么写

 

三、FreeMarker渲染前台菜单

1.最多展示三级

通常情况下,我们只需要展示两级菜单就差不多了,有时候也需要三级。

  1. <ul id="menu-mainmenu" class="down-menu nav-menu">
  2.         <#list frontMainMenus as menu>
  3.             <li id="menu-item-${menu.menuId}"
  4.                 class="menu-item menu-item-type-custom menu-item-object-custom">
  5.                 <a href="${(menu.menuUrl)!}">
  6.                     <i class="${(menu.menuIcon)!}"></i>
  7.                     <span class="font-text">${(menu.menuName)!}</span>
  8.                 </a>
  9.                 <ul class="sub-menu">
  10.                     <#list menu.childMenus as menu2>
  11.                         <li id="menu-item-${menu2.menuId}"
  12.                             class="menu-item menu-item-type-custom menu-item-object-custom">
  13.                             <a href="${(menu2.menuUrl)!}">
  14.                                 <i class="${(menu2.menuIcon)!}"></i>
  15.                                 <span class="font-text">${(menu2.menuName)!}</span>
  16.                             </a>
  17.                             <ul class="sub-menu">
  18.                                 <#list menu2.childMenus?if_exists as menu3>
  19.                                     <li id="menu-item-${menu3.menuId}"
  20.                                         class="menu-item menu-item-type-custom menu-item-object-custom">
  21.                                         <a href="${(menu2.menuIcon)!}"">
  22.                                             <span class="font-text">${(menu3.menuName)!}</span>
  23.                                         </a>
  24.                                     </li>
  25.                                 </#list>
  26.                             </ul>
  27.                         </li>
  28.                     </#list>
  29.                 </ul>
  30.             </li>
  31.         </#list>
  32.     </ul>

通过三层for循环展示,方法虽然蠢但是可以实现。

 

2.展示无限级菜单

如果用for循环可能实现不了

  1. <#macro childMenus menus>
  2.       <ul class="sub-menu">
  3.           <#list menus as menu>
  4.               <li id="menu-item-${menu.menuId}"
  5.                   class="menu-item menu-item-type-custom menu-item-object-custom">
  6.                   <a href="${(menu.menuIcon)!}"">
  7.                       <span class="font-text">${(menu.menuName)!}</span>
  8.                   </a>
  9.                   <#if menu.childMenus?? && menu.childMenus?size gt 0>
  10.                       <@childMenus menu.childMenus></@childMenus>
  11.                   </#if>
  12.               </li>
  13.           </#list>
  14.       </ul>
  15.   </#macro>
  16.   <ul id="menu-mainmenu" class="down-menu nav-menu">
  17.   <@commonTag method="menus">
  18.       <#list frontMainMenus as menu>
  19.           <li id="menu-item-${menu.menuId}"
  20.               class="menu-item menu-item-type-custom menu-item-object-custom">
  21.               <a href="${(menu.menuUrl)!}">
  22.                   <i class="${(menu.menuIcon)!}"></i>
  23.                   <span class="font-text">${(menu.menuName)!}</span>
  24.               </a>
  25.               <#if menu.childMenus?? && menu.childMenus?size gt 0>
  26.                   <@childMenus menu.childMenus></@childMenus>
  27.               </#if>
  28.           </li>
  29.       </#list>
  30.   </@commonTag>
  31.   </ul>

commonTag是自定义的 FreeMarker 标签,macro 是 FreeMarker 标签

 

  • 微信
  • 交流学习,有偿服务
  • weinxin
  • 博客/Java交流群
  • 资源分享,问题解决,技术交流。群号:590480292
  • weinxin
言曌

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: