二級動態菜單的實現, 我們可能需要一個 下方展示的這樣的一種數據結構:
{ 1: { 'title': '用戶管理', 'icon': 'fa fa-envira', 'children': [ {'title': '客戶列表', 'url': '/customer/list/'} ] }, 2: { 'title': '信息管理', 'icon': 'fa-black-tie', 'children': [ {'title': '賬單列表', 'url': '/payment/list/'} ] } }
OK 如何實現:
1. 數據庫中 我們新添加一張表吧。 就叫 Menu 表。 一級菜單表:
我們通過 一級菜單表, 來確定我們的 一級菜單應該有的樣子

class Menu(models.Model): mid = models.AutoField(primary_key=True) title = models.CharField(verbose_name='一級菜單標題', max_length=32) icon = models.CharField(verbose_name="圖標", max_length=32, null=True, blank=True) def __str__(self): return self.title
然后他長這個樣子:
so 一級菜單中,有 title 字段,表示菜單的名稱。 icon字段,表示這個菜單的圖標。
然后就是 一級菜單下的子菜單, 我們就修改一下 Permission表, 給他添加一個 ment 的外鍵。用來表示這個 url 可以被作為子菜單使用。
這樣 就不需要,原來的表中 is_menu 表示是否可以成為菜單的選項了, 我們刪掉他:

class Permission(models.Model): """ 權限表 二級菜單使用表 """ title = models.CharField(verbose_name='標題', max_length=32) url = models.CharField(verbose_name='含正則的URL', max_length=128) menu = models.ForeignKey(verbose_name="所屬菜單", to="Menu", null=True, blank=True, on_delete=models.CASCADE) def __str__(self): return self.title
這樣我們就只需要,為 可以成為子菜單的 url進行,關聯屬於哪一個。一級菜單。
大概的樣子就是這樣:
如果喜歡也可以,給子菜單添加icon。 為他也添加一個圖標。
2. 數據庫方面, 這就妥了。然后是 用戶登錄的時候。 我們要怎樣 進行數據結構的編寫了。就是 init_permission 初始化權限的階段。 我們就要把 最初說的數據結構保存到session中。

def init_permission(current_user, request): ''' 二級菜單,實現 :param current_user: 當前請求 用戶對象 :param request: 當前請求 數據 :return: ''' # 2. 權限 初始化 # 根據當前用戶信息,獲取當前用戶所擁有的所有的權限(queryset對象 是不能直接放入,session中的) permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__url","permissions__title","permissions__menu_id","permissions__menu__title", "permissions__menu__icon", ).distinct() # 獲取權限 和 菜單信息。 權限放在權限列表,菜單放在菜單列表 menu_dict = {} # 菜單我們就需要一個字典了 permission_list = [] # 所有的權限好說,還是放到列表中 for item in permission_queryset: permission_list.append(item.get("permissions__url")) menu_id = item.get("permissions__menu_id") #先取得當前這個url的父菜單的id if not menu_id: # 如果為null的話,直接跳過。 說明這不是一個子菜單,也就不需要后續的操作了。 continue node = {"title": item.get("permissions__title"), "url": item.get("permissions__url")} # 獲取到當前這個url的信息 if menu_id in menu_dict: menu_dict[menu_id]["children"].append(node) else: menu_dict[menu_id] = { "title": item.get("permissions__menu__title"), "icon": item.get("permissions__menu__icon"), "children": [node] } # 下面的解釋,有一點復雜: 如果當前這個url 的menu_id 在 menu_dict字典中。那么就將當前這條url 添加到一級菜單的 "children" 列表中。也就是node # 否則,就應該 以當前這條url 的menu_id為鍵。其中保存 一級菜單的title 和 icon字段信息。 並且"children"為鍵的列表中也要保存 node 這個參數代表的數據. # 最終我們就能得到,最初我們想要的 數據結構 request.session[settings.PERMISSIONS_SESSION_KEY] = permission_list request.session[settings.MENU_SESSION_KEY] = menu_dict
3. ok 數據結構已經 搞定。 就剩下 改怎么渲染的問題了! 還是使用 inclusion_tag 進行渲染:
考慮到,字典是一個 無序的,存儲容器。 那么我們將他轉化成有序字典 就好了呀!
這里 需要使用到 re模塊。 和OrderedDict模塊

from django.template import Library from django.conf import settings import re from collections import OrderedDict @register.inclusion_tag("rbac/multi_menu.html") def multi_menu(request): ''' 創建二級菜單 :return: ''' ''' { 1: { 'title': '用戶管理', 'icon': 'fa fa-envira', 'children': [ {'title': '客戶列表', 'url': '/customer/list/'} ] }, 2: { 'title': '信息管理', 'icon': 'fa-black-tie', 'children': [ {'title': '賬單列表', 'url': '/payment/list/'} ] } } 我們可以對字典進行排序, 構成一個有序字典 ''' path_info = request.path_info menu_dict = request.session.get(settings.MENU_SESSION_KEY) # 取出數據還是一樣的操作 key_list = sorted(menu_dict) # 對字典的key 進行排序 ordered_dict = OrderedDict() # 創建一個空的 有序字典 for key in key_list: # 循環根據key 排序之后的大字典 val = menu_dict[key] # 得到其中的每一個小字典 val["class"] = "hide" # 添加一個 class:hide 鍵值. 控制標簽的顯示隱藏 for per in val["children"]: # 循環 當前字典(菜單) 下的 children 子菜單 regex = "^%s$" % per["url"] # 每一個子菜單中的url 添加 起始和終止符。 進行嚴格的匹配。 if re.match(regex, request.path_info): # 與當前訪問的url:request.path_info 進行匹配 per["class"] = "active" # 匹配成功 為當前子菜單添加 class:active 類屬性(表示被選中的) val["class"] = "" # 當前一級菜單的 class:"" 跟改為空, 不hide(不隱藏) ordered_dict[key] = val # 最終將數據組織好的每一個小字典。 添加到有序的大字典當中 return {"ordered_dict": ordered_dict}

a = OrderedDict( [ ('1', { 'title': '用戶管理', 'icon': 'fa-envira', 'children': [ { 'title': '客戶列表', 'url': '/customer/list/', 'class': 'active' } ], 'class': '' } ), ('2', { 'title': '信息管理', 'icon': 'fa-black-tie', 'children': [ { 'title': '賬單列表', 'url': '/payment/list/' } ], 'class': 'hide' } ) ] )
再看看 我們的這個 multi_menu.html 要怎么搞:
{#二級菜單#} <div class="multi-menu"> {% for item in ordered_dict.values %} <div class="item"> <a class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>{{ item.title }}</a> <div class="body {{ item.class }}"> {% for per in item.children %} <a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a> {% endfor %} </div> </div> {% endfor %} </div>
使用了 兩層 for 循環來做這件事情!
{{ item.class }} 這個變量就是 一級菜單中保存的 class鍵對應的 值。 用來控制 這個一級菜單下的 子菜單得,顯示和隱藏;
{{ per.class }} 這個變量是循環 item.children 列表中,每個子菜單字典中,保存的 'class': 'active' , 用來表示當前這個子標簽,是被選中的狀態。
(怎么確定的呢? 通過request.path_info 這個是發送到服務端的 當前訪問的url。 我們和用戶所有的權限, 使用re模塊進行匹配時,確定的。 只有匹配成功的才會添加這個 鍵值對)
到這里基本功能已經實現了; 還卻一些。 比如我想要 的效果是: 當我點擊一個 一級菜單時,其余的一級菜單 全部收起.(也就是為除點擊菜單之外的 其余菜單下的 class="body" 的這個div添加上hide)
我們需要的就是 javascript 的東西了。 用 jQuery 來搞吧:
(function (jq) { jq('.multi-menu .title').click(function () { $(this).next().removeClass("hide"); $(this).parent().siblings().children(".body").addClass('hide'); }); })(jQuery);
點擊時 當前這個一級菜單,刪除 hide。 使用js中的 排他思想,為除我之外其他兄弟,全部加上hide。
當然,我們應該,把這些所有的代碼,都添加到。我的 rbac 組件當中。 其余地方使用時,只需要引用就可以。
在我們的 BASE.html 中添加,就可以所有的 繼承 都使用到: 頭部引入 css 的文件 <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %} "/> 在中間需要的地方, 使用我們的自定義,模板語法: {% load rbac_tags %} // 這一句放到,文件的首行或者首行下面就可以 <div class="left-menu"> <div class="menu-body"> {% multi_menu request %} </div> </div> 在 文件的最下方引入 js 文件: <script src="{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="{% static 'rbac/js/rbac.js' %} "></script>