Vue3 + Element Plus 实现动态二级菜单

1,992 阅读3分钟

一、数据库中创建菜单表

注意核心是 parent_menu_id记录了父菜单的 id。

插入一些数据:

idnamechinese_nameparent_menu_idmenu_status
1literature文学01
2noval小说11
3essay散文随笔11
4youth_literature青春文学11
5biography传记11
6cartoon动漫11
7suspenseful_reasoning悬疑推理11
8science_fiction科幻11
9martial_arts武侠11
10world_famous世界名著11
11humanity_social_science人文社科01
12history历史111
13psychology心理学111

parent_menu_id0表示当前菜单为一级菜单,没有父菜单。

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>

七、结果

没有展开二级菜单

展开二级菜单