动态菜单

介绍

主要为一级菜单和二级菜单两种。根据用户的不同看到的菜单是不一样的。

一级菜单

arcodesign 动态 菜单_django

思路

思考:菜单和权限的关系

权限就是url,点击一个菜单是发送一个url到后端请求内容,也就是说菜单的本质是url。
但是并不是所有的url都可以做菜单,由管理员动态来决定哪个url可以作为权限,需要在
权限表中新增一列来设定该url是否可以作为菜单。

实现步骤:

  1. 权限表中新增一列来辨识url是否可以作为菜单,录入菜单信息
  2. 用户登录后,拿到可以作为菜单的url存储到session中
  3. 在访问有菜单的界面时,动态从session中拿到菜单列表,进行渲染

实现

a.表结构修改

class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    is_menu = models.BooleanField(verbose_name="是否可以做菜单", default=False)
    icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)

b.进入admin设定菜单信息

图标可以从 https://fontawesome.dashgame.com 中拿。下载引入css文件。

icon中的值为图标的class值

arcodesign 动态 菜单_二级菜单_02


c.拿到菜单存入session

init_permision.py文件

from django.conf import settings

def init_permission(current_user, request):
    '''
    初始化权限信息
    :param current_user: 当前登录用户
    :param request: 请求相关参数
    :return:
    '''
    # 当前用户所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__url",
        "permissions__title",
        "permissions__is_menu",
        "permissions__icon").distinct()

    # 获取权限中的url + 菜单信息
    menu_list = []
    permission_list = []
    for item in permission_queryset:
        permission_list.append(item.get('permissions__url'))
        if item.get('permissions__is_menu'):
            temp = {
                'title': item.get('permissions__title'),
                'url': item.get('permissions__url'),
                'icon': item.get('permissions__icon')
            }
            menu_list.append(temp)

    # 放入session中
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list
    request.session[settings.MENU_SESSION_KEY] = menu_list

settings.py文件
MENU_SESSION_KEY = 'permission_menu_list'

d.菜单显示

使用到inclusion_tag知识点,在web app中只要使用一句话就可以动态生成菜单。

新建文件:

arcodesign 动态 菜单_html_03


rbac,py

from django.template import Library
from django.conf import settings

register = Library()

@register.inclusion_tag('rbac/static_menu.html')
def static_menu(request):
    """
    创建一级菜单
    :return:
    """
    menu_list = request.session.get(settings.MENU_SESSION_KEY)
    print(menu_list)

    return {'menu_list': menu_list}

static_menu.html

<div class="static-menu">
	{% for item in menu_list %}
		<a href="{{ item.url }}">
		<span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title}}</a>
	{% endfor %}
</div>

使用一行代码生成动态菜单:

{% load rbac %}
{% static_menu request %}

二级菜单

arcodesign 动态 菜单_html_04

思路

一级菜单没有网页跳转功能,只是对二级菜单进行展示。需要将二级菜单和一级菜单进行关联起来进行显示,点击一级菜单能展示该一级菜单下的所有二级菜单。

{
        'title': '信息管理',  # 一级菜单名称
        'icon': 'xxx',
        'children': [  # 二级菜单信息
            {
                'title':'客户列表',
                'url':'/customer/list/',
            },
            {
                'title': '账单列表',
                'url': '/account/list/'
            }
        ]
    }

需要新建一个一级菜单表,一级菜单和二级菜单之间是一对多的关系,同时需要修改表结构。

class Menu(models.Model):
    """
    一级菜单表
    """
    title = models.CharField(verbose_name='一级菜单名称', max_length=32)
    icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)

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", verbose_name="所属菜单", null=True, blank=True, help_text="null表示不是菜单,非null表示是二级菜单")

    def __str__(self):
        return self.title

将菜单功能使用inclusion_tag来进行显示。

实现

1.用户登录后数据存储在session中
init_permission.py

from django.conf import settings
from rbac.models import Menu

def init_permission(current_user, request):
    '''
    初始化权限信息
    :param current_user: 当前登录用户
    :param request: 请求相关参数
    :return:
    '''
    # 当前用户所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__url",
        "permissions__title",
        "permissions__menu__id",
        "permissions__menu__title",
        "permissions__menu__icon").distinct()

    # 获取权限中的url + 菜单信息
    menu_dict = {}
    permission_list = []
    for item in permission_queryset:
        permission_list.append(item.get('permissions__url'))
        menu_id = item.get('permissions__menu__id')
        if not menu_id:
            continue

        node = {'title': item.get('permissions__title'), 'url': item.get("permissions__url")}
        if menu_id not in menu_dict.keys():
            menu_dict[menu_id] = {
                'title': item.get('permissions__menu__title'),
                'icon': item.get('permissions__menu__icon'),
                'children': [node]
            }
        else:
            menu_dict[menu_id]['children'].append(node)

    print(menu_dict)
    # 放入session中
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list
    request.session[settings.MENU_SESSION_KEY] = menu_dict

2.inclusion_tag实现二级菜单,两层循环展示一级和二级菜单。

@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :param request:
    :return:
    """
    menu_dict = request.session.get(settings.MENU_SESSION_KEY)

    # 字典排序, 从session中拿到的是一个字典,他的顺序是不确定的,会导致菜单显示顺序偶尔不一致,要解决这个问题需要对拿出来的字典进行排序
    key_list = sorted(menu_dict)

    ordered_dict = OrderedDict()
    for key in key_list:
        ordered_dict[key] = menu_dict[key]

    return {"menu_dict": ordered_dict}

multi_menu.html

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
            <div class="body">
                {% for per in item.children %}
                    <a href="{{ per.url }}">{{ per.title }}</a>
                {% endfor %}
            </div>

        </div>
    {% endfor %}
</div>

样式代码,二级菜单的样式

.multi-menu .item {
    background-color: white;
}

.multi-menu .item > .title {
    padding: 10px 5px;
    border-bottom: 1px solid #dddddd;
    cursor: pointer;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0 1px 1px white;
}

.multi-menu .item > .body {
    border-bottom: 1px solid #dddddd;
}

.multi-menu .item > .body a {
    display: block;
    padding: 5px 20px;
    text-decoration: none;
    border-left: 2px solid transparent;
    font-size: 13px;

}

.multi-menu .item > .body a:hover {
    border-left: 2px solid #2F72AB;
}

.multi-menu .item > .body a.active {
    border-left: 2px solid #2F72AB;
}

js代码, 实现点击一级菜单显示和隐藏二级菜单
toggleClass:该方法检查每个元素中指定的类。如果不存在则添加类,如果已设置则删除之。这就是所谓的切换效果。

(function (jq) {
    jq('.multi-menu .title').click(function () {
        $(this).next().toggleClass('hide');
    });
})(jQuery);

效果展示:

一开始,所有二级菜单和一级菜单都会显示,点击一级菜单可以收起和展示一级菜单。但是初始的展示不应该是所有的菜单都展示出来,后续进行改进。

arcodesign 动态 菜单_html_05

二级菜单改进

改进1

用户访问某个级菜单url时,显示这个菜单并处于激活状态,其所属一级菜单下的所有二级菜单均展示,其他一级菜单不展示二级菜单。

思想:一级菜单是一个class=title的div,二级菜单都放在class=body的div,在用户访问某个二级菜单(对应url),将其他一级菜单下的所有二级菜单的class添加hide进行隐藏,将被访问的二级菜单class添加active显示为激活,将当前二级菜单所属的div移除hide属性值,显示所有二级标签。

inclusion_tag代码

@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :param request:
    :return:
    """
    menu_dict = request.session.get(settings.MENU_SESSION_KEY)

    # 字典排序, 从session中拿到的是一个字典,他的顺序是不确定的,会导致菜单显示顺序偶尔不一致,要解决这个问题需要对拿出来的字典进行排序
    key_list = sorted(menu_dict)

    ordered_dict = OrderedDict()
    for key in key_list:
        val = menu_dict[key]
        val['class'] = 'hide'  # 用来设置,二级菜单默认的class样式为hide,不显示
        for per in val['children']:
            regex = "^{}$".format(per['url'])
            if re.match(regex, request.path_info):
                per['class'] = 'active'  # 判断是当前url对应的二级菜单,将二级菜单显示为激活状态
                val['class'] = ''  # 某个一级菜单下,有一个是激活状态,将二级菜单默认class样式置空,即能显示其他二级菜单
        ordered_dict[key] = val

    return {"menu_dict": ordered_dict}

multi_menu.html

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
            <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>

效果:

arcodesign 动态 菜单_django_06

改进2

当我们访问非二级菜单的url时,如添加客户、删除客户等,我们的左侧菜单栏全部是没有展示二级菜单,没有默认菜单被选中,显然这不符合我们的期望。我们的期是点击非菜单的权限时,默认选中或者展开二级菜单。

思路:当点击某个不能成为菜单的权限时,指定一个可以成为菜单的权限让其默认选中并展开。需要对不是二级菜单的权限做一个归属,将可以成为菜单的二级菜单权限与不能成为二级菜单的权限做一个关联,让一张表中的某几行数据和某几行数据做关联,就是自关联实现。

1.表结构修改,添加子关联字段,必须添加relate_name 参数

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", verbose_name="所属菜单", null=True, blank=True, help_text="null表示不是菜单,非null表示是二级菜单")

    pid = models.ForeignKey(verbose_name="关联的权限",
                            help_text="对于非菜单权限选择一个可以成为菜单的权限,用于做默认展开和选中在访问非菜单功能时",
                            to="Permission",
                            null=True,
                            blank=True,
                            related_name="parents")

    def __str__(self):
        return self.title

2.后台管理手动进行关联
3.修改代码支持功能
权限在session中的存储结构修改:

[
        {
            'id': 1,
            'url': '/customer/list/',
            'pid': null
        },
        {
            'id': 2,
            'url': '/customer/list/',
            'pid': 1
        }
    ]

传入inclusion_tag中的结构修改:

{
    1: {
        'title': '信息管理',  # 一级菜单名称
        'icon': 'xxx',
        'class': '',
        'children': [  # 二级菜单信息
            {
                'id': 1,
                'title':'客户列表',
                'url':'/customer/list/',
            },
            {
                'id': 7,
                'title': '账单列表',
                'url': '/account/list/'
            }
        ]
    }
}

在中间件中对权限进行校验,拿到当前url的id或者pid(id的话就是可以做菜单的url,pid的话就是当前url不能做菜单,需要绑定一个菜单的id),将可以做菜单的url传入inclusion_tag中,作为判断显示哪个菜单的依据。

代码:
init_permission.py

def init_permission(current_user, request):
    '''
    初始化权限信息
    :param current_user: 当前登录用户
    :param request: 请求相关参数
    :return:
    '''
    # 当前用户所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values(
        "permissions__id",
        "permissions__url",
        "permissions__title",
        "permissions__pid",
        "permissions__menu__id",
        "permissions__menu__title",
        "permissions__menu__icon").distinct()

    # 获取权限中的url + 菜单信息
    menu_dict = {}
    permission_list = []
    for item in permission_queryset:
        permission_list.append({
            'id': item.get('permissions__id'),
            'url': item.get('permissions__url'),
            'pid': item.get('permissions__pid')
        })

        menu_id = item.get('permissions__menu__id')
        if not menu_id:
            continue

        node = {'title': item.get('permissions__title'), 'url': item.get("permissions__url"), 'id': item.get('permissions__id')}
        if menu_id not in menu_dict.keys():
            menu_dict[menu_id] = {
                'title': item.get('permissions__menu__title'),
                'icon': item.get('permissions__menu__icon'),
                'children': [node]
            }
        else:
            menu_dict[menu_id]['children'].append(node)

    print(menu_dict)
    # 放入session中
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list
    request.session[settings.MENU_SESSION_KEY] = menu_dict

mindwares/rbac.py

class RbacMiddleware(MiddlewareMixin):
    def process_request(self, request):
        '''
        1. 拿到当前用户请求的url
        2. 获取当前用户在session中保存的权限列表
        3. 权限信息匹配
        '''
        current_url = request.path_info
        # 有一些权限是所有人默认都有的,不需要做权限判断,先进行一个白名单判断,如果是白名单url,就不用再走权限判断了
        valid_url_list = settings.VALID_URL_LIST
        for valid_url in valid_url_list:
            if re.match(valid_url, current_url):
                # 白名单中的url,无需权限验证
                # 返回None,继续走后续步骤
                return None

        permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
        if not permission_list:
            # 返回 HttpResponse 不走 后续步骤,直接返回到页面
            return HttpResponse('为获取到用户权限信息')

        print(current_url)
        print(permission_list)

        flag = False
        for item in permission_list:
            regx = '^{}$'.format(item.get('url'))
            if re.match(regx, current_url):
                flag = True
                request.current_selected_permission = item.get('pid') or item.get('id')
                break

        if not flag:
            return HttpResponse('无权访问')

temlatetags/rbac.py

@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :param request:
    :return:
    """
    menu_dict = request.session.get(settings.MENU_SESSION_KEY)

    # 字典排序, 从session中拿到的是一个字典,他的顺序是不确定的,会导致菜单显示顺序偶尔不一致,要解决这个问题需要对拿出来的字典进行排序
    key_list = sorted(menu_dict)

    ordered_dict = OrderedDict()
    for key in key_list:
        val = menu_dict[key]
        val['class'] = 'hide'  # 用来设置,二级菜单默认的class样式为hide,不显示
        for per in val['children']:

            if per.get('id') == request.current_selected_permission:
                per['class'] = 'active'  # 判断是当前url对应的二级菜单,将二级菜单显示为激活状态
                val['class'] = ''  # 某个一级菜单下,有一个是激活状态,将二级菜单默认class样式置空,即能显示其他二级菜单
        ordered_dict[key] = val

    return {"menu_dict": ordered_dict}

效果展示:

arcodesign 动态 菜单_django_07

动态菜单路径导航

arcodesign 动态 菜单_二级菜单_08


上图所示就是路径导航,可以知道菜单之间的层级关系。

思路:路径就是从当前url出发,找到父亲url,再到首页,我们的层级关系已经在Permission权限表中体现出来了,将拿到的层级关系放到session中,在集成到rbac模块的insludion_tag中就可以使用了。
1.在中间件检测权限时,拿到当前菜单的层级关系,放入到session中
2.在rbac中实现inclusion_tag
3.web app中使用inclusion_tag显示菜单路径导航
实现:
mindwares/rbac.py

class RbacMiddleware(MiddlewareMixin):
    def process_request(self, request):
        '''
        1. 拿到当前用户请求的url
        2. 获取当前用户在session中保存的权限列表
        3. 权限信息匹配
        '''
        current_url = request.path_info
        # 有一些权限是所有人默认都有的,不需要做权限判断,先进行一个白名单判断,如果是白名单url,就不用再走权限判断了
        valid_url_list = settings.VALID_URL_LIST
        for valid_url in valid_url_list:
            if re.match(valid_url, current_url):
                # 白名单中的url,无需权限验证
                # 返回None,继续走后续步骤
                return None

        permission_list = request.session.get(settings.PERMISSION_SESSION_KEY)
        if not permission_list:
            # 返回 HttpResponse 不走 后续步骤,直接返回到页面
            return HttpResponse('为获取到用户权限信息')

        print(current_url)
        print(permission_list)

        flag = False
        url_record = [
            {'title': '首页', 'url': '#'}
        ]
        for item in permission_list:
            regx = '^{}$'.format(item.get('url'))
            if re.match(regx, current_url):
                flag = True
                request.current_selected_permission = item.get('pid') or item.get('id')
                if item.get('pid'):
                    # 三级路径导航
                    url_record.extend([
                        {'title': item.get('p_title'), 'url': item.get('p_url')},
                        {'title': item.get('title'), 'url': item.get('url'), 'class': 'active'}
                    ])
                else:
                    url_record.extend([{'title': item.get('title'), 'url': item.get('url'), 'class': 'active'}])
                print(url_record)
                break
        request.url_record = url_record

        if not flag:
            return HttpResponse('无权访问')

record_list.html

<div>
	<ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
		{% for item in record_list %}
			{% if item.class %}
				<li class="{{ item.class }}">{{ item.title }}</li>
			{% else %}
				<li><a href="{{ item.url }}">{{ item.title }}</a></li>
			{% endif %}
		{% endfor %}

	</ol>
</div>

使用:

{% load rbac %}
{% record_list request %}

效果:

arcodesign 动态 菜单_ico_09