Flask+LayUI开发手记(三):LayUI表格的后端数据分页展现

2024-08-25 08:20

本文主要是介绍Flask+LayUI开发手记(三):LayUI表格的后端数据分页展现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

       前几天写了数据表格table的前端分页展现,思路是把数据一次性取到前端,然后由前端来控制分页展现。这种做法主要目的是为了降低后端数据库读写的次数减轻服务端运行压力。但是,如果功能不单是查询还要进行增删改操作,那么一次数据提取到前端的做法就有些问题了,因为需要保持前后端数据集的同步,这个控制逻辑就比较复杂了(也不是不能写),不如老老实实用传统办法,就是后端根据前端的要求每次提供好分页内的数据即可。

       讲真,这种每次翻页就跑到后端取数的逻辑,我是不喜欢的,看上去很笨,因为每次换页都要提交后端进行数据请求,然后取数据还是要先读取全量数据,却只取指定偏移量后的10几条记录,看着就让人有种用大炮打苍蝇的感觉。而且后端服务器如果并发较多的情况下,这种逻辑对系统会产生很大的运行压力。不过,与前后端数据协同的复杂逻辑相比,想找一种效率很高又直白的控制逻辑,也是很难的。

       好在,大多数系统的并发量并不大,用这种模式也是不错的。毕竟这次用一直被吐槽运行效率低的python(还有flask)搭建这个系统,主要就是面向中小型企业的业务需求。,这些企业系统要求功能丰富业务逻辑严密完善,但用的人不多,并发量不大,真不必太教条地搬用那些运行效率法则。真到了要性能调优的时候,自然也会有很多种方案来解决。

       好吧,接着上程序。这个示范功能是用来做会员管理的,会员新增主要是通过自行注册完成,后台管理的会员管理主要起辅助作用,比传统的编辑功能,多加了一个封禁和解封的功能。主功能是做出会员信息的列表,对LayUI来说,就是一个经典的数据表格datatable的应用。

       首先是会员表model的程序,数据库结构如下定义,其中对password做了特殊处理,存储在数据库中的是加密后的结果。

class Members(db.Model):__tablename__ = 'cm_member'uid = db.Column(db.Integer, primary_key=True, autoincrement=True)username = db.Column(db.String(50), nullable=False, unique=True)  # 会员名不能为空,而且必须是唯一的_password = db.Column(db.String(200), nullable=False)             # 密码不能为空email = db.Column(db.String(50), nullable=False, unique=True)     # 用户邮箱不能为空,而且必须是唯一的nickname=db.Column(db.String(50),nullable=True)                   #用户昵称role_cd = db.Column(db.String(4),default='30')                    #角色编码 sex = db.Column(db.String(2), default='0')                        # 性别telephone = db.Column(db.String(11))                              # 电话agent = db.Column(db.String(20))                                  # 推荐人、代理人,对应内部员工号avatar = db.Column(db.String(128),default=None)                   # 头像status = db.Column(db.Integer)                                    # 状态 0:正常 1:审核 8:临封 9:封禁regtime = db.Column(db.DateTime,default=datetime.now)             #注册时间def __init__(self,username,password,email,status,avatar='',nickname='',sex='0',role_cd='30',telephone='',agent=''):self.username=usernameself.password=passwordself.email=emailself.status=statusself.avatar=avatarself.nickname=nicknameself.sex = sexself.telephone=telephoneself.agent = agentself.role_cd = role_cd#获取密码@propertydef password(self):return self._password#设置密码@password.setterdef password(self,raw_password):self._password=generate_password_hash(raw_password)#密码加密#检查密码def check_password(self,raw_password):result=check_password_hash(self.password,raw_password)#return result

       然后第二个程序是前端展现页面,用于会员信息列表的展示。前端程序的主体还是table.render()部分,和后端一次性提取数据前端控制分页展示用data指定本地数据集作为数据源相比,这个table.render()的主要区别,就是数据来源由url定义的路由来提供,其它的都完全一致。

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><title>会员管理</title><link rel="stylesheet" href="/static/layui/css/layui.css"  media="all">
</head>
<body><table id="table_list" lay-filter="table_list" style="margin-top:-15px;"></table><script type="text/html" id="toolBar"><div class="layui-btn-container"><div class="layui-inline"><label class="layui-btn-sm">会员名称:</label><div class="layui-input-inline"><input type="text" id="searchtext" placeholder="请输入名称" autocomplete="off" class="layui-input layui-btn-sm"></div></div><div class="layui-inline"><div class="layui-input-inline" style="padding-left:10px;padding-top:8px"><button id="btn_search" type="button" class="layui-btn layui-btn-normal layui-btn-sm" lay-event="search"><i class="layui-icon layui-icon-search"></i>查询</button><button id="btn_add" type="button" class="layui-btn layui-btn-sm" lay-event="add"><i class="layui-icon layui-icon-add-1"></i>增加会员</button><button id="btn_mban" type="button" class="layui-btn layui-btn-sm" lay-event="mban"><i class="layui-icon layui-icon-lock"></i>批量封禁</button></div></div></div>
</script><script type="text/html" id="linetoolBar">{% raw %}{{# if (d.status == 0 ) { }}<a lay-event="ban" title="封禁"><i class="layui-icon layui-icon-lock" style="color:red;"></i></a>{{# } if (d.status == 9) { }}<a lay-event="unban" title="解禁"><i class="layui-icon layui-icon-ok-circle" style="color:green;"></i></a>{{# } }}<a lay-event="edit" title="编辑" ><i class="layui-icon layui-icon-edit"></i></a><a lay-envent="rsetpwd" title="重置密码"><i class="layui-icon layui-icon-password"></i></a>{% endraw %}</script>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['jquery','layer','table'], function(){var layer=layui.layer,$=layui.jquery,table=layui.table;var cur_row;  	//初始化表格当前行var url_list = '{{url_for("sysadm.member_list")}}';table.render({elem: '#table_list',height: 'full',url: url_list,toolbar: '#toolBar',method: 'POST',page: true //开启分页,limits: [16, 20, 30, 40, 50]           ,limit : 16,even : true,size : 'sm',cols: [[ { type: 'checkbox', fixed: 'left' },{field: 'id', title: 'ID', width:30, sort: true, fixed: 'left'},{field: 'username', title: '会员名', width:90, sort: true, fixed: 'left'},{field: 'nickname', title: '昵称', width:90, sort: true},{field: 'email', title: '邮箱', width:170, sort: true},{field: 'sex_name', title: '性别', width:60, sort: true},{field: 'telephone', title: '电话', width:100, sort: true},{field: 'role_note', title: '会员角色', width: 100},{field: 'status_name', title: '状态', width: 30},{field: 'agent', title: '推荐人', width: 70},{field: 'regtime', title: '注册时间', width:160} ,{fixed: 'right', width:120, align:'center', toolbar: '#linetoolBar'}]]});//表头工具栏事件table.on('toolbar(table_list)', function (obj) {let cpage = obj.config.page.curr;console.log(JSON.stringify(obj.config.page))switch (obj.event) {case 'search':table_refresh(1);break;case 'add':break;case 'mban':break;};});//table行内工具栏事件table.on('tool(table_list)', function (obj) {    //obj是指这张表中的数据cur_row = obj.data;rid = cur_row.id;let cpage = obj.config.page.curr;//obj.event:获取触发事件的元素的 event 值,用于区分不同的操作switch(obj.event) {case 'edit':break;case 'ban':break;case 'unban':break;case 'resetpwd':break;}});function table_refresh(cpage) {table.reload('table_list', {where: {                           'searchtext':$('#searchtext').val()},  page: { curr: cpage },},true);}
});
</script>
</body>
</html>

       因为多了编辑功能,所以用toolbar和linetoolbar定义了一批button功能按钮,再用table.on进行事件侦听监控,本部分主要关注列表展示,所以toolbar中功能button的处理先都删除掉了。

       功能展现从表面看和一次性数据加载前端的分页控制并没有什么区别,不过内里是不一样,主要体现在分页的操作上。为了展示分页方便,将table.render()中的limit参数设为6,系统前端数据展现就会体现出分成3页的效果。

        表面上看,这个分页和前端分页的展示完全一致,但内部处理逻辑完全不同。在这个后端控制分页数据时,点击分页栏的任何一个按钮,都会向后端服务发出一个post请求,后端程序接收到请求后,即按要求生成相应的数据。

#会员列表
@bp.route('/member_list/',methods=['GET','POST'])
@login_required
@admin_auth
def member_list():if request.method == 'GET':return render_template('admin/member_list.html.j2')else :username = request.values.get('searchtext')filtstr = '1=1'if username :filtstr += ' and username like "' + username + r'%"'currPage = int(request.values.get('page'))pageLimit = int(request.values.get('limit'))logging.debug('POST Member_list (curr_page:%s pagernum:%s).....' % (str(currPage),str(pageLimit)))countTotal = db.session.query(func.count(Members.uid)).filter(text(filtstr)).scalar()if countTotal == 0:rsdata = {"code": 0,"msg": "无满足条件记录","count": countTotal,"data":[]}return json.dumps(rsdata)# 当总记录数小于偏移量时,将当前页数减1,生成最末一页数,前端会重新处理好页数。if (currPage -1 ) * pageLimit >= countTotal :currPage = currPage - 1; rows = db.session.query(Members).filter(text(filtstr)).offset((currPage-1)*pageLimit).limit(pageLimit).all()#获取总的记录recNum = len(rows)logging.debug('Total Rnumber %s Get Rec Number %s' % (str(countTotal),str(recNum)))reclist = []mbrStatus = Member_Status()mbrSex = Unv_Gender()mbrRole = Member_Role()for irow in rows:udata = dict(id=irow.uid,username=irow.username,email=irow.email,avatar=irow.avatar,nickname=irow.nickname,telephone=irow.telephone,agent=irow.agent,regtime=irow.regtime.strftime('%Y-%m-%d %H:%M:%S'),sex=irow.sex,sex_name=mbrSex.get_name(irow.sex),status=irow.status,status_name=mbrStatus.get_name(irow.status),role_cd=irow.role_cd,role_note=mbrRole.id_format(irow.role_cd))reclist.append(udata)rsdata = {"code": 0,"msg": "","count": countTotal,"data":reclist}return json.dumps(rsdata)

       通过后端程序可以看出来,除了前端post请求中列出的上传参数外,在request的参数里多了两个隐含参数,page和limit,page是当前页数,limit是每页记录数,通过这两个参数可以计算出前端要求数据的偏移量。

        后端程序分为三个部分,第一部分,是根据前端检索内容生成数据库筛选条件filtstr,sqlalchemy的query方法表面上看是一个面向单表的数据库查询接口,实质还是在拼SQL串,通过text()函数可以把任何的SQL字串拼到查询语句中。 

        第二部分是获取page和limit,并重新计算数据库结果集的记录数目,然后根据总记录数对page进行调整,这个调整主要是用于删除记录后的当前页调整(删除到最后一页的唯一记录时,当前页应该调整到前一页)。

       第三部分是程序的主体生成下传的数据结果集。结果集的格式是在table.render()的说明里规定的,包括四项,code是结果码,0表示正常返回,msg返回一个提示信息,当错误时前端会显示这个信息,count是总的记录数,前端将根据这个记录数重新调整分页栏的内容,data就是数据结果集。正常返回时,data里就是一个包含记录数据的字典列表。

        还有几点要说明的:

 一、返回数据是用json.dumps() 而不是flask自带的jsonify(),这两种JSON转换方法表面看是一样的,实际有一个重大的区别,就是json.dumps会将返回数据中的True/False(python的布尔值)转换成true/false(Javascript的布尔值),而flask-jsonify则不会做这个转换。这个布尔值的转换在datatable时用不到,但在treetable的返回结果集中是有用的,有个isparent属性用于标识是否有父节点,javascript是不识别python的布尔值的。

二、日期型的格式转换。用了strftime('%Y-%m-%d %H:%M:%S')将数据库内部时间字段转换成“YYYY-MM-DD HH:MM:SS"。如果不转,前端显示的原始记录还会带上星期值,不但不友好而且看着很乱。

三 、对代码字段(比如状态、性别、角色)等,由数据库内原始代码值转为码值说明,这个转换是做了一组维代码类来完成的,后面会介绍如何构建的。

四、linetoolBar的定义中用了{%raw%}......{%endraw%}的jinja2的屏蔽语义转换语句,其内部的{{...}}语句,jinja2不会做转换,所以前端LayUI就可以进行模板转换了。

       发现无论哪种渲染语言,似乎都对{{...}}有偏爱,以前也许这是冷门,但现在不约而同之下,冷门就变成了热点冲突。不单flask-jinja2用,LayUI用,VUE实际也在用。我的解决方案,就是严格限制在html的DOM中用jinja2转换,同时后端python严格遵守只生成数据不生成界面的原则,这样在前端就基本不会有啥冲突了。
       最终,显示的功能界面如下:

这篇关于Flask+LayUI开发手记(三):LayUI表格的后端数据分页展现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1105033

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

Hadoop集群数据均衡之磁盘间数据均衡

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可以执行磁盘数据均衡命令。(Hadoop3.x新特性) plan后面带的节点的名字必须是已经存在的,并且是需要均衡的节点。 如果节点不存在,会报如下错误: 如果节点只有一个硬盘的话,不会创建均衡计划: (1)生成均衡计划 hdfs diskbalancer -plan hadoop102 (2)执行均衡计划 hd

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设