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

news2024/11/15 8:13:25

       前几天写了数据表格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=username
         self.password=password
         self.email=email
         self.status=status
         self.avatar=avatar
         self.nickname=nickname
         self.sex = sex
         self.telephone=telephone
         self.agent = agent
         self.role_cd = role_cd
    #获取密码

    @property
    def password(self):
         return self._password

    #设置密码
    @password.setter
    def 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严格遵守只生成数据不生成界面的原则,这样在前端就基本不会有啥冲突了。
       最终,显示的功能界面如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2071727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自制项目镜像并拉取

1.先把项目jar包拉到Linux上看能用不 mvn clean package cd target java -jar shared_battery-0.0.1-SNAPSHOT.jar 成功&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&…

24-8-24-读书笔记(十五)-《空谷幽兰》([法] 巴尔扎克 [译] 李玉民 )

文章目录 《空谷幽兰》&#xff08;[法] 巴尔扎克 [译] 李玉民 &#xff09;阅读笔记记录&#xff08;P112-P126&#xff09;总结 《空谷幽兰》&#xff08;[法] 巴尔扎克 [译] 李玉民 &#xff09; 《空谷幽兰》巴尔扎克最为得意的作品&#xff0c;这篇主要记录一个德莫尔索夫…

SSRF漏洞实现

文章目录 SSRF题1SSRF题2fastcgi方法 入侵网站收集资产方法SSRF反弹 SSRF题1 漏洞复现平台 代码&#xff1a; 首先&#xff0c;可以用dict探测一下内网开放的端口 可以检测是否有fastcgi或者是redis&#xff0c;如果有fastcgi&#xff0c;那么就有RCE漏洞破解&#xff0c;如…

Adobe Animate (AN)软件安装,硬件配置(附安装包)

目录 一、Adobe An 软件简介 Adobe An 软件的特点 Adobe An 软件的优势 下载 二、Adobe An 软件安装 安装前的准备工作 安装过程中的注意事项 安装后的设置 三、Adobe An 软件使用 高级动画技巧 交互设计 优化与性能提升 四、Adobe An 软件快捷键 选择工具快捷键…

专业级推荐:2024年硬盘数据恢复软件全攻略

硬盘作为我们存储珍贵数据、工作文档、家庭照片以及个人视频的核心载体&#xff0c;其重要性不言而喻。然而&#xff0c;随着使用时间的增长或是操作不当硬盘可能会遭遇损坏。遇到这种问题我们要怎么挽救自己的数据呢&#xff0c;这次我就来分享下我用过的硬盘数据恢复软件。 …

基于Netty的RPC框架

RPC远程过程调用(Remote Procedure Call)是一种通信协议&#xff0c;它允许程序调用位于不同地址空间&#xff08;通常是网络上的另一台机器&#xff09;的方法&#xff0c;而无需程序员显式编码这个远程调用的细节。这种技术隐藏了底层的通讯细节&#xff0c;使得调用远程服务…

中国数据库的崛起:从本土化挑战到全球化机遇

引言 谈起中国的崛起&#xff0c;大家第一反应可能是“中国制造”“高铁奇迹”“电商帝国”&#xff0c;但今天我们要聊的&#xff0c;是一个比这些还要神秘的存在——中国的数据库技术。或许你平时并不会经常关注它&#xff0c;但这个隐身在你手机、电脑、服务器背后的无形力…

Towards Enriched Controllability for Educational Question Generation

文章目录 题目摘要引言生成显式和隐式问题实验设置结果基线结论 题目 迈向教育问题生成的丰富可控性 论文地址:https://arxiv.org/abs/2306.14917 摘要 问题生成 (QG) 是自然语言处理 (NLP) 中的一项任务&#xff0c;涉及根据输入自动生成问题&#xff0c;输入通常由文本和目标…

在进行网站链接时,‌加上http或https的重要性不言而喻

这一简单的操作背后&#xff0c;‌蕴含着对搜索引擎优化&#xff08;‌SEO&#xff09;‌的深刻理解&#xff0c;‌以及对网站权重提升的精准把握。‌以下&#xff0c;‌我们将深入探讨这一话题&#xff0c;‌以期为您的网站优化提供有价值的参考。‌优化&#xff08;‌SEO&…

即时通讯IM软件推荐:五款适合企业内部使用的IM即时通讯软件

随着企业的不断发展&#xff0c;内部沟通和协作变得尤为重要。为了提高沟通效率、加强团队协作以及促进信息共享&#xff0c;企业需要选择适合自身需求的即时通讯IM软件。本文将为大家推荐五款适合企业内部使用的IM即时通讯软件&#xff0c;其中包括了备受赞誉的WorkPlus。 Wor…

SPSS和MATLAB实现【典型相关分析】

典型相关分析&#xff08;Canonical Correlation analysis &#xff09;&#xff0c;是用于研究 两组 变量&#xff08;每组变量中都可能有多个指标&#xff09; 之间相关关系的一种多元统计方法。它能够揭示出两组变量之间的内在联系。 我们之前总结的相关性分析&#xff0c;也…

libtorch学习历程(二):张量

libtorch(pytorch c)的大多数api和pytorch保持一致。 使用之前要导入torch #include <torch/torch.h> #include <torch/script.h> 1. 张量初始化 1.1 固定的值与尺寸 在C中&#xff0c;使用{}来表示尺寸 zeros() zeros()产生值全为0的张量。 // 得到一个三维…

Java数据结构篇

Map体系 1.HashMap 哈希冲突&#xff1a;开放定址法、再哈希法、链地址法插入元素先检查是否到达阈值&#xff0c;是则先数组扩容&#xff0c;然后再插入链表&#xff0c;链表长度超过8则转红黑树1.7之前由于扩容导致的头插法尾插法混合导致指针错误&#xff0c;出现死循环问…

[底层原理] C/C++获取时间(将时间戳转换为年月日)?

前言 大家都知道&#xff0c;计算机中存储的时间是一个整数&#xff0c;在现在的编程语言中&#xff0c;可以很方便地将时间戳&#xff08;整数&#xff09;转换为字符串&#xff0c;但是如果没有这些我们该如何自己计算出呢&#xff1f; 刚好以前研究过Nginx的源代码&#xff…

docker系列12:Dockerfile实战

传送门 docker系列1&#xff1a;docker安装 docker系列2&#xff1a;阿里云镜像加速器 docker系列3&#xff1a;docker镜像基本命令 docker系列4&#xff1a;docker容器基本命令 docker系列5&#xff1a;docker安装nginx docker系列6&#xff1a;docker安装redis docker系…

红黑树、B+Tree、B—Tree

红黑树 B-Tree 这三个通常都是把内存全部加载到内存里&#xff0c;然后再内存中进行处理的&#xff0c;数据量通常不会很大。 内存一般容量都在GB级别&#xff0c;比如说现在常见的4G、8G或者16G。 如果要处理的数据规模非常大&#xff0c;大到内存根本存不下的时候。这个时候…

基于微信小程序靓丽内蒙古APP(源码+定制+辅导)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

验证码功能的思路和做法

验证码登录的思路和流程 步骤 1.导入依赖 <dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version> </dependency> 2.写一个验证码的配置类 package com.lzy.config;im…

IM即时通讯软件,企业即时通讯系统就选WorkPlus

在现代企业中&#xff0c;高效的沟通和协作是推动业务发展的关键。随着科技的不断进步&#xff0c;团队成员和企业之间的沟通已经超越了传统的邮件和电话方式&#xff0c;转向了更实时、更便捷的方式&#xff0c;即即时通讯软件。在众多即时通讯软件中&#xff0c;WorkPlus作为…

滑动窗口解决子串问题

问题解析&#xff1a; 以这道题为例子&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;找长度最小的子数组&#xff0c;子数组和必须大于条件中的target 暴力解法&#xff1a;左右指针列举出每一种子数组的可能&#xff0c;每种可能去求子数组的和&#xff0c;找到最小的…