有父子关系可无限递归我们以导航菜单为例

有父子关系可无限递归我们以导航菜单为例

前言

在开发中,我们经常会遇到导航菜单、部门菜单、权限树、评论等功能。

这些功能都有共同的特点:

存在父子关系,并且可以无限递归。

我们以导航菜单为例。 我们将导航菜单设置为动态角色权限设计,即动态加载菜单数据。

数据库设计

适合数据库存储的设计如下:

create table `menus`
(
  `id` int primary key auto_increment,
  `name` varchar(20) comment '菜单名称',
  `pid` int default 0 comment '父级 ID, 最顶级为 0',
  `order` int comment '排序, 序号越大, 越靠前'
)

前端渲染

对于前端来说,我们一般需要这样的效果:

菜单配置页面:

角色权限设计方案_角色权限设计_角色权限设计原理

对应的导航菜单:

角色权限设计原理_角色权限设计_角色权限设计方案

常用的树形展示插件有:JsTree、Layui Tree、Bootstrap Tree View等。

这些插件通常需要这两种格式:

基本格式:

[
    {
        "id": 1,
        "name": "权限管理",
        "pid": 0,
        "order": 1
    },
    {
        "id": 2,
        "name": "用户管理",
        "pid": 1,
        "order": 2
    },
    {
        "id": 3,
        "name": "角色管理",
        "pid": 1,
        "order": 3
    },
    {
        "id": 4,
        "name": "权限管理",
        "pid": 1,
        "order": 4
    }
]

树格式:

[
    {
        "id": 1,
        "name": "权限管理",
        "pid": 0,
        "order": 1,
        "children": [
            {
                "id": 2,
                "name": "用户管理",
                "pid": 1,
                "order": 2,
                "children": []
            },
            {
                "id": 3,
                "name": "角色管理",
                "pid": 1,
                "order": 3,
                "children": []
            },
            {
                "id": 4,
                "name": "权限管理",
                "pid": 1,
                "order": 4,
                "children": []
            }
        ]
    }
]

有的插件支持这两种格式游戏图片,有的则只支持树形结构,但我们数据库查询的结果往往都是普通的结构。 这时我们需要将普通格式转换为树格式。

这种转换一般在服务器端进行(因为大多数前端插件都是在后台请求一个URL来接收JSON数据地图场景,并且在加载数据之后-渲染之前不提供事件角色权限设计,所以无法在服务器端完成转换)前端。)

数据转换

首先是Java实体类:

public class Menu {
    private int id,
    private String name,
    private int pid
    // getter setter 略
}

数据库查询后,通常是在List中:

List menus = xxxMapper.selectXXX();

然后我们需要将这个List转换成树形结构。 首先定义一个树形结构的VO类:

public class MenuTreeVO {
    private int id,
    private String name,
    private int pid,
    private List children,
    // getter setter 略
}

转换工具类:

package im.zhaojun.util;
import im.zhaojun.model.vo.MenuTreeVO;
import java.util.ArrayList;
import java.util.List;
public class TreeUtil {
    /**
     * 所有待用"菜单"
     */
    private static List all = null;
    /**
     * 转换为树形
     * @param list 所有节点
     * @return 转换后的树结构菜单
     */
    public static List toTree(List list) {
        // 最初, 所有的 "菜单" 都是待用的
        all = new ArrayList<>(list);
        // 拿到所有的顶级 "菜单"
        List roots = new ArrayList<>();
        for (MenuTreeVO menuTreeVO : list) {
            if (menuTreeVO.getParentId() == 0) {
                roots.add(menuTreeVO);
            }
        }
        // 将所有顶级菜单从 "待用菜单列表" 中删除
        all.removeAll(roots);
        for (MenuTreeVO menuTreeVO : roots) {
            menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));;
        }
        return roots;
    }
    /**
     * 递归函数
     *      递归目的: 拿到子节点
     *      递归终止条件: 没有子节点
     * @param parent 父节点
     * @return  子节点
     */
    private static List getCurrentNodeChildren(MenuTreeVO parent) {
        // 判断当前节点有没有子节点, 没有则创建一个空长度的 List, 有就使用之前已有的所有子节点.
        List childList = parent.getChildren() == null ? new ArrayList<>() : parent.getChildren();
        // 从 "待用菜单列表" 中找到当前节点的所有子节点
        for (MenuTreeVO child : all) {
            if (parent.getMenuId().equals(child.getParentId())) {
                childList.add(child);
            }
        }
        // 将当前节点的所有子节点从 "待用菜单列表" 中删除
        all.removeAll(childList);
        // 所有的子节点再寻找它们自己的子节点
        for (MenuTreeVO menuTreeVO : childList) {
            menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));
        }
        return childList;
    }
}

调用方法:

// 从数据库获取
List menus = xxxMapper.selectXXX();
// Menu 转为 MenuTreeVO
List menuTreeVOS = new ArrayList<>();
for (Menu menu : menus) {
    MenuTreeVO menuTreeVO = new MenuTreeVO();
    BeanUtils.copyProperties(menu, menuTreeVO);
    menuTreeVOS.add(menuTreeVO);
}
// 调用转换方法
xxxUtil.toTree(menuTreeVOS);
// 通过 Json 或 ModelAndView 返回给前台.

附:模板引擎渲染

有时我们会使用模板引擎来渲染菜单,但由于菜单是树形结构,单纯使用模板引擎中的for并不能完成无限极菜单的渲染。

这里有一个很新颖的方法,我以thymeleaf引擎为例:

index.html 的导航部分:

public.html公共模板部分:


    
  • 系统管理
  • 基本逻辑是使用include来引用模板。 各种模板引擎都有这个功能,然后判断当前节点是否有子节点。 如果是,则模板文件引用自身来完成递归。

    结论

    上面的代码是开发Shiro权限管理后端时的一些想法和代码。 完整代码请参考:/zhaojun1998...

    文章来源:https://juejin.cn/post/6844903715648831495