無限級菜單/權限樹該如何設計

前言

在開發中我們經常會遇到:導航菜單、部門菜單、權限樹、評論等功能。

這些功能都有共同的特點:

  1. 有父子關係
  2. 可無限遞歸

我們以導航菜單爲例, 我們將導航菜單設置爲動態的, 即從動態加載菜單數據。

數據庫設計

適用於數據庫存儲的設計如下:

create table `menus`
(
  `id` int primary key auto_increment,
  `name` varchar(20) comment '菜單名稱',
  `pid` int default 0 comment '父級 ID, 最頂級爲 0',
  `order` int comment '排序, 序號越大, 越靠前'
)

前端渲染

對於前端來說, 我們一般需要這種效果:

菜單配置頁面:

對應的導航菜單:

常用的樹形顯示插件有: JsTree, zTree, 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<Menu> menus = xxxMapper.selectXXX();

然後我們需要將這個 List 轉換爲樹形結構, 首先定義一個樹形結構的 VO 類:

public class MenuTreeVO {
    private int id,
    private String name,
    private int pid,
    private List<MenuVo> 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<MenuTreeVO> all = null;

    /**
     * 轉換爲樹形
     * @param list 所有節點
     * @return 轉換後的樹結構菜單
     */
    public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) {
        // 最初, 所有的 "菜單" 都是待用的
        all = new ArrayList<>(list);

        // 拿到所有的頂級 "菜單"
        List<MenuTreeVO> 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<MenuTreeVO> getCurrentNodeChildren(MenuTreeVO parent) {
        // 判斷當前節點有沒有子節點, 沒有則創建一個空長度的 List, 有就使用之前已有的所有子節點.
        List<MenuTreeVO> 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<Menu> menus = xxxMapper.selectXXX();

// Menu 轉爲 MenuTreeVO
List<MenuTreeVO> 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 的導航部分:

<div class="left-nav">
    <div id="side-nav">
        <ul id="nav">
            <th:block th:include="public::menu(${menus})"/>
        </ul>
    </div>
</div>

public.html 公共模板部分:

<th:block th:fragment="menu(menus)">
    <li th:each="menu:${menus}">
        <a href="javascript:;">
            <i class="iconfont">&#xe6b8;</i>
            <cite th:text="${menu.menuName}">系統管理</cite>
            <i class="iconfont nav_right">&#xe697;</i>
        </a>
        <ul class="sub-menu">
            <li th:each="child:${menu.children}">
                <a th:if="${#lists.isEmpty(child.children)}" data-th-_href="${child.url}" _href="users">
                    <i class="iconfont">&#xe6a7;</i>
                    <cite th:text="${child.menuName}">用戶管理</cite>
                </a>
                <th:block th:unless="${#lists.isEmpty(child.children)}" th:include="this::menu(${child})" />
            </li>
        </ul>
    </li>
</th:block>

基本邏輯就是使用 include 引用模板, 各種模板引擎都有這種功能, 然後判斷當前節點有沒有子節點, 有的話, 模板文件引用自身, 來完成遞歸.

結語

上述代碼是在開發一個 Shiro 的權限管理後臺的時候的一些思路和代碼, 完整的代碼可以參考: https://github.com/zhaojun1998/Shiro-Action

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章