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组件文件结构