一、数据库中创建菜单表

image.png
注意核心是 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_id0表示当前菜单为一级菜单,没有父菜单。
id 为 6 的「动漫」菜单是一个二级菜单,父菜单 id 为 1 说明父菜单是「文学」。

id 为 12 的「历史」菜单也是一个二级菜单,父菜单 id 为 11 说明父菜单是「人文社科」。

二、新建 vo 实体类

新建 vo 目录,然后在目录下新建 MenuTreeVO.java树形类。

  1. @Data
  2. @TableName("t_menu")
  3. public class MenuTreeVO {
  4. @TableId("id")
  5. private Integer id;
  6. /**
  7. * 菜单的英文名称
  8. */
  9. @TableField("name")
  10. private String name;
  11. /**
  12. * 菜单的中文名
  13. */
  14. @TableField("chinese_name")
  15. private String chineseName;
  16. /**
  17. * 父菜单 id
  18. */
  19. @TableField("parent_menu_id")
  20. private int parentMenuId;
  21. /**
  22. * 菜单可见性
  23. */
  24. @TableField("menu_status")
  25. private Boolean menuStatus;
  26. /**
  27. * 该菜单的所有子菜单,注明非数据库中的字段
  28. */
  29. @TableField(exist = false)
  30. private List<MenuTreeVO> childMenu;
  31. }

增加一个字段 List<MenuTreeVO> childMenu表明当前菜单的子菜单列表。
使用 @TableField(exist = false)表明非数据库中的字段。

三、新建 mapper

  1. @Mapper
  2. public interface MenuTableMapper extends BaseMapper<MenuTreeVO> {
  3. }

四、新建 MenuTreeUtil.java

新建 util 目录,然后在目录下新建 MenuTreeUtil.java工具类,目的是转为 tree 结构。

  1. /**
  2. * @author yunhu
  3. * @date 2022-5-29
  4. */
  5. public class MenuTreeUtil {
  6. /**
  7. * 所有的菜单
  8. */
  9. private static List<MenuTreeVO> allList = null;
  10. /**
  11. * 转换为树形结构
  12. * @param list 所有的节点
  13. * @return 树结构菜单
  14. */
  15. public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) {
  16. allList = new ArrayList<>(list);
  17. // 获取所有的一级菜单,父菜单 id 为 0
  18. List<MenuTreeVO> roots = new ArrayList<>();
  19. // 遍历
  20. for (MenuTreeVO menuTreeVO: list) {
  21. if (menuTreeVO.getParentMenuId() == 0) {
  22. roots.add(menuTreeVO);
  23. }
  24. }
  25. // 删除一级菜单
  26. allList.removeAll(roots);
  27. // 对每一个一级菜单添加二级菜单
  28. for (MenuTreeVO menuTreeVO: roots) {
  29. // 设置子菜单
  30. menuTreeVO.setChildMenu(getCurrentChildrenMenu(menuTreeVO));
  31. }
  32. return roots;
  33. }
  34. /**
  35. * 通过父菜单获取子菜单列表
  36. * @param parentMenu 父菜单
  37. * @return 子菜单列表
  38. */
  39. private static List<MenuTreeVO> getCurrentChildrenMenu(MenuTreeVO parentMenu) {
  40. // 判断当前节点是否已经存在子结点
  41. List<MenuTreeVO> childMenuList;
  42. if (parentMenu.getChildMenu() == null) {
  43. childMenuList = new ArrayList<>();;
  44. } else {
  45. childMenuList = parentMenu.getChildMenu();
  46. }
  47. // 遍历所有的菜单,除了一级菜单,之前删过
  48. for (MenuTreeVO childMenu: allList) {
  49. if (parentMenu.getId() == childMenu.getParentMenuId()) {
  50. // 某个菜单的父菜单 id 等于当前菜单,这个菜单就是子菜单
  51. childMenuList.add(childMenu);
  52. }
  53. }
  54. allList.removeAll(childMenuList);
  55. return childMenuList;
  56. }
  57. }

五、使用

  1. /**
  2. * 从数据库中获取所有的菜单数据后,转为树形结构
  3. * @author 云胡
  4. * @return 树形菜单结构数据
  5. */
  6. @GetMapping(value = "/getAllMenu")
  7. @ResponseBody
  8. public List<MenuTreeVO> getAllSupplier(){
  9. // 先获取所有的数据表数据
  10. List<MenuTreeVO> allMenuTreeVoList = menuTableMapper.selectList(null);
  11. List<MenuTreeVO> menuTreeVOTreeList = MenuTreeUtil.toTree(allMenuTreeVoList);
  12. if (CollectionUtils.isNotEmpty(menuTreeVOTreeList)) {
  13. return menuTreeVOTreeList;
  14. }else {
  15. return null;
  16. }
  17. }

返回的 json 数据:

  1. [
  2. {
  3. "id": 1,
  4. "name": "literature",
  5. "chineseName": "文学",
  6. "parentMenuId": 0,
  7. "menuStatus": true,
  8. "childMenu": [
  9. {
  10. "id": 2,
  11. "name": "noval",
  12. "chineseName": "小说",
  13. "parentMenuId": 1,
  14. "menuStatus": true,
  15. "childMenu": null
  16. },
  17. {
  18. "id": 3,
  19. "name": "essay",
  20. "chineseName": "散文随笔",
  21. "parentMenuId": 1,
  22. "menuStatus": true,
  23. "childMenu": null
  24. },
  25. {
  26. "id": 4,
  27. "name": "youth_literature",
  28. "chineseName": "青春文学",
  29. "parentMenuId": 1,
  30. "menuStatus": true,
  31. "childMenu": null
  32. },
  33. {
  34. "id": 5,
  35. "name": "biography",
  36. "chineseName": "传记",
  37. "parentMenuId": 1,
  38. "menuStatus": true,
  39. "childMenu": null
  40. },
  41. {
  42. "id": 6,
  43. "name": "cartoon",
  44. "chineseName": "动漫",
  45. "parentMenuId": 1,
  46. "menuStatus": true,
  47. "childMenu": null
  48. },
  49. {
  50. "id": 7,
  51. "name": "suspenseful_reasoning",
  52. "chineseName": "悬疑推理",
  53. "parentMenuId": 1,
  54. "menuStatus": true,
  55. "childMenu": null
  56. },
  57. {
  58. "id": 8,
  59. "name": "science_fiction",
  60. "chineseName": "科幻",
  61. "parentMenuId": 1,
  62. "menuStatus": true,
  63. "childMenu": null
  64. },
  65. {
  66. "id": 9,
  67. "name": "martial_arts",
  68. "chineseName": "武侠",
  69. "parentMenuId": 1,
  70. "menuStatus": true,
  71. "childMenu": null
  72. },
  73. {
  74. "id": 10,
  75. "name": "world_famous",
  76. "chineseName": "世界名著",
  77. "parentMenuId": 1,
  78. "menuStatus": true,
  79. "childMenu": null
  80. }
  81. ]
  82. },
  83. {
  84. "id": 11,
  85. "name": "humanity_social_science",
  86. "chineseName": "人文社科",
  87. "parentMenuId": 0,
  88. "menuStatus": true,
  89. "childMenu": [
  90. {
  91. "id": 12,
  92. "name": "history",
  93. "chineseName": "历史",
  94. "parentMenuId": 11,
  95. "menuStatus": true,
  96. "childMenu": null
  97. },
  98. {
  99. "id": 13,
  100. "name": "psychology",
  101. "chineseName": "心理学",
  102. "parentMenuId": 11,
  103. "menuStatus": true,
  104. "childMenu": null
  105. }
  106. ]
  107. }
  108. ]

六、前端渲染

6.1 前端获取 JSON 数据

  1. setup() {
  2. const menuData = ref();
  3. onMounted(() => {
  4. axios({
  5. method: "GET",
  6. url: "http://localhost:8082/getAllMenu"
  7. })
  8. .then(res => {
  9. console.log(res);
  10. // 必须在加上 .value
  11. menuData.value = res.data;
  12. ElMessage.success("获取成功");
  13. })
  14. .catch(err => {
  15. console.log("err = " + err);
  16. ElMessage.error("获取失败");
  17. });
  18. });
  19. return {
  20. menuData
  21. };
  22. }

6.2 渲染到组件上

6.2.1 父组件

  1. <template>
  2. <div>
  3. <el-row>
  4. <el-col :span="3">
  5. <h3 class="mb-2">图书分类</h3>
  6. <el-menu
  7. :router="true"
  8. background-color="#c6e2ff"
  9. default-active="2"
  10. class="NavigationDefaultActive"
  11. mode="vertical"
  12. >
  13. <menus :menuItem="menuData" />
  14. </el-menu>
  15. </el-col>
  16. </el-row>
  17. </div>
  18. </template>

menus 是子组件名称。

6.2.2 子组件渲染出菜单

  1. <template>
  2. <div>
  3. <el-sub-menu v-for="item in menuItem" :key="item.id" :index="'/'+item.id">
  4. <template #title>
  5. <span>{{item.chineseName}}</span>
  6. </template>
  7. <!-- 二级菜单 -->
  8. <el-menu-item
  9. v-for="item2 in item.childMenu"
  10. :key="item2.id"
  11. :index="'/'+item2.id"
  12. >{{item2.chineseName }}
  13. </el-menu-item>
  14. </el-sub-menu>
  15. <!-- </el-col> -->
  16. </div>
  17. </template>

七、结果

image.png

image.png