后台菜单管理功能
一、业务功能分析
1. 业务需求分析
后台首页菜单根据用户权限动态生成,不同菜单对应不同的功能视图。菜单的增删改查。
2.功能分析
- 菜单列表
- 添加菜单
- 修改菜单
- 删除菜单
3.模型设计
- 字段分析
- name
- url
- parent
- order
- permission
- icon
- codename
- is_visible
- 模型定义
# 在myadmin/models.py中定义如下模型
from django.db import models
from django.contrib.auth.models import Permission
from utils.models import BaseModel
# Create your models here.
class Menu(BaseModel):
name = models.CharField('菜单名', max_length=48, help_text='菜单名')
url = models.CharField('url', max_length=256, null=True, blank=True, help_text='url')
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
order = models.SmallIntegerField('排序', default=0)
permission = models.OneToOneField(Permission, on_delete=models.SET_NULL, null=True)
icon = models.CharField('图标', max_length=48, default='fa-link')
codename = models.CharField('权限码', max_length=48, help_text='权限码', unique=True)
is_visible = models.BooleanField('是否可见', default=False)
class Meta:
ordering = ['-order']
db_table = 'tb_menu'
verbose_name = '菜单'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
二、菜单列表
1.业务流程分析
- 获取未删除,的一级菜单
- 根据一级菜单获取未删除的二级菜单
- 渲染页面
2.接口设计
- 接口说明
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /admin/menus/ |
参数格式 | 无参数 |
-
返回结果
html
3.后端代码
- 视图
# myadmin/views.py下定义如下视图:
class MenuListView(View):
"""
菜单列表视图
url:/admin/menus/
"""
def get(self, request):
menus = models.Menu.objects.only('name', 'url', 'icon', 'is_visible', 'order', 'codename').filter(is_delete=False, parent=None)
return render(request, 'myadmin/menu/menu_list.html', context={'menus': menus})
2 . 路由
# admin/urls.py中添加如下路由
path('menus/', views.MenusView.as_view(), name='menu_list'),
4.前端代码
1.html
<!-- 创建templates/myadmin/menu/menu_list.html-->
{% extends 'myadmin/base/content_base.html' %}
{% load static %}
{% block page_header %}系统设置{% endblock %}
{% block page_option %}菜单管理{% endblock %}
{% block content %}
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">菜单列表</h3>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm">添加菜单
</button>
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
<table class="table table-bordered">
<tbody>
<tr>
<th>菜单</th>
<th>子菜单</th>
<th>url</th>
<th>图标</th>
<th>权限码</th>
<th>顺序</th>
<th>是否可见</th>
<th>逻辑删除</th>
<th>操作</th>
</tr>
{% for menu in menus %}
<tr>
<td>{{ menu.name }}</td>
<td></td>
<td>{{ menu.url|default:'' }}</td>
<td>{{ menu.icon }}</td>
<td>{{ menu.codename }}</td>
<td>{{ menu.order }}</td>
<td>{% if menu.is_visible %}是{% else %}否{% endif %}</td>
<td style="width: 100px" data-id="{{ menu.id }}" data-name="{{ menu.name }}">
{% if menu.children.all %}
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
{% else %}
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
<button type="button" class="btn btn-danger btn-xs delete">删除</button>
{% endif %}
</td>
</tr>
{% if menu.children.all %}
{% for child in menu.children.all %}
<tr>
<td></td>
<td>{{ child.name }}</td>
<td>{{ child.url }}</td>
<td>{{ child.icon }}</td>
<td>{{ child.codename }}</td>
<td>{{ child.order }}</td>
<td style="width: 80px">{% if child.is_visible %}是{% else %}否{% endif %}</td>
<td style="width: 100px" data-id="{{ child.id }}" data-name="{{ child.name }}">
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
<button type="button" class="btn btn-danger btn-xs delete">删除</button>
</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
三、添加菜单页面
1. 接口设计
- 接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /admin/menu/ |
参数格式 | 无参数 |
-
返回数据
html
2. 后端代码
-
视图
# 在myadmin/views.py中添加如下视图 class MenuAddView(View): """ 添加菜单视图 url:/admin/menu/ """ def get(self, request): form = MenuModelForm() return render(request, 'myadmin/menu/add_menu.html', context={'form': form})
-
路由
# 在myadmin/urls.py中添加如下路由 path('menu/', views.MenuAddView.as_view(), name='add_menu')
-
表单
# 在myadmin/forms.py中定义如下表单 from django import forms from .models import Menu class MenuModelForm(forms.ModelForm): parent = forms.ModelChoiceField(queryset=None, required=False, help_text='父菜单') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['parent'].queryset = Menu.objects.filter(is_delete=False, is_visible=True, parent=None) # https://docs.djangoproject.com/en/2.2/ref/forms/fields/#fields-which-handle-relationships class Meta: model = Menu fields = ['name', 'url', 'order', 'parent', 'icon', 'codename', 'is_visible']
为了在渲染表单是能加入自定义css样式,在应用admin中定义自定义标签,在admin下创建templatetags包,在其中创建admin_customer_tags.py模块
# myadmin/tamplatetags/admin_customer_tags.py from django.template import Library register = Library() @register.simple_tag() def add_class(field, class_str): return field.as_widget(attrs={'class': class_str})
#### 3.前端代码
1. html
```html
<!-- 修改 templates/myadmin/menu/menu_list.html -->
{% extends 'myadmin/base/content_base.html' %}
{% load static %}
{% block page_header %}系统设置{% endblock %}
{% block page_option %}菜单管理{% endblock %}
{% block content %}
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">菜单列表</h3>
<div class="box-tools">
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#modal-add"
href="{% url 'admin:add_menu' %}">添加菜单
</button>
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
<table class="table table-bordered">
<tbody>
<tr>
<th>菜单</th>
<th>子菜单</th>
<th>url</th>
<th>图标</th>
<th>权限码</th>
<th>顺序</th>
<th>是否可见</th>
<th>操作</th>
</tr>
{% for menu in menus %}
<tr>
<td>{{ menu.name }}</td>
<td></td>
<td>{{ menu.url|default:'' }}</td>
<td>{{ menu.icon }}</td>
<td>{{ menu.codename }}</td>
<td>{{ menu.order }}</td>
<td>{% if menu.is_visible %}是{% else %}否{% endif %}</td>
<td style="width: 100px" data-id="{{ menu.id }}" data-name="{{ menu.name }}">
{% if menu.children.all %}
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
{% else %}
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
<button type="button" class="btn btn-danger btn-xs delete">删除</button>
{% endif %}
</td>
</tr>
{% if menu.children.all %}
{% for child in menu.children.all %}
<tr>
<td></td>
<td>{{ child.name }}</td>
<td>{{ child.url }}</td>
<td>{{ child.icon }}</td>
<td>{{ child.codename }}</td>
<td>{{ child.order }}</td>
<td style="width: 80px">{% if child.is_visible %}是{% else %}否{% endif %}</td>
<td style="width: 100px" data-id="{{ child.id }}" data-name="{{ child.name }}">
<button type="button" class="btn btn-info btn-xs edit">编辑</button>
<button type="button" class="btn btn-danger btn-xs delete">删除</button>
</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- add modle -->
<div class="modal fade" id="modal-add" role="dialog" >
<div class="modal-dialog">
<div class="modal-content">
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}
<!-- 新建 templates/myadmin/menu/add_menu.html -->
{% load admin_customer_tags %}
{% load static %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span></button>
<h4 class="modal-title">添加菜单</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" id="add-menu">
{% csrf_token %}
<div class="box-body">
{% for field in form %}
{% if field.name == 'is_visible' %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label for="{{ field.id_for_label }}">{{ field }}{{ field.label }}</label>
</div>
</div>
</div>
{% else %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10">
{% for error in field.errors %}
<label class="control-label" for="{{ field.id_for_label }}">{{ error }}</label>
{% endfor %}
{% add_class field 'form-control' %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary add">添加</button>
</div>
四、添加菜单
1.业务流程分析
- 接收表单参数
- 校验表单参数
- 校验成功保存菜单数据,创建菜单一对一关联权限对象,返回创建成功的json数据
- 校验失败,返回渲染了错误信息的表单
2.接口设计
- 接口说明:
类目 | 说明 |
---|---|
请求方法 | POST |
url定义 | /admin/menu/ |
参数格式 | 表单参数 |
- 参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
name | 字符串 | 是 | 菜单名 |
url | 字符串 | 否 | 当前文章页数 |
order | 整数 | 是 | 排序 |
parent | 整数 | 否 | 父菜单id |
icon | 字符串 | 是 | 渲染图标类名 |
codename | 字符串 | 是 | 权限码 |
is_visible | 整数 | 是 | 是否可见 |
-
返回数据
# 添加正常返回json数据 { "errno": "0", "errmsg": "菜单添加成功!" }
如果有错误,返回html表单
3.后端代码
-
视图
# 在myadmin/views.py中的MenuAddView视图中添加post方法 class MenuAddView(View): """ 添加菜单视图 url:/admin/menu/ """ def get(self, request): form = MenuModelForm() return render(request, 'myadmin/menu/add_menu.html', context={'form': form}) def post(self, request): form = MenuModelForm(request.POST) if form.is_valid(): new_menu = form.save() content_type = ContentType.objects.filter(app_label='myadmin', model='menu').first() permission = Permission.objects.create(name=new_menu.name, content_type=content_type, codename=new_menu.codename) new_menu.permission = permission new_menu.save(update_fields=['permission']) return json_response(errmsg='菜单添加成功!') else: return render(request, 'myadmin/menu/add_menu.html', context={'form': form})
4.前端代码
-
html
<!-- 在 templates/myadmin/menu/add_menu.html 中引入js --> ... <script src="{% static 'js/myadmin/menu/add_menu.js' %}"></script>
-
js
// 创建static/js/myadmin/menu/add_menu.js $(() => { let $addBtn = $('button.add'); // 模态框中的添加按钮 let $form = $('#add-menu'); // 模态矿中的表单 let data = {}; $addBtn.click(function () { $ .ajax({ url: '/admin/menu/', type: 'POST', data: $form.serialize(), // dataType: "json" }) .done((res) => { if (res.errno === '0') { // 添加成功,关闭模态框,并刷新菜单列表 $('#modal-add').modal('hide').on('hidden.bs.modal', function (e) { $('#content').load( $('.sidebar-menu li.active a').data('url'), (response, status, xhr) => { if (status !== 'success') { message.showError('服务器超时,请重试!') } } ); }); message.showSuccess(res.errmsg); } else { message.showError('添加菜单失败!'); // 更新模特框中的表单信息 $('#modal-add .modal-content').html(res) } }) .fail(() => { message.showError('服务器超时,请重试'); }); }); });
五、删除菜单
1.接口设计
- 接口说明:
类目 | 说明 |
---|---|
请求方法 | DELETE |
url定义 | /admin/menu/<int:menu_id>/ |
参数格式 | 路径参数 |
- 参数说明
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
menu_id | 整数 | 是 | 菜单id |
-
返回值
{ "errno": "0", "errmsg": "删除菜单成功!" }
2.后端代码
-
视图
# 在admin/views.py中创建一个MenuUpdateView视图 class MenuUpdateView(View): """ 菜单管理视图 url:/admin/menu/<int:menu_id>/ """ def delete(self, request, menu_id): menu = models.Menu.objects.filter(is_delete=False, id=menu_id) if menu: menu = menu[0] if menu.children.filter(is_delete=False).exists(): return json_response(errno=Code.DATAERR, errmsg='父菜单不能删除!') menu.is_delete = True menu.save(update_fields=['is_delete']) return json_response() else: return json_response(errno=Code.NODATA, errmsg='菜单不存在!')
-
路由
# 在admin/urls.py中添加如下路由 path('menu/<int:menu_id>/', views.MenuUpdateView.as_view(), name='menu_manage'),
3.前端代码
-
html
<!-- 修改 templates/admin/menu/menu_list.html 在content中,添加删除模态框 然后引入menu.js --> {% block content %} ... ... <div class="modal modal-danger fade" id="modal-delete"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span></button> <h4 class="modal-title">警告</h4> </div> <div class="modal-body"> <p>One fine body…</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline pull-left" data-dismiss="modal">取消</button> <button type="button" class="btn btn-outline delete-confirm">删除</button> </div> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> {% endblock %} {% block script %} <script src="{% static 'js/admin/menu/menu.js' %}"></script> {% endblock %}
-
js
// 创建 static/js/admin/menu/menu.js $(() => { let $editBtns = $('button.edit'); let $deleteBtns = $('button.delete'); // 删除按钮 menuId = 0; // 被点击菜单id let $currentMenu = null; // 当前被点击菜单对象 $deleteBtns.click(function () { let $this = $(this); $currentMenu = $this.parent().parent(); menuId = $this.parent().data('id'); let menuName = $this.parent().data('name'); $('#modal-delete .modal-body p').html('确定删除菜单:《' + menuName + '》?'); $('#modal-delete').modal('show'); $('#modal-delete button.delete-confirm').click(() => { deleteMenu(); }) }); // 删除菜单 function deleteMenu() { $ .ajax({ url: '/admin/menu/' + menuId + '/', type: 'DELETE', dataType: "json" }) .done((res) => { if (res.errno === '0') { $('#modal-delete').modal('hide'); $('#modal-delete button.delete-confirm').unbind(); $currentMenu.remove(); message.showSuccess('删除成功!'); } else { message.showError(res.errmsg) } }) .fail(() => { message.showError('服务器超时,请重试!') }); } });
六、编辑菜单页面
1. 接口设计
- 接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /admin/menu/<int:menu_id> |
参数格式 | 路径参数 |
- 参数说明
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
menu_id | 整数 | 是 | 菜单id |
-
返回数据
html
2.后端代码
# 在admin/views.py中的MenuUpdateView视图中添加一个get方法
class MenuUpdateView(View):
"""
菜单管理视图
url:/admin/menu/<int:menu_id>/
"""
def get(self, request, menu_id):
menu = models.Menu.objects.filter(is_delete=False, id=menu_id).first()
form = MenuModelForm(instance=menu)
return render(request, 'admin/menu/update_menu.html', context={'form': form})
3.前端代码
-
html
<!-- 修改 templates/admin/menu/menu_list.html 在content中,添加修改模态框 --> {% block content %} ... <!-- update modle --> <div class="modal fade" id="modal-update" role="dialog" aria-labelledby="myLargeModalLabel"> <div class="modal-dialog"> <div class="modal-content"> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> {% endblock %}
<!-- 新建 templates/admin/menu/update_menu.html --> {% load admin_customer_tags %} {% load static %} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span></button> <h4 class="modal-title">修改菜单</h4> </div> <div class="modal-body"> <form class="form-horizontal" id="update-menu"> {% csrf_token %} <div class="box-body"> {% for field in form %} {% if field.name == 'is_visible' %} <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> <label for="{{ field.id_for_label }}">{{ field }}{{ field.label }}</label> </div> </div> </div> {% else %} <div class="form-group {% if field.errors %}has-error{% endif %}"> <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label> <div class="col-sm-10"> {% for error in field.errors %} <label class="control-label" for="{{ field.id_for_label }}">{{ error }}</label> {% endfor %} {% add_class field 'form-control' %} </div> </div> {% endif %} {% endfor %} </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-default pull-left" data-dismiss="modal">取消</button> <button type="button" class="btn btn-primary update">修改</button> </div>
-
js
// 修改 static/js/admin/menu/menu.js $(() => { let $editBtns = $('button.edit'); let $deleteBtns = $('button.delete'); // 删除按钮 menuId = 0; // 被点击菜单id let $currentMenu = null; // 当前被点击菜单对象 // 编辑菜单 $editBtns.click(function () { let $this = $(this); $currentMenu = $this.parent().parent(); menuId = $this.parent().data('id'); $ .ajax({ url: '/admin/menu/'+ menuId +'/', type: 'get' }) .done((res)=>{ $('#modal-update .modal-content').html(res); $('#modal-update').modal('show') }) .fail(()=>{ message.showError('服务器超时,请重试!') }) }); $deleteBtns.click(function () { let $this = $(this); $currentMenu = $this.parent().parent(); menuId = $this.parent().data('id'); let menuName = $this.parent().data('name'); $('#modal-delete .modal-body p').html('确定删除菜单:《' + menuName + '》?'); $('#modal-delete').modal('show'); $('#modal-delete button.delete-confirm').click(() => { deleteMenu(); }) }); // 删除菜单 function deleteMenu() { $ .ajax({ url: '/admin/menu/' + menuId + '/', type: 'DELETE', dataType: "json" }) .done((res) => { if (res.errno === '0') { $('#modal-delete').modal('hide'); $('#modal-delete button.delete-confirm').unbind(); $currentMenu.remove(); message.showSuccess('删除成功!'); } else { message.showError(res.errmsg) } }) .fail(() => { message.showError('服务器超时,请重试!') }); } });
七、编辑菜单
1.业务流程分析
- 接收表单参数
- 校验表单参数
- 校验成功保存菜单,判断改动字段是否影响了权限,如果有影响,修改权限,返回json信息
- 校验失败,返回包含错误信息的html
2.接口设计
- 接口说明:
类目 | 说明 |
---|---|
请求方法 | PUT |
url定义 | /admin/menu/<int:menu_id> |
参数格式 | 路径参数+表单参数 |
- 参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
menu_id | 整数 | 是 | 菜单id |
name | 字符串 | 是 | 菜单名 |
url | 字符串 | 否 | 当前文章页数 |
order | 整数 | 是 | 排序 |
parent | 整数 | 否 | 父菜单id |
icon | 字符串 | 是 | 渲染图标类名 |
codename | 字符串 | 是 | 权限码 |
is_visible | 整数 | 是 | 是否可见 |
-
返回数据
# 添加正常返回json数据 { "errno": "0", "errmsg": "菜单修改成功!" }
如果有错误,返回html表单
3.后端代码
-
视图
# 在admin/views.py中的MenuUpdateView视图中添加一个put方法 class MenuUpdateView(View): """ 菜单管理视图 url:/admin/menu/<int:menu_id>/ """ ... def put(self, request, menu_id): menu = models.Menu.objects.filter(is_delete=False, id=menu_id).first() put = QueryDict(request.body) form = MenuModelForm(put, instance=menu) if form.is_valid(): obj = form.save() if 'name' in form.changed_data: obj.permission.name = obj.name if 'codename' in form.changed_data: obj.permission.codename = obj.codename obj.permission.save() return json_response(errmsg='菜单修改成功!') else: return render(request, 'admin/menu/update_menu.html', context={'form': form}) ...
4.前端代码
- html
<!-- 在 templates/admin/menu/update_menu.html
中引入update_menu.js
-->
...
<script src="{% static 'js/admin/menu/update_menu.js' %}"></script>
-
js
$(()=>{ let $updateBtn = $('#modal-update button.update'); let $form = $('#update-menu'); let data = {}; $updateBtn.click(function () { $.each($form.serializeArray(), function () { data[this.name] = this.value }); $ .ajax({ url: '/admin/menu/' + menuId + '/', type: 'PUT', data: data, // dataType: "json" }) .done((res) => { if (res.errno === '0') { $('#modal-update').modal('hide').on('hidden.bs.modal', function (e) { $('#content').load( $('.sidebar-menu li.active a').data('url'), (response, status, xhr) => { if (status !== 'success') { message.showError('服务器超时,请重试!') } } ); }); message.showSuccess(res.errmsg); } else { message.showError('修改菜单失败!'); $('#modal-update .modal-content').html(res) } }) .fail(() => { message.showError('服务器超时,请重试'); }); }); });
八、整合后台首页面菜单加载
1. 后端代码
-
视图
class IndexView(LoginRequiredMixin, View): """ 后台首页视图 """ def get(self, request): objs = models.Menu.objects.only('name', 'url', 'icon', 'permission__codename', 'permission__content_type__app_label').select_related( 'permission__content_type').filter(is_delete=False, is_visible=True, parent=None) has_permissions = request.user.get_all_permissions() menus = [] for menu in objs: if '%s.%s' % (menu.permission.content_type.app_label, menu.permission.codename) in has_permissions: temp = { 'name': menu.name, 'icon': menu.icon } children = menu.children.filter(is_delete=False, is_visible=True) if children: temp['children'] = [] for child in children: if '%s.%s' % (child.permission.content_type.app_label, child.permission.codename) in has_permissions: temp['children'].append({ 'name': child.name, 'url': child.url }) else: if not menu.url: continue temp['url'] = menu.url menus.append(temp) print(menus) return render(request, 'admin/index.html', context={'menus': menus})

网友评论