第五章 动态表格分页展示数据
教学目的
- 初步了解layui的使用
- 理解table组件在layui中的应用
- 了解数据添加、删除、编辑操作引入JavaScript框架的优势
layUI动态表格
表格组件 table 是 Layui 中使用率极高的一个组件,它以表格的承载方式对数据进行渲染、重载、排序、统计、分页等等一系列交互操作,并提供了丰富的 API 用于扩展,基本涵盖了日常业务所涉及的大部分需求。
表格标签
<table class="layui-hide" id="currentTableId" lay-filter="currentTableFilter"></table>
layui-hide
类样式,表格标签不显示,表格中没有行列- 必须定义表格的id
lay-filter
layui中表格事件对象标识
表格的渲染
在layui中通过table.render()
函数实现表格数据的渲染,常用属性。
elem
:表格对象IDurl
:表格数据接口地址method
数据查询是通过post还是gettoolbar
数据表格上部工具栏模板defaultToolbar
表格右上角工具图标cols
表格列数组limits
数组,表格每页显示数据条数方案limit
默认每页显示记录数page
是否开启分页skin
设置表格边框风格。可选值:grid
|line
|row
|nob
even
是否开启隔行背景。
模板定义
基本语法结构:
<script type="text/html" id="toolbar">
<!--html标签-->
</script>
常用的几个事件
-
表格事件
基本语法:
table.on('event(filter)', callback);
toolbar
头部工具栏事件tool
单元格工具事件。可在该事件中实现行的更新与删除操作。
-
表格方法
table.reload(id, options, deep)
表格完整重载。
创建动态表格实现数据展示
建立动态表格实现数据展示的模板
-
创建数据展示表格的通用模板页面
base_list.html
{%extends 'common/layout.html'%} {%block content%} <div class="layui-card-body"> <!-- 查询操作区一 --> <div class="layui-collapse" style="margin-bottom: 10px;"> <div class="layui-colla-item"> <div class="layui-colla-title">查询</div> <div class="layui-colla-content layui-show"> {% block query%}{% endblock%} </div> </div> </div> <!-- 表格区域 --> <div class="layui-form layui-table-box"> <table class="layui-hide" id="TableId" lay-filter="TableFilter"></table> </div> </div> {% block templet %}{% endblock %} {% endblock %}
- 模板页面主要有两部分组成,查询区域和表格区域。
- 新添加了templet模板块,用于动态表格的元素模板的存放。
-
修改employ_list.html模板页面,使用动态模板显示数据
{%extends 'common/base_list.html'%} {% block templet %} <!-- 顶部工具栏模板 --> <script type="text/html" id="toolbar"> <div class="layui-btn-container"> <button class="layui-btn layui-btn-sm" lay-event="add"> <i class="layui-icon layui-icon-addition"></i> 新增 </button> </div> </script> <!-- 表格操作模板 --> <script type="text/html" id="TableBar"> <div class="layui-clear-space"> <a class="layui-btn layui-btn-xs " lay-event="edit"> <i class="layui-icon layui-icon-edit"></i> 编辑 </a> <a class="layui-btn layui-btn-xs layui-bg-red" lay-event="delete"> <i class="layui-icon layui-icon-delete"></i> 删除 </a> </div> </script> {%endblock%} {% block js %} <script> layui.use(function(){ var $ = layui.jquery, form = layui.form, table = layui.table, layer = layui.layer; // 渲染表格 table.render({ elem: '#TableId', url: '/employee/list/', method:'post', toolbar: '#toolbar', defaultToolbar: ['filter', 'exports', 'print'], cols: [[ {type: "checkbox", width: 50}, {field: 'index', width: 80, title: '序号',align:'center', sort: true}, {field: 'id', width: 80, title: 'ID',align:'center', sort: true,hide:true}, {field: 'job_number', width: 150, title: '工号',align:'center',sort: true}, {field: 'name', width: 150, title: '姓名',align:'center',sort: true}, {field: 'depart', width: 150, title: '部门',align:'center',sort: true}, {field: 'gender', width: 150, title: '性别',align:'center',sort: true}, {field: 'birthday', width: 150, title: '出生日期',align:'center',sort: true}, {field: 'phone', width: 150, title: '手机号',align:'center',sort: true}, {field: 'hiredate', width: 150, title: '入职日期',align:'center',sort: true}, {title: '操作', minWidth: 240, toolbar: '#TableBar', align: "center", fixed: 'right' } ]], limits: [10, 20, 50,100,200,500], limit: 10, page: true, skin: 'row', even: true, }); }) </script> {%endblock%}
-
运行可以看见基本界面,但是出现数据403错误原因是我们没有提供数据接口。
建立数据接口
-
查看页面https://layui.dev/static/json/2/table/demo1.json,可以看见表格数据的基本JSON格式。
-
在common文件夹中创建
res_json_data.py
文件存放JSON数据通用response函数# JSON 数据提交通用方法 from django.http import JsonResponse def success_api(msg: str = "成功"): """ 成功响应 默认值”成功“ """ res = { 'msg': msg, 'success': True, } return JsonResponse(res, safe=False) def fail_api(msg: str = "失败"): """ 失败响应 默认值“失败” """ res = { 'msg': msg, 'success': False, } return JsonResponse(res, safe=False) def table_api(msg: str = "success", count=0, data=None, limit=10): """ 动态表格渲染响应 """ res = { 'msg': msg, 'code': 0, 'data': data, 'count': count, 'limit': limit } return JsonResponse(res, safe=False,json_dumps_params={'ensure_ascii': False}) # 返回单页数据 def json_api(code=200,msg="success",data=None): # 包括200(成功)、201(创建)、400(错误请求)、401(未授权)、403(禁止)、404(未找到)、500(服务器错误) res={ "code": code, "message": msg, "data": data } return JsonResponse(res, safe=False, json_dumps_params={'ensure_ascii': False}) # 返回多页数据 def json_list_api(code=200,msg="success",data=None,total=0,limit=10, page=1): res={ "code": code, "message": msg, "data": { 'total': total, 'limit': limit, 'page': page, 'data': data } } return JsonResponse(res, safe=False,json_dumps_params={'ensure_ascii': False})
-
改造视图函数
# 员工数据表格 def employee_list(request): print(request.method,"==请求方式") if request.method == "GET": return render(request,"archives/employee_list.html")
出现错误:
CSRF token missing or incorrect
,错误原因是ajax的post提交没有进行CSRF验证。 -
解决ajax提交不进行CSRF验证
from django.views.decorators.csrf import csrf_exempt @csrf_exempt def viewfun(request)
- 导入
csrf_exempt
- 在ajax请求的视图函数上面加上@csrf_exempt注解(装饰器)。
- 导入
-
改造视图函数,对POST请求返回表格需要的JSON格式数据。
# 员工数据表格 @csrf_exempt def employee_list(request): print(request.method,"==请求方式") if request.method == "GET": return render(request,"archives/employee_list.html") # post请求返回JSON data_obj = Employee.objects.all() data_list=[] for row in data_obj: data_list.append({ "id":row.id, "name":row.name, "job_number":row.job_number, "phone":row.phone, }) return res_json_data.table_api(data=data_list)
分析这个程序在效率方面,主要是对JSON字典的创建,比较麻烦。
-
优化JSON字典的创建
Model._meta.fields
:返回模型字段对象列表# post请求返回JSON data_obj = Employee.objects.all() row_obj = data_obj[0] print(row_obj._meta.fields) field_names = [field.name for field in row_obj._meta.fields] print("==字段列表:",field_names)
- ⑤行:采用列表推导式,生成一个列表
-
改造视图函数
# post请求返回JSON data_obj = Employee.objects.all() data_list=[] for row in data_obj: item_dict = {} field_names = [field.name for field in row._meta.fields] for field in field_names: item_dict[field] = getattr(row,field) data_list.append(item_dict) return res_json_data.table_api(data=data_list)
getattr(row,field)
:内置函数,等同于row.field
-
解决返回对象的错误
for field in field_names: if field == "depart": item_dict[field] = row.depart.name else: item_dict[field] = getattr(row,field)
-
解决性别显示错误问题
for field in field_names: if field == "depart": item_dict[field] = row.depart.name elif field == "gender": item_dict[field] = row.get_gender_display() else: item_dict[field] = getattr(row,field)
数据分页和排序
-
批量向人员表格中添加数据。
def index(request): for i in range(100): new_data ={ 'name':f"员工{i}", 'job_number':f"RS0{i}", 'gender':1, 'birthday':'2018-08-11', 'hiredate':datetime.now(), 'depart':Department.objects.first(), } # Employee.objects.create(**new_data) employee =Employee(**new_data) employee.save() return HttpResponse("<h1 style='color:red'>我的第一个Django视图</h1>")
- 12行:用到了
**
对字典进行解包 - 11行:可以通过create向数据库中插入元素
- 12行,13行:通过创建一个模型对象,然后调用模型对象的
save()
方法保存到数据库中。注意:如果这里对象是存在的对象,save()
方法将执行更新update
操作,否则执行insert
操作。
- 12行:用到了
-
排序和分页的相关语法。
排序:
- 升序:
实体模型.objects.all().order_by("排序字段1","排序字段2")
- 降序:
实体模型.objects.all().order_by("-排序字段")
分页:
Paginator
类用于对列表进行分页,它允许你将对象列表分成固定长度的页。语法 :
Paginator(列表对象,每页数量)
返回当前页数据 :
page = paginator.page(页码)
- 升序:
-
改造视图函数实现排序和分页
from django.core.paginator import Paginator page = request.POST.get('page', 1) limit = request.POST.get('limit', 10) data_obj = Employee.objects.all() data_page = Paginator(data_obj, limit).page(page) data_list=[] for row in data_page: item_dict = {} field_names = [field.name for field in row._meta.fields] …… return res_json_data.table_api(data=data_list,count=len(data_obj))
-
改造新增操作
// 工具栏事件 table.on('toolbar(TableFilter)', function (obj) { if (obj.event === 'add') { window.location.href = '/employee/add/'; } });
基本语法:
table.on('event(filter)', callback);
toolbar
头部工具栏事件tool
单元格工具事件。可在该事件中实现行的更新与删除操作。
-
改造数据排序
data_obj = Employee.objects.all().order_by('-id')
-
改造编辑操作
// 单元格事件 table.on('tool(TableFilter)', function (obj) { var data = obj.data; //获得当前行数据 if (obj.event === 'edit') { window.location.href = '/employee/edit/'+data.id+'/'; } });
-
删除员工操作
// 单元格事件 table.on('tool(TableFilter)', function (obj) { var data = obj.data; //获得当前行数据 if (obj.event === 'edit') { window.location.href = '/employee/edit/'+data.id+'/'; } else if (obj.event === 'delete') { layer.confirm('确认删除吗?', { btn: ['确定', '取消'] //按钮 }, function(index){ console.log('点击了确定'); layer.close(index); window.location.href = '/employee/delete/'+data.id+'/'; } ); } });
# 员工删除 def employee_del(request,nid): Employee.objects.filter(id=nid).delete() return redirect("/employee/list/")
数据查询操作
查询语句基本语法
基本语法
filter(**kwargs)
返回一个新的 QuerySet,包含的对象满足给定查询参数。
exclude(**kwargs)
返回一个新的 QuerySet,包含的对象不满足给定查询参数。
表示并且关系查询
# 方法一:
UserInfo.objects.filter(name="张晓红",age=39).all()
# 方法二:
data_dict ={"name":"张晓红","age":39}
UserInfo.objects.filter(**data_dict).all()
表示或者关系查询
from django.db.models import Q
# 假设我们有一个 Employee 模型,部门和头衔都是外键,你可以直接使用外键字段
employees = Employee.objects.filter(
Q(department__name='研发部') | Q(department__name='市场部'),
Q(title__name='工程师') | Q(title__name='经理')
)
我们使用了 Q
对象来构建两个查询条件,每个条件都包含一个逻辑或操作。第一个查询条件检查 department
字段是否等于“研发部”或“市场部”,第二个查询条件检查 title
字段是否等于“工程师”或“经理”。
查询比较运算符
数字类型查询比较
- 等于:
data_list = UserInfo.objects.filter(age=26)
- 大于:
data_list = UserInfo.objects.filter(age__gt=26)
- 大于等于:
data_list = UserInfo.objects.filter(age__gte=26)
- 小于:
data_list = UserInfo.objects.filter(age__lt=28)
- 小于等于:
data_list = UserInfo.objects.filter(age__lte=28)
- 不等于:
data_list = UserInfo.objects.exclude(age=28)
- 在列表中 :
order_obj = Order.objects.filter(id__in=orderID_list).order_by('-update_time')
字符串比较
- 等于:
data_list = UserInfo.objects.filter(name="张晓红")
- 不等于:
data_list = UserInfo.objects.filter(name__ne="张晓红")
- 以XX开头:
data_list = UserInfo.objects.filter(name__startswith="刘")
- 以XX结尾:
data_list = UserInfo.objects.filter(name__endswith="红")
- 包含XX:
data_list = UserInfo.objects.filter(name__contains="小")
查询语句举例
-
查询所有性别为女的员工
def index(request): data_obj = Employee.objects.filter(gender=2).all() res_list = [item.name for item in data_obj] print(res_list) return HttpResponse("<h1 style='color:red'>我的第一个Django视图</h1>")
-
查询销售部的所有员工
data_obj = Employee.objects.filter(depart__name='销售部').all()
data_obj = Employee.objects.filter(depart_id=5).all()
-
查询2018-1-1日之前出生的员工
data_obj = Employee.objects.filter(birthday__lt="2018-1-1").all()
-
查询2018-1-1日之前出生的女员工
data_obj = Employee.objects.filter(birthday__lt="2018-1-1",gender=2).all()
-
查询姓名中包含李的员工
data_obj = Employee.objects.filter(name__contains="李").all()
查询操作改造
-
创建员工信息查询表单类
class employee_seacher_form(forms.ModelForm): class Meta: model = Employee fields = ['name','gender','depart','job_number','phone'] def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) for name,field in self.fields.items(): field.widget.attrs={"class":"layui-input","placeholder":"输入"+field.label} field.required = False
field.required = False
设置所有字段都不是必须输入字段 -
改造视图函数
# 员工数据表格 @csrf_exempt def employee_list(request): print(request.method,"==请求方式") if request.method == "GET": init_data ={"gender":0} form =employee_form.employee_seacher_form(initial=init_data) return render(request,"archives/employee_list.html",{"form":form})
-
改造模板页面
{% block query%} <form class="layui-form" novalidate> <div class="layui-form-item"> {% for field in form %} <div class="layui-inline"> <label class="layui-form-label w-auto">{{ field.label }}: </label> <div class="layui-input-inline"> {{ field }} </div> </div> {% endfor %} <div class="layui-inline" style="float: right;"> <div class="layui-input-inline" style="width: auto;"> <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="data-search-btn" id="search"><i class="layui-icon layui-icon-search"></i>查 询 </button> <button class="layui-btn layui-btn-primary"><i class="layui-icon layui-icon-refresh"></i> 重 置 </button> </div> </div> </div> </form> {%endblock%}
-
查询事件的处理
// 监听搜索操作 form.on('submit(data-search-btn)', function (data) { console.log(data.field) var result = JSON.stringify(data.field); //执行搜索重载 table.reload('TableId', { page: { curr: 1 }, where: { searchParams: result } }, 'data'); return false; });
JSON.stringify()
:将 JavaScript 对象或值转换为 JSON 格式的字符串。data.field
:layui表单方法,可以获取表单中所有控件的值
-
查询函数的改造
import json # post请求返回JSON page = request.POST.get('page', 1) limit = request.POST.get('limit', 10) searchParams = request.POST.get('searchParams', None) if searchParams: searchParams =json.loads(searchParams) fileld_names = list(searchParams.keys()) fileld_vaules = list(searchParams.values()) filter_dict = dict() for i in range(len(fileld_vaules)): if fileld_vaules[i]: item = fileld_names[i]+"__contains" filter_dict[item] = fileld_vaules[i] print(filter_dict,"==查询参数") data_obj = Employee.objects.filter(**filter_dict).order_by('-id') else: data_obj = Employee.objects.all().order_by('-id') data_page = Paginator(data_obj, limit).page(page)
json.loads()
是 Python 的标准库模块 json 中的一个函数,用于将 JSON 格式的字符串解码成 Python 字典对象。 -
修改部门查询错误问题
for i in range(len(fileld_vaules)): if fileld_vaules[i]: if fileld_names[i] == "depart": item = fileld_names[i]+"_id" else: item = fileld_names[i]+"__contains" filter_dict[item] = fileld_vaules[i]
优化编辑删除等操作
弹出层组件
弹出层组件 layer 是 Layui 最古老的组件,也是使用覆盖面最广泛的代表性组件。 layer 集众多弹层功能为一体,灵活而多样,是许多开发者的网页弹出层的首选交互方案,在各类业务场景都能发挥重要作用。
layer.open({
title: ['编辑', 'font-size:18px;font-weight:bold;'],
type: 2,
shade: 0.3,
maxmin:true,
shadeClose: true,
area: ['60%', '400px'],
content: url+"?id="+data.id,
});
上面函数可以弹出一个iframe页面层。
title
:弹出层的标题type
:弹层类型,2,为 iframe 内联框架层shade
:弹层的遮罩。shade: 0.3
设置遮罩深色背景的透明度;shade: [0.3, '#FFF']
设置遮罩透明度和颜色值;shade: 0
不显示遮罩maxmin
:是否开启标题栏的最大化和最小化图标。shadeClose
:是否点击遮罩时关闭弹层。当遮罩存在时有效。area
:设置弹层的宽高content
:弹层内容。若type: 2
(iframe 层)则为url地址
具体操作
-
改造模板编辑函数
if (obj.event === 'edit') { layer.open({ title: ['编辑', 'font-size:18px;font-weight:bold;'], type: 2, shade: 0.3, maxmin:true, shadeClose: true, area: ['60%', '400px'], content: '/employee/edit/'+data.id+'/', }); }
-
解决拒绝链接请求的错误
在
settings.py
中添加X_FRAME_OPTIONS = 'SAMEORIGIN' # 设置Frame-Options为SAMEORIGIN,否则无法在iframe中加载页面
X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在 frame , iframe 或者 object 中展现的标记。网站可以使用此功能,来确保自己网站的内容没有被嵌到别人的网站中去,也从而避免了点击劫持 (clickjacking) 的攻击。
X-Frame-Options有三个可能的值:
deny
表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。sameorigin
表示该页面可以在相同域名页面的 frame 中展示。allow-from uri(http://......)
表示该页面可以在指定来源的 frame 中展示。
-
创建对话框公共模板页面
layout_dlg.html
{% load static %} <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>layui</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0"> <link href="{%static 'layui/css/layui.css'%}" rel="stylesheet" media="all"> {% block css %}{% endblock %} <style> /* 必须字段 */ .label-required-next:after { top: 6px; right: 5px; color: red; content: '*'; position: absolute; margin-left: 4px; font-weight: 700; line-height: 1.8em; } /* 只读文本框 */ .readonly-input { background-color: #f2f2f2; border: 1px solid #d2d2d2; } </style> </head> <body style="background-color: #FFF;"> <div class="layui-form"> {% block content %}{% endblock %} </div> <script src="{%static 'layui/layui.js'%}"></script> {% block js %}{% endblock %} </body>
-
创建两行编辑模板
{%extends 'common/layout_dlg.html'%} {%block content%} <div class="layui-card-body"> <form class="layui-form" method="post" novalidate> <div class="layui-form-item"> {% csrf_token %} {% for field in form %} <div class="layui-inline"> {% if field.field.required %} <label class="layui-form-label label-required-next">{{ field.label }}:</label> {% else %} <label class="layui-form-label">{{ field.label }}:</label> {% endif %} <div class="layui-input-inline layui-input-wrap"> {{ field }} <span style="color: red">{{ field.errors.0 }}</span> </div> </div> {% endfor %} </div> <div class="layui-form-item"> <div class="layui-input-block"> <button type="submit" class="layui-btn" lay-submit>确认提交</button> </div> </div> </form> </div> {% endblock %} {%block js%} {% block extendsjs %}{% endblock %} {% for msg in messages %} <script> layui.use(['layer'], function () { var iconValue = "{{msg.tags}}" == 'success' ? 1 : 2; layer.msg( '{{msg}}', { icon: iconValue, time: 2000 }, function (){ if( "{{msg.tags}}" == 'success' ){ parent.layer.close(parent.layer.getFrameIndex(window.name)); //关闭当前页 parent.layui.table.reload('TableId');//重新加载父窗口表格 } } ) }) </script> {% endfor %} {% endblock %}
layui-row layui-col-xs6
是Layui 栅格系统,采用业界比较常用的容器横向12
等分规则layer.getFrameIndex(window.name);
在 iframe 页中获取弹层索引layer.close(index, callback);
关闭弹层。参数index
打开弹层时返回的唯一索引;参数callback
关闭弹层后的回调函数
-
改造编辑视图函数,进行提醒和关闭对话框
from django.contrib import messages …… if form.is_valid(): form.save() messages.success(request, '编辑成功') return render(request,"archives/employee_add.html",{"form":form}) ……
返回错误信息
-
方法一 : 通过form对象附加到下相应的字段上
if form.is_valid(): # 获取输入的验证码,并从form中移除 input_captcha = form.cleaned_data.pop('captcha') captcha_code = request.session.get('code','') request.session["code"] = None # 验证码验证后,移除session中的验证码 if captcha_code.upper() != input_captcha.upper(): form.add_error("captcha","验证码错误") return render(request,'login/login.html',{"form":form})
-
方法二 : 通过Message进行传递
#views from django.contrib import messages messages.warning(request, '用户名或密码错误')
#html {% for msg in messages %} layer.alert('{{ msg}}'); {% endfor %}
django.contrib.messages详解
后端传递消息的方法:
messages.debug(request, '消息1')
将在生产部署中被忽略(或删除)的与开发相关的消息messages.info(request, '消息2')
为用户提供信息消息messages.success(request, '消息3')
行为成功消息messages.warning(request, '消息4')
失败并没有发生,但可能即将发生messages.error(request, '消息5')
一个操作没有成功,或者发生了其他一些失败
前端将通过
messages
列表对象传递消息- 可以通过
message.tags
拿到每个消息 - 通过message拿到具体的消息
-
-
改造列表数据生成记录编号
…… data_list=[] count = (int(page) - 1) * int(limit) for row in data_page: count += 1 item_dict = {} field_names = [field.name for field in row._meta.fields] ……
-
改造查询选择框选择后自动执行查询
-
修改查询表单
class employee_seacher_form(forms.ModelForm): class Meta: model = Employee fields = ['name','gender','depart','job_number','phone'] widgets = { "gender":forms.Select(attrs={"lay-filter":"select-filter"}), "depart":forms.Select(attrs={"lay-filter":"select-filter"}), } def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) for name,field in self.fields.items(): field.widget.attrs.update({"class":"layui-input","placeholder":"输入"+field.label})
- 通过模式表单的
widgets
属性给选择框添加lay-filter
属性 - 改造初始化函数属性采用update方法,避免覆盖。
- 通过模式表单的
-
添加选择框事件
//监听选择框搜索事件 form.on('select(select-filter)', function(data){ $("#search").click() });
通过Jquery方法调用查询按钮的单击事件
layui选择框事件:
form.on('select(filter)', callback);
select
为选择框事件固定名称filter
为选择框元素对应的lay-filter
属性值
-