二级动态菜单的实现, 我们可能需要一个 下方展示的这样的一种数据结构:
{ 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模块进行匹配时,确定的。 只有匹配成功的才会添加这个 键值对)
到这里基本功能已经实现了; 还却一些。 比如我想要 的效果是: 当我点击一个 一级菜单时,其余的一级菜单 全部收起.(也就是为除点击菜单之外的 其余菜单下的 的这个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="http://t.zoukankan.com/{% static 'plugins/bootstrap/css/bootstrap.css' %} "/> <link rel="stylesheet" href="http://t.zoukankan.com/{% static 'plugins/font-awesome/css/font-awesome.css' %} "/> <link rel="stylesheet" href="http://t.zoukankan.com/{% static 'css/commons.css' %} "/> <link rel="stylesheet" href="http://t.zoukankan.com/{% static 'css/nav.css' %} "/> <link rel="stylesheet" href="http://t.zoukankan.com/{% static 'rbac/css/rbac.css' %} "/> 在中间需要的地方, 使用我们的自定义,模板语法: {% load rbac_tags %} // 这一句放到,文件的首行或者首行下面就可以 <div class="left-menu"> <div class="menu-body"> {% multi_menu request %} </div> </div> 在 文件的最下方引入 js 文件: <script src="http://t.zoukankan.com/{% static 'js/jquery-3.3.1.min.js' %} "></script> <script src="http://t.zoukankan.com/{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script> <script src="http://t.zoukankan.com/{% static 'rbac/js/rbac.js' %} "></script>