html五编写视屏_Python Django+SQL+Pandas+Pyecharts自建在线数据分析平台(五)

本文是系列教程的第五部分,介绍如何在Django项目中创建前端表单进行交互,允许用户通过表单提交筛选参数。内容包括设计表单结构、使用Django模板和Jinja2语法,以及利用Semantic UI的search dropdown增强用户体验。此外,还讨论了在表单加载和搜索响应中动态获取选项的方法,以应对大量数据的场景。
摘要由CSDN通过智能技术生成

本篇是系列文章的第5篇,之前的更新见:

(一)需求分析&技术实现

(二)初步搭建Django环境

(三)页面布局&Django模板

(四)SQL+Pandas初步处理数据

(五)前端表单交互

(六)Ajax异步传参与加载

(七)前端数据格式的处理

(八)DataTables接管前端表格

(九)Pyecharts实现交互图表

(十)静态图表的展示

(十一)“导出数据至Excel”功能

(十二)添加和配置缓存

(十三)用户登录系统

(十四)部署Django至生产环境

在上一章,我们已经成功从后端将分析结果传回前端Django模板并展示,但这个分析结果是静态的,缺乏交互性。本章我们希望在预留的filter.html模板内建立表单,从前端向后端提交数据筛选的参数。本章的内容比较容易理解,但对用户体验至关重要,是个细致活。

还是回到上一章对数据本身各字段的分析,这对表单设计也格外重要:

b24a534f23a2cd0b3f05620ed214cd32.png
第4章的纸上谈兵本章依然有用

如上图,我们需要有一个必填单选代表一个分析目标字段(我还是习惯称之为breakout字段),它决定返回的数据结果里是品类份额,还是品牌份额,还是其他xx份额。它也是后端Pandas的pivot_table方法里column的动态参数。

我们还需要另外两个必填单选字段——UNIT和PERIOD,原因也请参考上图。

而我们所有的属性字段都是可为空的多选。

本例中我们表单不需要考虑AMOUNT和DATE字段。因为AMOUNT是唯一的指标字段,而我们的分析结果会取最新一个DATE做横断面结果,并计划把所有DATE的数据作为趋势分析,我们不需要对DATE动态选择。而在其他一些场景下,日期字段是经常作为表单的一员的,甚至有很多专门为其设计的calendar控件。

综上所述,我们的表单设计是下面这个样子,我们需要在filter.html文件中实现它。

6e8b72554e653d18d227958b981cae3c.png
TC为Therapy Class的简写,可理解为其他行业的不同层级的品类

实际前端模板代码编写前,可以后端先传一个预设的字段字典。这样操作一是分离前端方便以后修改,大部分情况下以后只修改后端就可以了;二是可以利用循环极大缩短代码长度,更加elegant。

我们再次修改views.py里index方法的代码,在context字典内增加表单的预设值传至前端:

# 该字典key为前端准备显示的所有多选字段名, value为数据库对应的字段名
D_MULTI_SELECT = {
    'TC I': '[TC I]',
    'TC II': '[TC II]',
    'TC III': '[TC III]',
    'TC IV': '[TC IV]',
    '通用名|MOLECULE': 'MOLECULE',
    '商品名|PRODUCT': 'PRODUCT',
    '包装|PACKAGE': 'PACKAGE',
    '生产企业|CORPORATION': 'CORPORATION',
    '企业类型': 'MANUF_TYPE',
    '剂型': 'FORMULATION',
    '剂量': 'STRENGTH'
}


def index(request):
    
    ...

    mselect_dict = {}
    for key, value in D_MULTI_SELECT.items():
        mselect_dict[key] = {}
        mselect_dict[key]['select'] = value
        # mselect_dict[key]['options'] = option_list 以后可以后端通过列表为每个多选控件传递备选项
    
    context = {
       ...
       'mselect_dict': mselect_dict
    }
    return render(request, 'chpa_data/analysis.html', context) # 注意本句和前一章也有变化,渲染至analysis.html而不是display.html

前端html模板filter.html代码如下,为了用户体验,我们希望所有的下拉菜单都使用Semantic UI的search dropdown提供搜索响应功能,主要就是应用这个class:class="ui fluid search dropdown":

<div class="ui container">
    <div class="ui form">
        <form action="" method="post">
            <!-- 在Django所有的 POST 表单元素时,需要加上下方的csrf_token tag,主要是安全方面的机制,本例后续使用AJAX方法,这里的POST class和token都不生效 -->
            {% csrf_token %}
            <h3 class="ui header" id="analysis">分析维度</h3>
            <div class="field">
                <div class="fields">
                    <div class="sixteen wide field">
                        <select name="DIMENSION_select" id="DIMENSION_select" class="ui fluid search dropdown">
                            {% for key, value in mselect_dict.items %}
                                {% if value.select == 'PRODUCT' %}
                                    <option value="{{ value.select }}" selected>{{ key }}</option>
                                {% else %}
                                    <option value="{{ value.select }}">{{ key }}</option>
                                {% endif %}
                            {% endfor %}
                        </select>
                    </div>
                </div>
                <div class="fields">
                    <div class="eight wide field">
                        <select name="UNIT_select" id="UNIT_select" class="ui fluid search dropdown">
                            <option value="Value" selected>金额</option>
                            <option value="Volume">盒数</option>
                            <option value="Volume (Counting Unit)">最小制剂单位数</option>
                        </select>
                    </div>
                    <div class="eight wide field">
                        <select name="PERIOD_select" id="PERIOD_select" class="ui fluid search dropdown">
                            <option value="MAT" selected>滚动年</option>
                            <option value="QTR">季度</option>
                        </select>
                    </div>
                </div>
            </div>
            <h3 class="ui header" id="data_filter">数据筛选</h3>
            <div class="field">
                {% for key, value in mselect_dict.items %}
                <div class="field">
                    <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple=""
                            class="ui fluid search dropdown">
                        <option value="">{{ key }}</option>
{#                        {% for item in value.options %}#}
{#                            <option value="{{ item }}">{{ item }}</option>#}
{#                        {% endfor %}#}
                    </select>
                </div>
                {% endfor %}
            </div>
            <br>
            <div class="ui buttons">
                <input class="ui blue button" type='button' id='AJAX_get' value="查询"/>
            </div>
        </form>
    </div>
</div>

<!-- 因为用到Semantic UI的Search Dropdown控件,必须有下面语句初始化 -->
<script>
    $('.ui.fluid.search.dropdown')
        .dropdown({ fullTextSearch: true });
</script>

这里首先我们第一次遇到了Django/Jinja2模板语法的集中应用,因为本文没有使用Django ORM,这种应用后续出场不多。我们只需要明白{% %}是功能标签,而{{ }}是变量标签,类似在模板层面的简单编程。而下方代码的意思是循环遍历后方传来的mselect_dict字典,字典的key是单选dimension_select下拉菜单选项的text,而value里嵌套的select键的值是菜单选项的value:

<select name="DIMENSION_select" id="DIMENSION_select" class="ui fluid search dropdown">
    {% for key, value in mselect_dict.items %}
        {% if value.select == 'PRODUCT' %}
            <option value="{{ value.select }}" selected>{{ key }}</option>
        {% else %}
            <option value="{{ value.select }}">{{ key }}</option>
        {% endif %}
    {% endfor %}
</select>

同理,后续又循环了一次mselect_dict,为根据字典内容生成若干个多选下拉菜单,注释掉的部分是后端动态生成备选项的一种解决方案,本文后半部分会涉及:

{% for key, value in mselect_dict.items %}
    <div class="field">
        <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}"
                multiple=""
                class="ui fluid search dropdown">
            <option value="">{{ key }}</option>
{#            {% for item in value.options %}#}
{#                <option value="{{ item }}">{{ item }}</option>#}
{#            {% endfor %}#}
        </select>
    </div>
{% endfor %}

这里有一个大坑是下面这句,可能会让人觉得很奇怪(这里的|add是tag filter,下一章会解释,这并不是最奇怪的地方):

<select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple=""
                            class="ui fluid search dropdown">

为什么<select name>要加个后缀[]?这是因为以后在jQuery传参时多选控件(实际就是传送array而不是单个变量的控件)的<select name>在很多场景下必须以[]结尾才能正确工作。

但有时[]也不是必须的,这里有一篇详细的文章阐释这个现象,感兴趣的可以做进一步参考,在此就不再赘述了。

How to send FormData objects with Ajax-requests in jQuery?​stackoverflow.com


此时再访问我们的主页http://127.0.0.1:8088/chpa/index,界面已经变成了下面这样:

46e7a10076fa9b9b2f7deb55b5ac6576.png

筛选框已经在那了,但下方的多选框点开还没选项,我们还需要一个步骤,从后端动态传入所有多选下拉菜单的备选选项。

此时有两种常用方法:

  • 在页面初始化时从后端提取所有字段的不重复值作为选项传入前端。
  • 在控件搜索时根据键入关键字实时从后端返回前n个相关备选项。

第一种方法的优点是简单直接。在上方的代码块中,我们其实已经预留了注释掉的相应的代码,将views.py的index方法修改成类似下面这样,增加option_list部分传至前端:

def index(request):
    
    ...

    mselect_dict = {}
    for key, value in D_FIELD.items():
        mselect_dict[key] = {}
        mselect_dict[key]['select'] = value
        mselect_dict[key]['options'] = option_list # option_list可以通过sql Distinct语句或Pandas的Unique方法获得,在此不再赘述
    
    ...

# 下面是一个获得各个字段option_list的简单方法
def get_distinct_list(column, db_table):
    sql = "Select DISTINCT " + column + " From " + db_table
    df = pd.read_sql_query(sql, ENGINE)
    l = df.values.flatten().tolist()
    return l

再在前端filter.html用下面的循环语句渲染<option></option>部分:

<h3 class="ui header" id="data_filter">数据筛选</h3>
<div class="field">
    {% for key, value in mselect_dict.items %}
    <div class="field">
        <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple=""
                class="ui fluid search dropdown">
            <option value="">{{ key }}</option>
            {% for item in value.options %}
                <option value="{{ item }}">{{ item }}</option>
            {% endfor %}
        </select>
    </div>
    {% endfor %}
</div>

很遗憾,功能是实现了,但用户体验很不好。因为我们部分字段的可选项过多,造成页面初始化加载很慢,并且点开选项较多的下拉菜单时反应也很慢。这也是初始化控件选项方法的最大缺点,不适应加载量太大的情况。

c450ec95ff8698752e714840a47c65c6.png
下拉菜单的Search Select功能实现了,但加载时间不可接受

但是我们必须使用search select功能,因为医药行业的专业术语太多了。于是考虑使用第二个方法,在控件搜索时根据键入关键字实时从后端返回前n个相关备选项,也就是我们说的on Server Response的方法。该方法适合表单可选项过多的场景。不使用Vue或React的情况下,Semantic UI的dropdown API就支持建设这种响应式搜索功能,并且官网提供了下方的例子:

Match Search Query on Server​semantic-ui.com

本例中实现这种方法确实要相对复杂。我们需要先在views.py建立search方法,该方法除request外包含2个参数,要查询的字段名和查询的字符串,返回不重复的匹配结果作为前端表单选项,格式为符合Semantic UI要求格式的json。

import json

def search(request, column, kw):
    sql = "SELECT DISTINCT TOP 10 %s FROM %s WHERE %s like '%%%s%%'" % (column, DB_TABLE, column, kw) # 最简单的单一字符串like,返回不重复的前10个结果
    try:
        df = pd.read_sql_query(sql, ENGINE)
        l = df.values.flatten().tolist()
        results_list = []
        for element in l:
            option_dict = {'name': element,
                           'value': element,
                           }
            results_list.append(option_dict)
        res = {
            "success": True,
            "results": results_list,
            "code": 200,
        }
    except Exception as e:
        res = {
            "success": False,
            "errMsg": e,
            "code": 0,
        }
    return HttpResponse(json.dumps(res, ensure_ascii=False), content_type="application/json charset=utf-8") # 返回结果必须是json格式

上面只是个匹配关键字的最简单例子,未来还可以继续完善,例如处理多个关键字,模糊查询等。

同时,我们需要在url.py编辑对应search方法的URL pattern,并用<>括号预留column和kw两个对应的参数位置:

urlpatterns = [
    ...
    path(r'search/<str:column>/<str:kw>', views.search, name='search')
]

此时可在浏览器输入上面的URL试试看效果,能看到已经正常返回预期的json了:

f6e907505cb8ad6b5ffef8684054c15d.png

最后参考Semantic UI官网的例子在前端模板文件filter.html末尾加上下面这段JS代码,将后台search方法和多选框绑定。注意下方代码相对复杂有好几个坑,我都在注释一一标出了:

<script>
    // 在JS中再次使用字段字典,要加|safe不转义
    var dict = {{ mselect_dict|safe }};
    // 还是转义问题,在Django模板中遇到带有{}的html代码必须使用replace这种方式处理
    var url = "{% url 'chpa:search' 'COLUMNPLACEHOLDER' 'QUERYPLACEHOLDER' %}".replace(
        'QUERYPLACEHOLDER', '{query}'
    );
    // jQuery语法遍历所有多选框
    $('.ui.fluid.search.dropdown.selection.multiple').each(function () {
        // Semantic UI语法获得多选框默认文本
        var text = $(this).dropdown('get default text');
        // 根据字典倒推该多选框是哪个字段
        var column = dict[text]['select'];
        $(this).dropdown(
            {
                apiSettings: {
                    // 用下方URL从后端返回查询后的json
                    url: url.replace('COLUMNPLACEHOLDER', column)
                },
                // 输入至少2个字符后才query
                minCharacters : 2
            })
        ;
    })
</script>

在评论区有人回复下面语句会出现bug:

        // Semantic UI语法获得多选框默认文本
        var text = $(this).dropdown('get default text');

虽然我个人没有碰到,但是如果有碰到的,可以考虑摒弃Semantic UI API,使用原生的JQuery语句:

        var text = $(this).children('select').children('option:first').text();

至此,我们终于完成了大部分前端表单交互的表面工作。本章内容比较繁杂,又第一次在项目中引入了二手程序员的天敌JS,我们在此停笔告一段落。下一章再讨论传参和异步加载的话题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值