rbac组件之动态二级菜单栏实现
对于功能比较少的应用程序 “一级菜单” 基本可以满足需求,但是功能多的程序就需要 “二级菜单” 了,并且访问时候需要默认选中指定菜单。
示例效果:
基于一级菜单的表结构上进行表结构的更改,Permission权限表中把is_menu和icon两个字段删除,新增一张Menu表,这张表记录一级菜单,Menu表字段为title和icon两个字段,因为其不需要URL,只是展示二级菜单而已,二级菜单需要URL;而这两张表需要建立一对多的关系,一级菜单会有多个二级菜单,所以外键建在Permission表中。
models.py
from django.db import models # Create your models here. class Menu(models.Model): """一级菜单表""" title = models.CharField( verbose_name="一级菜单名称", max_length=32, ) icon = models.CharField( verbose_name="图标", max_length=64, null=True, # 表示数据库中可以为空 blank=True, # admin后台管理中可以输入为空 ) def __str__(self): return self.title class Permission(models.Model): """权限表""" title = models.CharField( verbose_name="权限名称", max_length=32, ) url = models.CharField( verbose_name="含正则URL", max_length=128, ) menu = models.ForeignKey( to="Menu", on_delete=models.CASCADE, verbose_name="所属二级菜单", null=True, # 并不是所有的url都可以做二级菜单,所以这里需要设置null=True blank=True, help_text="null表示不是二级菜单,非null表示是二级菜单", ) def __str__(self): return self.title class Role(models.Model): """角色表""" title = models.CharField( verbose_name="角色名称", max_length=32, ) permissions = models.ManyToManyField( verbose_name="角色所拥有的权限", to="Permission", blank=True, ) def __str__(self): return self.title class Userinfo(models.Model): """用户表""" name = models.CharField( verbose_name="用户名", max_length=32, ) password = models.CharField( verbose_name="密码", max_length=64, ) email = models.CharField( verbose_name="邮箱", max_length=32, ) roles = models.ManyToManyField( verbose_name="用户所拥有的角色", to="Role", blank=True, ) def __str__(self): return self.name
因为表结构发生了变化,所以在权限初始化的步骤上也需要进行更改,即init_permission函数
from django.conf import settings def init_permission(current_user, request): permission_queryset = current_user.roles.filter( permissions__isnull=False, ).values( "permissions__id", # 二级菜单id "permissions__url", # 二级菜单url "permissions__title", # 二级菜单名称 "permissions__menu__id", # 一级菜单ID "permissions__menu__title", # 一级菜单名称 "permissions__menu__icon", # 一级菜单图标 ).distinct() permission_list = [] # 存放用户的权限数据信息 menu_dict = {} # 存放菜单信息 for item in permission_queryset: permission_list.append(item["permissions__url"]) # 将所有url存放到permission_list中,方便中间件的权限判断 menu_id = item["permissions__menu__id"] # 取到二级菜单所对应的一级菜单的id,如果不能做二级菜单的,那么这个字段值是null if not menu_id: continue node = { "title": item["permissions__title"], "url": item["permissions__url"], } # 二级菜单数据信息,方便后续添加 if menu_id in menu_dict: menu_dict[menu_id]["children"].append(node) # 如果一级菜单已经存在,那么直接添加二级菜单数据信息即可 else: # 如果一级菜单不存在,那么还需要添加一级菜单的数据信息和二级菜单的数据信息 menu_dict[menu_id] = { "title": item["permissions__menu__title"], "icon": item["permissions__menu__icon"], "children": [node,], } request.session[settings.PERMISSION_SESSION_KEY] = permission_list request.session[settings.MENU_SESSION_KEY] = menu_dict """ menu_dict = { 1: { 'title': '信息管理', # 一级菜单名称 'icon': 'fa-camera-retro', # 一级菜单图标 'children': [ { 'title': '客户列表', 'url': '/customer/list/' } ] # 一级菜单下的所有二级菜单都在这个列表,每个二级菜单都是一个字典结构,存储了二级菜单的名称和url }, 2: { 'title': '用户管理', 'icon': 'fa-fire', 'children': [ { 'title': '账单列表', 'url': '/payment/list/' } ] } } """
菜单栏数据信息结构发生了变化, 所以在前端渲染时也需要对其进行更改,之前一级菜单栏渲染时,我们做了一个inclusion_tag,所以直接对inclusion_tag更改即可
from django.template import Library from django.conf import settings import re from collections import OrderedDict register = Library() @register.inclusion_tag("rbac/menu.html") def menu(request): menu_dict = request.session.get(settings.MENU_SESSION_KEY) # 从session中取出信息 key_list = sorted(menu_dict) # 对字典的key进行排序 ordered_dict = OrderedDict() # 建立一个有序空字典(按存入顺序排序,先存入的在前面) for key in key_list: val = menu_dict[key] # 取到一级菜单的所有数据信息 val["class"] = "hide" # 添加一个class键,值为hide;这个class属性是二级菜单引用的,并不是一级菜单使用;在前端的效果为所有二级菜单class都有hide属性值,即隐藏所有二级菜单 for per in val["children"]: # 循环当前一级菜单下的每个二级菜单 regex = "^%s$" % per["url"] if re.match(regex, request.path_info): # 如果当前访问的url与二级菜单匹配成功 per["class"] = "active" # 为匹配成功的二级菜单的a标签添加一个class键,值为active;在前端的效果为此二级菜单为激活的状态,即被选中的效果 val["class"] = "" # 把匹配成功的二级菜单的直属一级菜单,class键的值改为空,即这个二级菜单的class没有了hide属性值,也就会展开显示所有二级菜单 ordered_dict[key] = val # 将更改一级菜单所有数据信息,根据key和val存放到有序空字典中 return {"menu_dict": ordered_dict} """ menu_dict = { 1: { 'title': '信息管理', # 一级菜单名称 'icon': 'fa-camera-retro', # 一级菜单图标 'children': [ { 'title': '客户列表', 'url': '/customer/list/' } ] # 一级菜单下的所有二级菜单都在这个列表,每个二级菜单都是一个字典结构,存储了二级菜单的名称和url }, 2: { 'title': '用户管理', 'icon': 'fa-fire', 'children': [ { 'title': '账单列表', 'url': '/payment/list/' } ] } } """
inclusion_tag返回的html页面也需要进行更改(menu.html)
<div class="multi-menu"> {% for item in menu_dict.values %} <div class="item"> <div class="title"> {# 这个div是一级菜单 #} <span class="icon-wrap"> <i class="fa {{ item.icon }}">i> span> {{ item.title }} div> <div class="body {{ item.class }}"> {# 这个div是二级菜单,二级菜单引用了后端为一级菜单设置的class属性值,即hide属性 #} {% for children in item.children %} <a href="{{ children.url }}" class="{{ children.class }}"> {{ children.title }} a> {% endfor %} div> div> {% endfor %} div>
此时,rbac组件已经更改完毕,已经支持二级菜单的显示了;因为前端页面的菜单栏展示发生了变化,所以菜单栏的css样式也还需要更改(多了个二级菜单);根据后端书写的逻辑,现在二级菜单的点击效果是:当点击一个二级菜单后,除了当前的一级菜单是展开显示外,其余的一级菜单都是隐藏的;如果想更改其效果,后端的这一段逻辑可以删除,自行编写js即可;为了解耦,把二级菜单的css样式和js代码都放入到rbac组件static文件夹下。
二级菜单栏的css,部分与一级菜单栏的css重叠了,所以在一级菜单栏的layout.html的样式设计中需要删除一些样式设计,从.luffy-container以下的所有样式删除,且样式和js解耦后,再需要使用那么需要引入两个文件。
<link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %}"> <script src="{% static 'rbac/js/rbac.js' %}">script>
前端的inclusion_tag调用和一级菜单一样,不需要做任何更改
rbac组件文件结构