效果图:
一、路由配置
rbac/urls.py
from django.urls import re_path
from rbac.views import menu urlpatterns = [ ... # 菜单管理 re_path(r'^menu/list/$', menu.menu_list, name='menu_list'), re_path(r'^menu/add/$', menu.menu_add, name='menu_add'), re_path(r'^menu/edit/(?P<pk>d+)/$', menu.menu_edit, name='menu_edit'), re_path(r'^menu/del/(?P<pk>d+)/$', menu.menu_del, name='menu_del'), ... ]
二、forms表单验证
rbac/forms/menu.py
from django import forms from django.utils.safestring import mark_safe from rbac import models ICON_LIST = [ ['fa-hand-scissors-o', '<i aria-hidden="true" class="fa fa-hand-scissors-o"></i>'], ['fa-hand-spock-o', '<i aria-hidden="true" class="fa fa-hand-spock-o"></i>'], ['fa-hand-stop-o', '<i aria-hidden="true" class="fa fa-hand-stop-o"></i>'], ['fa-handshake-o', '<i aria-hidden="true" class="fa fa-handshake-o"></i>'], ['fa-hard-of-hearing', '<i aria-hidden="true" class="fa fa-hard-of-hearing"></i>'], ['fa-hashtag', '<i aria-hidden="true" class="fa fa-hashtag"></i>'], ['fa-hdd-o', '<i aria-hidden="true" class="fa fa-hdd-o"></i>'], ['fa-headphones', '<i aria-hidden="true" class="fa fa-headphones"></i>'], ['fa-heart', '<i aria-hidden="true" class="fa fa-heart"></i>'], ['fa-heart-o', '<i aria-hidden="true" class="fa fa-heart-o"></i>'], ['fa-heartbeat', '<i aria-hidden="true" class="fa fa-heartbeat"></i>'], ['fa-history', '<i aria-hidden="true" class="fa fa-history"></i>'], ['fa-home', '<i aria-hidden="true" class="fa fa-home"></i>'], ['fa-hotel', '<i aria-hidden="true" class="fa fa-hotel"></i>'], ['fa-hourglass', '<i aria-hidden="true" class="fa fa-hourglass"></i>'], ['fa-hourglass-1', '<i aria-hidden="true" class="fa fa-hourglass-1"></i>'], ['fa-hourglass-2', '<i aria-hidden="true" class="fa fa-hourglass-2"></i>'], ['fa-hourglass-3', '<i aria-hidden="true" class="fa fa-hourglass-3"></i>'], ['fa-hourglass-end', '<i aria-hidden="true" class="fa fa-hourglass-end"></i>'], ['fa-hourglass-half', '<i aria-hidden="true" class="fa fa-hourglass-half"></i>'], ['fa-hourglass-o', '<i aria-hidden="true" class="fa fa-hourglass-o"></i>'], ['fa-hourglass-start', '<i aria-hidden="true" class="fa fa-hourglass-start"></i>'], ['fa-i-cursor', '<i aria-hidden="true" class="fa fa-i-cursor"></i>'], ['fa-id-badge', '<i aria-hidden="true" class="fa fa-id-badge"></i>'], ['fa-id-card', '<i aria-hidden="true" class="fa fa-id-card"></i>'], ['fa-id-card-o', '<i aria-hidden="true" class="fa fa-id-card-o"></i>'], ['fa-image', '<i aria-hidden="true" class="fa fa-image"></i>'], ['fa-mail-reply-all', '<i aria-hidden="true" class="fa fa-mail-reply-all"></i>'], ['fa-reply', '<i aria-hidden="true" class="fa fa-reply"></i>'], ['fa-reply-all', '<i aria-hidden="true" class="fa fa-reply-all"></i>'], ['fa-retweet', '<i aria-hidden="true" class="fa fa-retweet"></i>'], ['fa-wrench', '<i aria-hidden="true" class="fa fa-wrench"></i>']] """ mark_safe: Explicitly mark a string as safe for (HTML) output purposes. The returned object can be used everywhere a string is appropriate. """ for item in ICON_LIST: item[1] = mark_safe(item[1]) class MenuModelForm(forms.ModelForm): class Meta: model = models.Menu fields = ['title', 'icon'] widgets = { 'title': forms.TextInput(attrs={'class': 'form-control'}), 'icon': forms.RadioSelect( choices=ICON_LIST, attrs={'class': 'clearfix'} ) }
三、视图函数
from django.shortcuts import HttpResponse, render, redirect, reverse from rbac import models from rbac.forms.menu import MenuModelForm from rbac.service.urls import memory_reverse def menu_list(request): """ 菜单和权限列表 :param request: :return: """ menu_queryset = models.Menu.objects.all() menu_id = request.GET.get('mid') context = { 'menu_list': menu_queryset, 'menu_id': menu_id } return render(request, 'rbac/menu_list.html', context) def menu_add(request): """ 菜单和权限列表 :param request: :return: """ if request.method == 'GET': forms = MenuModelForm() return render(request, 'rbac/change.html', {'forms': forms}) forms = MenuModelForm(data=request.POST) if forms.is_valid(): forms.save() url = memory_reverse(request, 'rbac:menu_list') return redirect(url) return render(request, 'rbac/change.html', {'forms': forms}) def menu_edit(request, pk): """ 编辑一级菜单 :param request: :param pk: :return: """ menu_obj = models.Menu.objects.filter(id=pk).first() if not menu_obj: return HttpResponse('菜单不存在') if request.method == 'GET': forms = MenuModelForm(instance=menu_obj) return render(request, 'rbac/change.html', {'forms': forms}) forms = MenuModelForm(data=request.POST, instance=menu_obj) if forms.is_valid(): forms.save() url = memory_reverse(request, 'rbac:menu_list') return redirect(url) return render(request, 'rbac/change.html', {'forms': forms}) def menu_del(request, pk): """ 删除一级菜单 :param request: :param pk: :return: """ menu_list_url = memory_reverse(request, 'rbac:menu_list') if request.method == 'GET': return render(request, 'rbac/delete.html', {'cancel': menu_list_url}) models.Menu.objects.filter(id=pk).delete() return redirect(menu_list_url)
memory_reverse的功能是当用户完成增删改返回列表页的时候,还带有原参数,这样回列表页的时候还会默认选中用户刚刚选中的参数
四、保留原参数
rbac/templatestags/rbac.py
... from django.template import Library from rbac.service import urls register = Library() @register.simple_tag() def memory_url(request, name, *args, **kwargs): """ 生成带有原搜索条件的URL(替代了模板中的url) :param request: :param name: :param args: :param kwargs: :return: """ return urls.memory_url(request, name, *args, **kwargs) ...
rbac/service/urls.py
from django.http import QueryDict from django.shortcuts import reverse def memory_url(request, name, *args, **kwargs): # reverse用法:reverse('name', kwargs={'pk': 1}) # reverse用法:reverse('name', args=(1,)) basic_url = reverse(name, args=args, kwargs=kwargs) # 当前url中无参数 if not request.GET: return basic_url old_params = request.GET.urlencode() # 获取url中的参数 query_dict = QueryDict(mutable=True) # 提供转义功能 query_dict['_filter'] = old_params # urlencode帮我们自动转义。 # 如果不用urlencode,&符号会把这个参数分割成两个参数:_filter=mid=2 和 age=99 return '%s?%s' % (basic_url, query_dict.urlencode()) # _filter=mid=2&age=99 def memory_reverse(request, name, *args, **kwargs): """ 反向生成URL http://127.0.0.1:8000/rbac/menu/edit/1/?_filter=mid%3D4 1. 在URL获取原来的搜索条件获取(filter后的值) 2. reverse生成原来的URL,如:/menu/list/ 3. /menu/list/?mid%3D4 :param request: :param name: :param args: :param kwargs: :return: """ url = reverse(name, args=args, kwargs=kwargs) original_parmas = request.GET.get('_filter') if original_parmas: url = '%s?%s' % (url, original_parmas) return url
由于需要传的参数超过了两个,所以需要用simple_tag。memory_url在模板用的。memory_reverse是在视图函数中反向解析的时候用的。
五、模板
模板层新增了菜单列表的模板,增、改页面由于新增了许多图标,所以也有一些小的变动。
rbac/templates/rbac/menu_list.html
{% extends 'layout.html' %} {% load rbac %} <style> tr.active { border-left: 3px solid #fdc00f; } </style> {% block content %} <div class="luffy-container"> <div class="col-md-3"> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"> <i class="fa fa-book" aria-hidden="true">一级菜单</i> <a href="{% memory_url request 'rbac:menu_add' %}" class="right btn btn-success btn-xs" style="padding: 2px 8px;margin:-3px"> <i class="fa fa-plus-circle" aria-hidden="true">新建</i> </a> </div> <!-- Table --> <table class="table"> <thead> <tr> <th>名称</th> <th>图标</th> <th>选项</th> </tr> </thead> <tbody> {% for menu in menu_list %} <!-- 管道符可以将后端传来的整型,转换成字符串 --> <tr class="{% if menu.id|safe == menu_id %}active{% endif %}"> <td><a href="?mid={{ menu.id }}">{{ menu.title }}</a></td> <td><i class="fa {{ menu.icon }}" aria-hidden="true"></i></td> <td> <a style="color: #333333; font-size:18px" href="{% memory_url request 'rbac:menu_edit' pk=menu.id %}"> <i class="fa fa-edit" aria-hidden="true"></i> </a> <a style="color: red; font-size:18px" href="{% memory_url request 'rbac:menu_del' pk=menu.id %}"> <i class="fa fa-trash-o" aria-hidden="true"></i> </a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> {% endblock content %}
需要用模板语法判断用户选中的菜单并给其一个active的样式
rbac/templates/rbac/change.html
{% extends 'layout.html' %} {% block css %} <style> ul { list-style-type: none; padding: 0; } ul li { float: left; padding: 10px; padding-left: 0; width: 80px; } ul li i { font-size: 18px; margin-left: 5px; color: #6d6565; } </style> {% endblock css %} {% block content %} <div class="luffy-container"> <form class="form-horizontal" action="" method="post" novalidate> {% csrf_token %} {% for field in forms %} <div class="form-group"> <label class="col-sm-2 control-label" for="{{ field.auto_id }}">{{ field.label }}</label> <div class="col-sm-8"> {{ field }} <span style="color:red;">{{ field.errors.0 }}</span> </div> </div> {% endfor %} <div class="form-group"> <div class="col-sm-offset-2 col-sm-8"> <input type="submit" value="提交" class="btn btn-primary"> </div> </div> </form> </div> {% endblock content %}