無(wú)限級(jí)菜單/權(quán)限樹該如何設(shè)計(jì)

前言

在開發(fā)中我們經(jīng)常會(huì)遇到:導(dǎo)航菜單绢掰、部門菜單痒蓬、權(quán)限樹、評(píng)論等功能滴劲。

這些功能都有共同的特點(diǎn):

  1. 有父子關(guān)系
  2. 可無(wú)限遞歸

我們以導(dǎo)航菜單為例, 我們將導(dǎo)航菜單設(shè)置為動(dòng)態(tài)的, 即從動(dòng)態(tài)加載菜單數(shù)據(jù)攻晒。

數(shù)據(jù)庫(kù)設(shè)計(jì)

適用于數(shù)據(jù)庫(kù)存儲(chǔ)的設(shè)計(jì)如下:

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

前端渲染

對(duì)于前端來(lái)說(shuō), 我們一般需要這種效果:

菜單配置頁(yè)面:

image

對(duì)應(yīng)的導(dǎo)航菜單:

image

常用的樹形顯示插件有: JsTree, zTree, Layui Tree, Bootstrap Tree View 等。

這些插件一般需要這兩種格式:

基礎(chǔ)格式:

[
    {
        "id": 1,
        "name": "權(quán)限管理",
        "pid": 0,
        "order": 1
    },
    {
        "id": 2,
        "name": "用戶管理",
        "pid": 1,
        "order": 2
    },
    {
        "id": 3,
        "name": "角色管理",
        "pid": 1,
        "order": 3
    },
    {
        "id": 4,
        "name": "權(quán)限管理",
        "pid": 1,
        "order": 4
    }
]

樹形格式:

[
    {
        "id": 1,
        "name": "權(quán)限管理",
        "pid": 0,
        "order": 1,
        "children": [
            {
                "id": 2,
                "name": "用戶管理",
                "pid": 1,
                "order": 2,
                "children": []
            },
            {
                "id": 3,
                "name": "角色管理",
                "pid": 1,
                "order": 3,
                "children": []
            },
            {
                "id": 4,
                "name": "權(quán)限管理",
                "pid": 1,
                "order": 4,
                "children": []
            }
        ]
    }
]

有的插件這兩種格式都支持, 而有些只支持樹形結(jié)構(gòu), 但我們數(shù)據(jù)庫(kù)查詢出來(lái)的結(jié)果往往又是普通結(jié)構(gòu), 這時(shí)候我們就需要將普通格式轉(zhuǎn)換成樹形格式班挖。

這個(gè)轉(zhuǎn)換一般是在服務(wù)端進(jìn)行(因?yàn)榍岸瞬寮蠖喽际钦?qǐng)求后臺(tái)的一個(gè) URL 來(lái)接收 JSON 數(shù)據(jù), 沒(méi)有提供加載數(shù)據(jù)后 - 渲染前的事件, 所以無(wú)法在前端完成轉(zhuǎn)換.)

數(shù)據(jù)轉(zhuǎn)換

首先有 Java 實(shí)體類:

public class Menu {
    private int id,
    private String name,
    private int pid

    // getter setter 略
}

數(shù)據(jù)庫(kù)查詢后的一般是在 List 中:

List<Menu> menus = xxxMapper.selectXXX();

然后我們需要將這個(gè) List 轉(zhuǎn)換為樹形結(jié)構(gòu), 首先定義一個(gè)樹形結(jié)構(gòu)的 VO 類:

public class MenuTreeVO {
    private int id,
    private String name,
    private int pid,
    private List<MenuVo> children,

    // getter setter 略
}

轉(zhuǎn)換工具類:

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;

    /**
     * 轉(zhuǎn)換為樹形
     * @param list 所有節(jié)點(diǎn)
     * @return 轉(zhuǎn)換后的樹結(jié)構(gòu)菜單
     */
    public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) {
        // 最初, 所有的 "菜單" 都是待用的
        all = new ArrayList<>(list);

        // 拿到所有的頂級(jí) "菜單"
        List<MenuTreeVO> roots = new ArrayList<>();

        for (MenuTreeVO menuTreeVO : list) {
            if (menuTreeVO.getParentId() == 0) {
                roots.add(menuTreeVO);
            }
        }

        // 將所有頂級(jí)菜單從 "待用菜單列表" 中刪除
        all.removeAll(roots);

        for (MenuTreeVO menuTreeVO : roots) {
            menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));;
        }
        return roots;
    }

    /**
     * 遞歸函數(shù)
     *      遞歸目的: 拿到子節(jié)點(diǎn)
     *      遞歸終止條件: 沒(méi)有子節(jié)點(diǎn)
     * @param parent 父節(jié)點(diǎn)
     * @return  子節(jié)點(diǎn)
     */
    private static List<MenuTreeVO> getCurrentNodeChildren(MenuTreeVO parent) {
        // 判斷當(dāng)前節(jié)點(diǎn)有沒(méi)有子節(jié)點(diǎn), 沒(méi)有則創(chuàng)建一個(gè)空長(zhǎng)度的 List, 有就使用之前已有的所有子節(jié)點(diǎn).
        List<MenuTreeVO> childList = parent.getChildren() == null ? new ArrayList<>() : parent.getChildren();

        // 從 "待用菜單列表" 中找到當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)
        for (MenuTreeVO child : all) {
            if (parent.getMenuId().equals(child.getParentId())) {
                childList.add(child);
            }
        }

        // 將當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)從 "待用菜單列表" 中刪除
        all.removeAll(childList);

        // 所有的子節(jié)點(diǎn)再尋找它們自己的子節(jié)點(diǎn)
        for (MenuTreeVO menuTreeVO : childList) {
            menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));
        }
        return childList;
    }
}

調(diào)用方式:

// 從數(shù)據(jù)庫(kù)獲取
List<Menu> menus = xxxMapper.selectXXX();

// Menu 轉(zhuǎn)為 MenuTreeVO
List<MenuTreeVO> menuTreeVOS = new ArrayList<>();
for (Menu menu : menus) {
    MenuTreeVO menuTreeVO = new MenuTreeVO();
    BeanUtils.copyProperties(menu, menuTreeVO);
    menuTreeVOS.add(menuTreeVO);
}

// 調(diào)用轉(zhuǎn)換方法
xxxUtil.toTree(menuTreeVOS);

// 通過(guò) Json 或 ModelAndView 返回給前臺(tái).

附:模板引擎渲染

有時(shí)我們會(huì)使用模板引擎來(lái)渲染菜單, 但由于菜單是樹形結(jié)構(gòu)的, 所以在模板引擎中單純的使用 for 是無(wú)法完成無(wú)限極菜單的渲染的.

這里有一個(gè)很新奇的方法, 我以 thymeleaf 引擎為例:

index.html 的導(dǎo)航部分:

<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}">系統(tǒng)管理</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 引用模板, 各種模板引擎都有這種功能, 然后判斷當(dāng)前節(jié)點(diǎn)有沒(méi)有子節(jié)點(diǎn), 有的話, 模板文件引用自身, 來(lái)完成遞歸.

結(jié)語(yǔ)

上述代碼是在開發(fā)一個(gè) Shiro 的權(quán)限管理后臺(tái)的時(shí)候的一些思路和代碼, 完整的代碼可以參考: https://github.com/zhaojun1998/Shiro-Action

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鲁捏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子萧芙,更是在濱河造成了極大的恐慌给梅,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件双揪,死亡現(xiàn)場(chǎng)離奇詭異动羽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)渔期,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門运吓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人擎场,你說(shuō)我怎么就攤上這事羽德。” “怎么了迅办?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵宅静,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我站欺,道長(zhǎng)姨夹,這世上最難降的妖魔是什么纤垂? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮磷账,結(jié)果婚禮上峭沦,老公的妹妹穿的比我還像新娘。我一直安慰自己逃糟,他們只是感情好吼鱼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绰咽,像睡著了一般菇肃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上取募,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天琐谤,我揣著相機(jī)與錄音,去河邊找鬼玩敏。 笑死斗忌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旺聚。 我是一名探鬼主播织阳,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼翻屈!你這毒婦竟也來(lái)了陈哑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤伸眶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后刽宪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厘贼,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年圣拄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嘴秸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庇谆,死狀恐怖岳掐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饭耳,我是刑警寧澤串述,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站寞肖,受9級(jí)特大地震影響纲酗,放射性物質(zhì)發(fā)生泄漏衰腌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一觅赊、第九天 我趴在偏房一處隱蔽的房頂上張望右蕊。 院中可真熱鬧,春花似錦吮螺、人聲如沸饶囚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坯约。三九已至,卻和暖如春莫鸭,著一層夾襖步出監(jiān)牢的瞬間闹丐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工被因, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留卿拴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓梨与,卻偏偏與公主長(zhǎng)得像堕花,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粥鞋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容