关于左侧菜单的开发和动态路由注册
el-menu的菜单的基本认识和理解
- el-menu
- el-sub-menu
- `<template #title>item four
- el-menu-item
<template #title><span>item four</span></template> - el-menu-item
<template #title><span>item four</span></template> - el-menu-item
<template #title><span>item four</span></template>
还原web格式
在后端的菜单
存在两种情况,一种有子元素,一种是没子元素,所以我们就必须进行区分。区分的界限其实就通过一个menu.children.length > 0 说明存在子元素。反之,就没子元素。就直接显示,具体代码如下:
<el-menu :default-active="1" class="border-0" :unique-opened="true" :collapse-transition="false">
<el-sub-menu index="1-1">
<template #title>
<el-icon><Location/></el-icon>
<span>控制面板</span>
</template>
<el-menu-item index="1-1-1">
<el-icon><Location/></el-icon>
<span>后台首页</span>
</el-menu-item>
<el-menu-item index="1-1-2">
<el-icon><User/></el-icon>
<span>后台设置</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><Share/></el-icon>
<span>优惠券管理</span>
</el-menu-item>
</el-menu>
菜单接口的设计和递归 - 服务端
1: 数据库表 
CREATE TABLE `kss_admin_menu` (
`id` bigint(20) NOT NULL COMMENT '主键',
`name` varchar(128) DEFAULT '0' COMMENT '菜单名词',
`sorted` int(11) DEFAULT NULL COMMENT '菜单排序',
`path` varchar(400) DEFAULT NULL COMMENT '菜单链接',
`icon` varchar(128) DEFAULT NULL COMMENT '菜单图标',
`status` int(11) DEFAULT NULL COMMENT '菜单发布',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`pid` bigint(20) DEFAULT NULL COMMENT '菜单名称',
`componentname` varchar(200) DEFAULT NULL COMMENT '组件名称',
`pathname` varchar(100) DEFAULT NULL COMMENT '路径名称',
`layout` varchar(50) DEFAULT NULL COMMENT '父组件',
`indexon` int(11) DEFAULT NULL COMMENT '排序',
`showflag` int(1) DEFAULT NULL COMMENT '是否展示',
`isdelete` int(1) DEFAULT NULL COMMENT '删除状态 0未删除 1删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='菜单管理 '
2: 创建实体
package com.pug.zixun.pojo;
import java.util.Date;
import java.util.List;
import lombok.*;
import com.baomidou.mybatisplus.annotation.*;
import org.pug.generator.anno.PugDoc;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName("kss_admin_menu")
public class AdminMenu implements java.io.Serializable {
@PugDoc(name="主键")
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@PugDoc(name="菜单名词")
private String name;
@PugDoc(name="菜单链接")
private String path;
@PugDoc(name="路径名称")
private String pathname;
@PugDoc(name="菜单图标")
private String icon;
@PugDoc(name="菜单排序")
private Integer sorted;
@PugDoc(name="菜单发布")
private Integer status;
@PugDoc(name="创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@PugDoc(name="更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@PugDoc(name="菜单名称")
private Long pid;
@PugDoc(name="删除状态 0未删除 1删除")
private Integer isdelete;
@TableField(exist = false)
private List<AdminMenu> children;
}
3: AdminMenuMapper
package com.pug.zixun.mapper;
import com.pug.zixun.pojo.AdminMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface AdminMenuMapper extends BaseMapper<AdminMenu>{
}
4: AdminMenuService.java
package com.pug.zixun.service.adminmenu;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pug.zixun.pojo.AdminMenu;
import com.pug.zixun.vo.AdminMenuVo;
import com.pug.zixun.bo.AdminMenuBo;
import com.pug.zixun.service.BaseService;
import java.util.List;
public interface IAdminMenuService extends IService<AdminMenu>,BaseService{
List<AdminMenu> findAdminMenuTree();
}
5: 实现类
package com.pug.zixun.service.adminmenu;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pug.zixun.mapper.AdminMenuMapper;
import com.pug.zixun.pojo.AdminMenu;
import com.pug.zixun.vo.AdminMenuVo;
import com.pug.zixun.bo.AdminMenuBo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.pug.zixun.commons.enums.ResultStatusEnum;
import com.pug.zixun.commons.ex.PugValidatorException;
import com.pug.zixun.commons.utils.fn.asserts.Vsserts;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AdminMenuServiceImpl extends ServiceImpl<AdminMenuMapper,AdminMenu> implements IAdminMenuService {
@Override
public List<AdminMenu> findAdminMenuTree(){
LambdaQueryWrapper<AdminMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AdminMenu::getStatus,1);
List<AdminMenu> allList = this.list(lambdaQueryWrapper);
List<AdminMenu> rootList = allList.stream().filter(category -> category.getPid().equals(0L))
.sorted((a, b) -> a.getSorted() - b.getSorted()).collect(Collectors.toList());
List<AdminMenu> subList = allList.stream().filter(category -> !category.getPid().equals(0L)).collect(Collectors.toList());
rootList.forEach(root -> buckForback(root, subList));
return rootList;
}
private void buckForback(AdminMenu root, List<AdminMenu> subList) {
List<AdminMenu> childrenList = subList.stream().filter(category -> category.getPid().equals(root.getId()))
.sorted((a, b) -> a.getSorted() - b.getSorted())
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(childrenList)) {
root.setChildren(childrenList);
childrenList.forEach(category -> buckForback(category, subList));
} else {
root.setChildren(new ArrayList<>());
}
}
}
6:定义菜单controller
package com.pug.zixun.controller.adminmenu;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.pug.zixun.service.adminmenu.IAdminMenuService;
import com.pug.zixun.pojo.AdminMenu;
import com.pug.zixun.vo.AdminMenuVo;
import com.pug.zixun.bo.AdminMenuBo;
import com.pug.zixun.commons.enums.ResultStatusEnum;
import com.pug.zixun.commons.ex.PugValidatorException;
import com.pug.zixun.commons.utils.fn.asserts.Vsserts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import com.pug.zixun.controller.BaseController;
import java.util.List;
import org.pug.generator.anno.PugDoc;
@RestController
@RequiredArgsConstructor
@Slf4j
@PugDoc(name="后台菜单",tabname="kss_admin_menu")
public class AdminMenuController extends BaseController{
private final IAdminMenuService adminmenuService;
@PostMapping("/menu/tree")
@PugDoc(name="查询后台菜单信息")
public List<AdminMenu> tree() {
return adminmenuService.findAdminMenuTree();
}
}
7:分析原理
如何做到无限极菜单。通过表自映射过程
- 控制面板 id=1 pid = 0
- 后台首页 id=2 pid=1
- 后台设置 id=3 pid=1
- 优惠券管理 id=4 pid = 0
- 用户管理 id=5 pid=0
- 用户管理id=6 pid = 5
- 用户设置id=7 pid = 6
- 用户头像id =9 pid = 6
- 用户审核 id=10 pid = 6
- 用户添加id = 11 pid = 5
- 菜单管理 id = 12 pid =0
- 角色管理 id = 13 pid =0
- 权限管理 id = 14 pid =0
- 权限添加 id = 15 pid =14
- 权限列表 id = 16 pid =14
上面的菜单或者未来的百度网盘的目录结构的设计,其实都是一张数据库表。来完成的。 原理就是通过 id 和 pid来形成自映射的过程.
数据递归方式由如下几种
-
全查,不考虑父子关系,全部在java代码来完成
- 这种数据量比较小的情况,可以考虑
- 场景:菜单查询,分类查询
-
查询数据库的方式
-
根据pid=0查询所有的菜单根元素 -
循环遍历,然后再根据id去查询表中pid=id子元素。 -
场景:评论,百度目录设置 ,子元素查询和分页更注重异步去查询 - 今天心情不不错,发生大事情 pid = 0 id=1
- 是的 id=2 pid = 1
- 棒棒的 id=3 pid = 1
- 美美的 id=4 pid = 1
查看更多(120)
- 今天心情不不错,发生大事情 pid = 0 id=2
- 是的 id=12 pid = 2
- 帮帮的 id=13 pid = 2
- 美美 id=14 pid = 2
查看更多(12)
因为菜单的数据的是非常小的。所有考虑第一种方案,全查。
查询所有菜单数据
LambdaQueryWrapper<AdminMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AdminMenu::getStatus,1);
lambdaQueryWrapper.eq(AdminMenu::getIsdelete,0);
List<AdminMenu> allList = this.list(lambdaQueryWrapper);
得到代码如下:

过滤所有的根(父)元素
也就是pid=0的元素,如下
List<AdminMenu> rootMenuList = allList.stream().filter(menu -> menu.getPid().equals(0L)).collect(Collectors.toList());
结果如下:
- 控制面板 id=1 pid = 0
- 优惠券管理 id=4 pid = 0
- 用户管理 id=5 pid=0
- 菜单管理 id = 12 pid =0
- 角色管理 id = 13 pid =0
- 权限管理 id = 14 pid =0
遍历父元素开始找子元素
代码如下:
rootMenuList = rootMenuList.stream().map(rootMenu -> {
List<AdminMenu> childrenMenuList = allList.stream()
.filter(menu -> menu.getPid().equals(rootMenu.getId())).collect(Collectors.toList());
if (CollectionUtils.isEmpty(childrenMenuList)) {
childrenMenuList = new ArrayList<>();
}
rootMenu.setChildren(childrenMenuList);
return rootMenu;
}).collect(Collectors.toList());
结构入下:
- 控制面板 id=1 pid = 0
- 后台首页 id=2 pid=1
- 后台设置 id=3 pid=1
- 优惠券管理 id=4 pid = 0
- 用户管理 id=5 pid=0
- 用户管理id=6 pid = 5
- 用户添加id = 11 pid = 5
- 菜单管理 id = 12 pid =0
- 角色管理 id = 13 pid =0
- 权限管理 id = 14 pid =0
- 权限添加 id = 15 pid =14
- 权限列表 id = 16 pid =14
最终递归代码
List<AdminMenu> findAdminMenuTree() {
LambdaQueryWrapper<AdminMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AdminMenu::getStatus, 1);
lambdaQueryWrapper.eq(AdminMenu::getIsdelete, 0);
List<AdminMenu> allList = this.list(lambdaQueryWrapper);
List<AdminMenu> rootMenuList = allList.stream().filter(menu -> menu.getPid().equals(0L)).collect(Collectors.toList());
rootMenuList.forEach(rootMenu -> bucketList(rootMenu,allList));
return rootMenuList;
}
public void bucketList(AdminMenu rootMenu , List<AdminMenu> allList){
List<AdminMenu> childrenMenuList = allList.stream()
.filter(menu -> menu.getPid().equals(rootMenu.getId())).collect(Collectors.toList());
if (CollectionUtils.isEmpty(childrenMenuList)) {
rootMenu.setChildren(new ArrayList<>());
}else{
rootMenu.setChildren(childrenMenuList);
childrenMenuList.forEach((childrenMenu)->bucketList(childrenMenu,allList));
}
}
- 控制面板 id=1 pid = 0
- 后台首页 id=2 pid=1
- 后台设置 id=3 pid=1
- 优惠券管理 id=4 pid = 0
- 用户管理 id=5 pid=0
- 用户管理id=6 pid = 5
- 用户设置id=7 pid = 6
- 用户头像id =9 pid = 6
- 用户审核 id=10 pid = 6
- 用户添加id = 11 pid = 5
- 菜单管理 id = 12 pid =0
- 角色管理 id = 13 pid =0
- 权限管理 id = 14 pid =0
- 权限添加 id = 15 pid =14
- 权限列表 id = 16 pid =14
测试菜单的接口
http://127.0.0.1:8877/admin/menu/tree
前台对接菜单管理
1: 定义异步请求接口调用
在services/navmenu/AdminMenuService.js 如下:
import request from '@/utils/request'
export default {
loadNavMenu() {
return request.post("/menu/tree");
}
}
2:菜单渲染
找到layouts下面的PugMenu.vue进行异步调用接口,如下:
js部分
import adminMenuService from '@/services/navmenu/AdminMenuService.js'
const menuList = ref([]);
onMounted(async () => {
const severResponse = await adminMenuService.loadNavMenu();
menuList.value = severResponse.data;
})
vue部分
<el-menu
:default-active="1"
class="border-0"
:unique-opened="true"
:collapse-transition="false"
>
<template v-for="(menu,index) in menuList" :key="menu.id" >
<!--有子元素的菜单-->
<el-sub-menu :index="menu.name" v-if="menu.children && menu.children.length > 0">
<template #title>
<el-icon><Location/></el-icon>
<span>{{menu.name}}</span>
</template>
<el-menu-item :index="cmenu.path" v-for="(cmenu,cindex) in menu.children" :key="cmenu.id">
<el-icon><Location/></el-icon>
<span>{{cmenu.name}}</span>
</el-menu-item>
</el-sub-menu>
<!--无子元素的菜单-->
<el-menu-item :index="menu.path" v-else>
<el-icon><Share/></el-icon>
<span>{{menu.name}}</span>
</el-menu-item>
</template>
</el-menu>

关于路由转发
<el-sub-menu :index="menu.name" v-if="menu.children && menu.children.length > 0">
<el-menu-item :index="cmenu.path" v-for="(cmenu,cindex) in menu.children" :key="cmenu.id">
为什么上面的:index=menu.name 有的是 cmenu.path呢?存在的父级元素不参与路由转发。只有子元素才参与,所以子元素:index=“cmenu.path”
转发的过程如下:
- 通过点击子菜单或者没有子元素的菜单,获取菜单的index。(path)
- 然后通过router.push(index) 跳转即可
如下:
const handleSelectMenu = (index) => {
router.push(index);
}
注意记得先把所有的菜单路由和spa页面先进行定义。并且绑定关系哦才可以生效。否则全部跳入404页面
关于菜单图标的问题
使用动态组件实现
图标集合:https://element-plus.gitee.io/zh-CN/component/icon.html#%E5%9B%BE%E6%A0%87%E9%9B%86%E5%90%88
代码
<el-icon>
<component :is="menu.icon"></component>
</el-icon>
注意:
- 名字定义到数据库的时候,要么是和官方一模一样 ,比如:AddLocation
- 要么就是遵循驼峰小写定义,比如:add-location

|