一、数据库中创建菜单表
注意核心是 parent_menu_id记录了父菜单的 id。
插入一些数据:
| id | name | chinese_name | parent_menu_id | menu_status |
|---|---|---|---|---|
| 1 | literature | 文学 | 0 | 1 |
| 2 | noval | 小说 | 1 | 1 |
| 3 | essay | 散文随笔 | 1 | 1 |
| 4 | youth_literature | 青春文学 | 1 | 1 |
| 5 | biography | 传记 | 1 | 1 |
| 6 | cartoon | 动漫 | 1 | 1 |
| 7 | suspenseful_reasoning | 悬疑推理 | 1 | 1 |
| 8 | science_fiction | 科幻 | 1 | 1 |
| 9 | martial_arts | 武侠 | 1 | 1 |
| 10 | world_famous | 世界名著 | 1 | 1 |
| 11 | humanity_social_science | 人文社科 | 0 | 1 |
| 12 | history | 历史 | 11 | 1 |
| 13 | psychology | 心理学 | 11 | 1 |
parent_menu_id 为 0表示当前菜单为一级菜单,没有父菜单。
id 为 6 的「动漫」菜单是一个二级菜单,父菜单 id 为 1 说明父菜单是「文学」。
id 为 12 的「历史」菜单也是一个二级菜单,父菜单 id 为 11 说明父菜单是「人文社科」。
二、新建 vo 实体类
新建 vo 目录,然后在目录下新建 MenuTreeVO.java树形类。
/**
* @author yunhu
* @date 2022-5-29
*/
@Data
@TableName("t_menu")
public class MenuTreeVO {
@TableId("id")
private Integer id;
/**
* 菜单的英文名称
*/
@TableField("name")
private String name;
/**
* 菜单的中文名
*/
@TableField("chinese_name")
private String chineseName;
/**
* 父菜单 id
*/
@TableField("parent_menu_id")
private int parentMenuId;
/**
* 菜单可见性
*/
@TableField("menu_status")
private Boolean menuStatus;
/**
* 该菜单的所有子菜单,注明非数据库中的字段
*/
@TableField(exist = false)
private List<MenuTreeVO> childMenu;
}
增加一个字段 List<MenuTreeVO> childMenu表明当前菜单的子菜单列表。
使用 @TableField(exist = false)表明非数据库中的字段。
三、新建 mapper
@Mapper
public interface MenuTableMapper extends BaseMapper<MenuTreeVO> {
}
四、新建 MenuTreeUtil.java
新建 util 目录,然后在目录下新建 MenuTreeUtil.java工具类,目的是转为 tree 结构。
/**
* @author yunhu
* @date 2022-5-29
*/
public class MenuTreeUtil {
/**
* 所有的菜单
*/
private static List<MenuTreeVO> allList = null;
/**
* 转换为树形结构
* @param list 所有的节点
* @return 树结构菜单
*/
public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) {
allList = new ArrayList<>(list);
// 获取所有的一级菜单,父菜单 id 为 0
List<MenuTreeVO> roots = new ArrayList<>();
// 遍历
for (MenuTreeVO menuTreeVO: list) {
if (menuTreeVO.getParentMenuId() == 0) {
roots.add(menuTreeVO);
}
}
// 删除一级菜单
allList.removeAll(roots);
// 对每一个一级菜单添加二级菜单
for (MenuTreeVO menuTreeVO: roots) {
// 设置子菜单
menuTreeVO.setChildMenu(getCurrentChildrenMenu(menuTreeVO));
}
return roots;
}
/**
* 通过父菜单获取子菜单列表
* @param parentMenu 父菜单
* @return 子菜单列表
*/
private static List<MenuTreeVO> getCurrentChildrenMenu(MenuTreeVO parentMenu) {
// 判断当前节点是否已经存在子结点
List<MenuTreeVO> childMenuList;
if (parentMenu.getChildMenu() == null) {
childMenuList = new ArrayList<>();;
} else {
childMenuList = parentMenu.getChildMenu();
}
// 遍历所有的菜单,除了一级菜单,之前删过
for (MenuTreeVO childMenu: allList) {
if (parentMenu.getId() == childMenu.getParentMenuId()) {
// 某个菜单的父菜单 id 等于当前菜单,这个菜单就是子菜单
childMenuList.add(childMenu);
}
}
allList.removeAll(childMenuList);
return childMenuList;
}
}
五、使用
/**
* 从数据库中获取所有的菜单数据后,转为树形结构
* @author 云胡
* @return 树形菜单结构数据
*/
@GetMapping(value = "/getAllMenu")
@ResponseBody
public List<MenuTreeVO> getAllSupplier(){
// 先获取所有的数据表数据
List<MenuTreeVO> allMenuTreeVoList = menuTableMapper.selectList(null);
List<MenuTreeVO> menuTreeVOTreeList = MenuTreeUtil.toTree(allMenuTreeVoList);
if (CollectionUtils.isNotEmpty(menuTreeVOTreeList)) {
return menuTreeVOTreeList;
}else {
return null;
}
}
返回的 json 数据:
[
{
"id": 1,
"name": "literature",
"chineseName": "文学",
"parentMenuId": 0,
"menuStatus": true,
"childMenu": [
{
"id": 2,
"name": "noval",
"chineseName": "小说",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 3,
"name": "essay",
"chineseName": "散文随笔",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 4,
"name": "youth_literature",
"chineseName": "青春文学",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 5,
"name": "biography",
"chineseName": "传记",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 6,
"name": "cartoon",
"chineseName": "动漫",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 7,
"name": "suspenseful_reasoning",
"chineseName": "悬疑推理",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 8,
"name": "science_fiction",
"chineseName": "科幻",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 9,
"name": "martial_arts",
"chineseName": "武侠",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
},
{
"id": 10,
"name": "world_famous",
"chineseName": "世界名著",
"parentMenuId": 1,
"menuStatus": true,
"childMenu": null
}
]
},
{
"id": 11,
"name": "humanity_social_science",
"chineseName": "人文社科",
"parentMenuId": 0,
"menuStatus": true,
"childMenu": [
{
"id": 12,
"name": "history",
"chineseName": "历史",
"parentMenuId": 11,
"menuStatus": true,
"childMenu": null
},
{
"id": 13,
"name": "psychology",
"chineseName": "心理学",
"parentMenuId": 11,
"menuStatus": true,
"childMenu": null
}
]
}
]
六、前端渲染
6.1 前端获取 JSON 数据
setup() {
const menuData = ref();
onMounted(() => {
axios({
method: "GET",
url: "http://localhost:8082/getAllMenu"
})
.then(res => {
console.log(res);
// 必须在加上 .value
menuData.value = res.data;
ElMessage.success("获取成功");
})
.catch(err => {
console.log("err = " + err);
ElMessage.error("获取失败");
});
});
return {
menuData
};
}
6.2 渲染到组件上
6.2.1 父组件
<template>
<div>
<el-row>
<el-col :span="3">
<h3 class="mb-2">图书分类</h3>
<el-menu
:router="true"
background-color="#c6e2ff"
default-active="2"
class="NavigationDefaultActive"
mode="vertical"
>
<menus :menuItem="menuData" />
</el-menu>
</el-col>
</el-row>
</div>
</template>
menus 是子组件名称。
6.2.2 子组件渲染出菜单
<template>
<div>
<el-sub-menu v-for="item in menuItem" :key="item.id" :index="'/'+item.id">
<template #title>
<span>{{item.chineseName}}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item
v-for="item2 in item.childMenu"
:key="item2.id"
:index="'/'+item2.id"
>{{item2.chineseName }}
</el-menu-item>
</el-sub-menu>
<!-- </el-col> -->
</div>
</template>