多级菜单树形结构构建

建议先看:整形数组转换树形结构

树形结构,常用于上下级分级或导航栏的菜单分级,它们都可以算作是树形结构。

例如部门树:

    "data": [
        {
            "id": 100,
            "label": "若依科技",
            "children": [
                {
                    "id": 101,
                    "label": "深圳总公司",
                    "children": [
                        {
                            "id": 103,
                            "label": "研发部门"
                        },
                        {
                            "id": 104,
                            "label": "市场部门"
                        },
                        {
                            "id": 105,
                            "label": "测试部门"
                        },
                        {
                            "id": 106,
                            "label": "财务部门"
                        },
                        {
                            "id": 107,
                            "label": "运维部门"
                        }
                    ]
                },

实现效果:

我们知道通常来说会在设计数据库字段的时候,增设一个parentId用来指向父类(顶级菜单默认为null,同root->null一致),当然如果只有一二级菜单,并只查询指定一级菜单下的二级菜单,实现就很简单:

在这里插入图片描述

这里只涉及一二级菜单,将parent_id为0设置为一级菜单,二级菜单的parent_id是指向一级菜单的主键id,要获取所有的一级菜单以及对应所有的二级菜单,这里实际就是2个嵌套list,外层list的每一个元素代表一个一级菜单,例如list[0]代表商品,外层list的每一个元素都是list数据结构,其下代表对应多个二级菜单。
在这里插入图片描述

其中PmsProductCategoryWithChildrenItem的结构为:
在这里插入图片描述

这里要获取每个一级菜单下的二级菜单,其实在sql层面就可以解决,它底层就是一个自查询,首先二级菜单的parent_id是指向一级菜单的主键id(c1.id = c2.parent_id),通过这个查询就可以获取到每个顶级菜单对应子级菜单的组合:

在这里插入图片描述

这里还有一种写法,也是十分优雅:

在这里插入图片描述
在这里插入图片描述

它就是先查询出所有数据,然后再使用一个stream来进行填充嵌套list,其实现更通俗易懂一些。

但实际上这种实现方式都不能算是严格的树形结构,如果是有多级菜单,例如是三级菜单,那么就需要在 PmsProductCategoryNode 中嵌套一个 List<List> 来表示三级层级,这样维护填充这个list就太麻烦了,很容易就绕晕了,下面介绍一种多级树的解决方式。

多级菜单

例如导航栏,它通常是在首页进行展示,它要求后端查询所有的节点并表现层级关系,获取所有节点的数据很好实现,只需要执行一次全表查询即可,重点在于如何实现表现层级关系呢?

形象来说我们查询出来的所有节点数据,它是用列表来存储,但我们最终要的结果是要求以树形层次来返回。那么实际上问题就变为我们如何构建这样一颗树,并按照层级依次遍历打印,是不是很熟悉呢?想想二叉树的遍历就很好理解,那么既然是树,遍历最简单的就是通过递归实现,下面我们来看看代码是如何实现的。

递归最重要的就是要确定终止条件,这里的终止条件就是是否达到叶子节点和当前父节点没有需要遍历的子节点。

构造树形结构

树形结构的构建,是依次通过 递归查询 +判断的方式实现的,TreeSelect 类作为树节点的模型类,用于表示每一个树节点的基本信息和子节点:

public class TreeSelect implements Serializable {
    private static final long serialVersionUID = 1L;

    /** 节点ID */
    private Long id;

    /** 节点名称 */
    private String label;

    /** 子节点 */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<TreeSelect> children;

    // Getters and Setters
}
  • id:节点的唯一标识。
  • label:节点的名称(例如:部门名称)。
  • children:节点的子节点,递归地包含树形结构的子节点列表。使用 @JsonInclude(JsonInclude.Include.NON_EMPTY) 注解确保如果没有子节点时不返回 null

构建树形结构:后端依次从数据库查询出所有的部门(如果是父类节点则向下查询子类),通过递归的方式将部门列表转化为树形结构,确保每个部门的 children 列表中包含它的所有子部门。

节点信息(部门):

public class SysDept {
    private Long deptId;
    private Long parentId;
    private String deptName;
    private List<SysDept> children;  // 用来存储子部门

    // Getters and Setters
}

流程:

  • 查询所有部门数据
  • 构建树形结构
@Override
public List<TreeSelect> selectDeptTreeList(SysDept dept)
{
    // 查询所有部门数据
    List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);

    // 构建树形结构
    return buildDeptTreeSelect(depts);
}
  • selectDeptList(dept):查询所有部门列表的数据。
  • buildDeptTreeSelect(depts):将查询出来的所有部门数据传给 buildDeptTreeSelect 方法来构建树形结构。
@Override
public List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts)
{
    // 构建树结构
    List<SysDept> deptTrees = buildDeptTree(depts);

    // 将树结构转换为前端需要的格式
    return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
}
  • buildDeptTree(depts):此方法将部门列表转换为树形结构。
  • stream().map(TreeSelect::new):将 SysDept 类型的树结构转换为前端所需的 TreeSelect 类型。
@Override
public List<SysDept> buildDeptTree(List<SysDept> depts)
{
    List<SysDept> returnList = new ArrayList<>();
    List<Long> tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList());

    // 遍历所有部门,找出顶级部门(根节点)
    for (SysDept dept : depts)
    {
        if (!tempList.contains(dept.getParentId()))
        {
            recursionFn(depts, dept);  // 对顶级部门递归构建树
            returnList.add(dept);  // 将顶级部门添加到返回列表
        }
    }

    // 如果没有顶级部门,直接返回所有部门
    if (returnList.isEmpty()) {
        returnList = depts;
    }
    
    return returnList;
}
  • tempList.contains(dept.getParentId()):通过这个条件判断每个部门是否是顶级部门(即 parentId 不在其他部门的 deptId 中)。
  • recursionFn(depts, dept):对每个顶级部门调用递归方法,将它的子部门加入 children 属性。
  • 最终,returnList 包含了所有顶级部门,且每个部门都正确地构建了它的子部门。

核心方法—递归查询

private void recursionFn(List<SysDept> list, SysDept t)
{
    // 获取当前部门的子部门
    List<SysDept> childList = getChildList(list, t);
    t.setChildren(childList);  // 设置当前部门的子部门列表

    // 递归遍历所有子部门
    for (SysDept tChild : childList)
    {
        if (hasChild(list, tChild))  // 判断该子部门是否还有子部门
        {
            recursionFn(list, tChild);  // 递归调用,继续为子部门找子部门
        }
    }
}
  • getChildList(list, t):获取当前部门 t 的所有子部门。
  • t.setChildren(childList):设置当前部门的子部门列表,形成树形结构。
  • hasChild(list, tChild):判断某个子部门是否还有子部门。如果有,则继续递归。

获取子部门:getChildList 方法

private List<SysDept> getChildList(List<SysDept> list, SysDept t)
{
    List<SysDept> tlist = new ArrayList<>();
    Iterator<SysDept> it = list.iterator();
    while (it.hasNext())
    {
        SysDept n = it.next();
        // 判断当前部门是否是目标部门的子部门
        if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue())
        {
            tlist.add(n);
        }
    }
    return tlist;
}
  • 遍历所有部门,判断每个部门的 parentId 是否等于目标部门 tdeptId,如果相等则表示该部门是目标部门的子部门。

判断是否有子部门:hasChild 方法

private boolean hasChild(List<SysDept> list, SysDept t)
{
    return getChildList(list, t).size() > 0;
}
  • 判断某个部门是否有子部门。如果该部门的子部门列表不为空,则返回 true,否则返回 false

递归查询流程:

  1. 查询部门数据:首先通过 selectDeptList 查询出所有的部门数据。
  2. 构建树形结构:调用 buildDeptTree 方法,将顶级部门(即没有父部门的部门)提取出来,递归地查找每个部门的子部门,并形成树形结构。
  3. 树的构建过程:在 recursionFn 方法中,针对每个部门,递归地将它的子部门添加到它的 children 属性中。直到所有部门都被正确地组织成树形结构。
  4. 将树转换为 TreeSelect 对象:最终将树形结构转换为适合前端展示的 TreeSelect 对象,并返回给前端。

简易版本

可以根据实现细节进行修改:

  • 查找当前节点的子节点。
  • 递归构建树形结构:将子节点添加到父节点的 children 属性中,递归遍历直到树结构构建完成。
  • 构建根节点:从所有节点中找出根节点,并对每个根节点递归构建完整的树形结构。
// 定义树形结构节点类
class TreeNode {
    private Long id;            // 节点的唯一标识符
    private String label;       // 节点的名称或标签
    private Long parentId;      // 节点的父节点ID
    private List<TreeNode> children = new ArrayList<>(); // 子节点列表

    // Getters 和 Setters
}

// 构建树形结构的类
public class TreeBuilder {
    
    // 查找当前节点的所有子节点
    private List<TreeNode> getChildNodes(List<TreeNode> allNodes, TreeNode parent) {
        List<TreeNode> children = new ArrayList<>();  // 存放当前父节点的子节点列表
        for (TreeNode node : allNodes) {  // 遍历所有节点
            // 判断当前节点是否是给定父节点的子节点
            if (node.getParentId().equals(parent.getId())) {
                children.add(node);  // 将符合条件的节点加入子节点列表
            }
        }
        return children;  // 返回当前节点的所有子节点
    }

    // 递归构建树形结构
    private void buildTree(List<TreeNode> allNodes, TreeNode parent) {
        // 查找父节点的所有子节点
        List<TreeNode> children = getChildNodes(allNodes, parent);
        
        // 将子节点列表设置到当前父节点的 children 属性中
        parent.setChildren(children);
        
        // 对每一个子节点,递归调用 buildTree 方法
        for (TreeNode child : children) {
            buildTree(allNodes, child);  // 递归处理每个子节点,直到树的底层
        }
    }

    // 构建树形结构
    public List<TreeNode> buildTree(List<TreeNode> allNodes) {
        List<TreeNode> rootNodes = new ArrayList<>();  // 存储所有的根节点
        
        // 找到所有根节点(parentId 为 null 或 0 的节点)
        for (TreeNode node : allNodes) {
            if (node.getParentId() == null || node.getParentId().equals(0L)) {
                rootNodes.add(node);  // 如果 parentId 为 null 或 0,说明该节点是根节点
            }
        }
        
        // 对每个根节点进行递归构建树形结构
        for (TreeNode rootNode : rootNodes) {
            buildTree(allNodes, rootNode);  // 对每个根节点进行递归构建
        }
        
        return rootNodes;  // 返回所有根节点的树形结构
    }
}

假设我们有以下的节点数据:

idlabelparentId
1Anull
2B1
3C1
4D2
5E2
6F3

调用 buildTree(allNodes) 后,树的结构将如下所示:

A
├── B
│ ├── D
│ └── E
└── C
└── F

A 是根节点,它有两个子节点 B 和 C。
B 有两个子节点 D 和 E。
C 有一个子节点 F。

项目实战–实现一个多级菜单统一工具类_多级菜单设计-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值