说明
稍微修正、梳理一下之前的的做法。
目标:可以快速的重复使用,基本上前后端的表格交互我就用这一套了。
内容
1 三部分功能
前后端设计到三部分:
- 1 H5 元素: 前端页面的展示效果
- 2 Javascript:前后端的交互桥梁
- 3 View(Flask):使用python基本对象和DataFrame与JS通信。
假设现在是要做某个页面(view.html
), 梳理一下前后端的代码,Flask的view函数产生内容,然后render_template
填充内容。
2 view.html
某个功能的页面从所在的app导入索引页面(这样就不必单独设置导航条),新增的页面知识扩充了script,并且覆盖刷新的pagecontent。
{% extends '/app/index.html'%}
{%block head%}
{{super()}}
{%for some_script in myscrips%}
{{some_script|safe}}
{%endfor%}
{%endblock%}
{%block pagecontent%}
{%for row_content in rows%}
{{row_content|safe}}
{%endfor%}
{%endblock%}
3 view.py
将需要的脚本和h5元素一个个的写好,‘串起来’就可以了
@app.route('/view/', methods=['GET', 'POST'])
@login_required
def view():
# js脚本
scripts = []
script1 = render_template('/app/view_main.js')
scripts.append(script1)
...
# h5元素
rows = []
# 存储页面的临时存储变量
global_vars = render_template('/app3/global_vars.html')
rows.append(global_vars)
...
# 页面
return render_template('/app3/view0011.html', rows = rows, myscrips= scripts)
4 动态交互表格部分
这次的改进:根据js获取到的表格,动态的创建交互表格。
4.0 开始
在项目模板下创建一个文件夹,作为本次的应用。
然后:
- 1 创建蓝图
__init__.py
- 2 创建应用的主页
index.py
- 3 创建应用主页的html框架 ‘app/index.html’
这个基础模板有三个地方需要关注:
- 1 从最基础的网站模板扩展(已经定义很多bootstrap,jquery等资源)
{% extends 'base.html'%}
- 2 覆盖写入导航条部分
...
{%block navcontent%}
<div class="container-fluid"><a xtype="a" class="navbar-brand" href="#">Base_Web</a>
<ul xtype="h5_ele" class="navbar-nav navbar-top mr-5">
<li xtype="h5_ele" class="nav-item"><a xtype="h5_ele" href="{{url_for('main.index')}}" class="nav-link">主页</a>
</li>
<li xtype="h5_ele" class="nav-item"><a xtype="h5_ele" href="{{url_for('main.view4')}}" class="nav-link">Datatables测试</a>
</li>
<li xtype="h5_ele" class="nav-item"><a xtype="h5_ele" href="{{url_for('main.view5')}}" class="nav-link">Markdown测试</a>
</li>
...
</ul>
</div>
{%endblock%}
- 3 写一个应用的简介
{%block pagecontent%}
<!-- 空白行作为间隔 -->
<div class="m-5"></div>
<div class="row">
<div class="col md-12">
<div xtype="h5_ele" class="jumbotron">
<h1>Memos</h1>
<hr>
<p> 备忘表格通知</p>
<p> 重要点如下:</p>
<p> 1. 使用动态方法创建交互式表格</p>
<p> 2. 提供周期任务检查和提醒</p>
<p> 3. 进行邮件和短信通知</p>
</div>
</div>
</div>
{%endblock%}
效果如下
4.1 构造应用主页面
同样准备一个h5页面和一个view。
view1.html
从欢迎页扩展,里面只定义了脚本和h5元素的列表循环。
{% extends '/memos/index.html'%}
{%block head%}
{{super()}}
{%for some_script in myscrips%}
{{some_script|safe}}
{%endfor%}
{%endblock%}
{%block pagecontent%}
{%for row_content in rows%}
{{row_content|safe}}
{%endfor%}
{%endblock%}
view1.py
这里将根据实际情况逐个添加脚本/元素:
- 1 如果是通用的部分,就放在
templates/memos
下面 - 2 如果是该功能页的个性化部分,就放在
templates/memos/view1/
下面
from . import memos
from flask import render_template, current_app
from funcs import graph
@memos.route('/view1/', methods=['GET','POST'])
def view1():
# js脚本
scripts = []
...
rows = []
...
# 存储页面的临时存储变量
global_vars = render_template('/memos/global_vars.html')
rows.append(global_vars)
# 页面
return render_template('/memos/view1.html', rows = rows, myscrips= scripts)
4.2 构造数据函数
假设某个视图会从数据库获取数据并形成一个dataframe。
-
1 三套列名
- 1 数据列:实际上真实需要编辑的数据列
- 2 后端列:关于数据行的状态,前后端交互时使用。主要是序号、同步状态和操作类型。
- 3 前端列:只用在前端的列,目前看主要就是checkbox那一列。
三套列名 | checkbox | 序号 | 需要编辑的列 | 同步状态 | 操作类型 |
---|---|---|---|---|---|
数据列 | X | X | Y | X | X |
后端列 | X | Y | Y | Y | Y |
前端列 | Y | Y | Y | Y | Y |
- 2 将dataframe转为h5
dataframe 数据
x1 x2 y
0 0.066579 0.679521 2.017807
1 0.859233 0.663780 -0.307603
这个函数会把df转换为html,并将数据列变为后端列
# 从df生成datatables需要的html
def df2dttable(some_df, keep_cols,replace_str):
some_df1 = some_df[keep_cols]
# 序号
some_df1.insert(0, '序号', range(len(some_df1)))
# checkbox
some_df1.insert(0, '','')
# 编辑类型
some_df1.insert(len(some_df1.columns),'操作类型','查询')
# 输出html
return some_df1.to_html(index=False).replace('''class="dataframe"''', replace_str)
这里还是需要使用一个小函数来创建需要替换的元素属性,默认生成的class不是我们想要的,也没有id属性。
# 替换h5元素属性
def a_replace_h5attr(para_dict):
res = ''
for k in para_dict.keys():
res += ''' %s="%s" ''' %(k, para_dict[k])
return res
使用的时候,这样就完成了df向普通的table转换,并且加上了id
...
df = pd.DataFrame(np.random.randn(2,3), columns=['x1','x2', 'y'])
para_dict = {'id':'memo_table', 'class':'display'}
replace_str = a_replace_h5attr(para_dict)
res_h5 = df2dttable(df, list(df.columns), replace_str)
...
<table border="1" id="memo_table" class="display" >
<thead>
<tr style="text-align: right;">
<th></th>
<th>序号</th>
<th>x1</th>
<th>x2</th>
<th>y</th>
<th>操作类型</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>0</td>
<td>0.066579</td>
<td>0.679521</td>
<td>2.017807</td>
<td>查询</td>
</tr>
<tr>
<td></td>
<td>1</td>
<td>0.859233</td>
<td>0.663780</td>
<td>-0.307603</td>
<td>查询</td>
</tr>
</tbody>
</table>
4.3 交互式表格h5模板
这个模板大致分为三部分:
- 1 按钮组。提供对表格的增删改。
- 2 模态框。增加,修改行数据是通过模态框来编辑的。
- 3 数据表格。将上面的dataframe数据“灌”进去就可以了。
<!-- 空白行作为间隔 -->
<div class="m-5"></div>
<div class="row">
<div class="col md-12">
<h3>{{title}}</h3><hr>
</div>
</div>
<div class="row" id="{{row_id}}_btn">
<div class="col md-12" id="{{col_id}}_btn">
<!-- ======================> 按钮组 -->
<div class="btn-group operation">
<button id="{{col_id}}_btn_add" type="button" class="btn btn-primary mx-2" data-toggle="modal">
<!-- bootstrap4已经不默认提供图标了,而是分开了,需要在别的地方下载,并使用方法也有所变化 -->
<i class="fa fa-plus" aria-hidden="true">新增</i>
</button>
<button id="{{col_id}}_btn_edit" type="button" class="btn btn-info mx-2">
<i class="fa fa-pencil" aria-hidden="true">修改</i>
</button>
<button id="{{col_id}}_btn_delete" type="button" class="btn btn-danger mx-2">
<i class="fa fa-remove" aria-hidden="true">删除</i>
</button>
<button id="{{col_id}}_btn_submmit" type="button" class="btn btn-success mx-2">
<i class="fa fa-paper-plane" aria-hidden="true">批量提交</i>
</button>
</div>
<!-- ======================> 新增模态框 -->
{{add_modal|safe}}
<!-- ======================> 修改模态框 -->
{{mod_modal|safe}}
</div>
</div>
<!-- 空白行作为间隔 -->
<div class="m-2"></div>
<!-- 以下这部分是表格内容 -->
<div class="row" id="{{row_id}}">
<div class="col md-12" id="{{col_id}}">
</div>
</div>
4.3.1 动态的部分
因为行数据的修改是通过模态框进行的,因此需要对模态框中可编辑的元素进行声明:我希望直接通过给定df,根据其列名自动的生成模态框。
其中some_part
的作用是区别一个页面中的多套动态表格。动态的核心部分使用了jinja的字典循环。
add_modal.html
<!-- 点击添加按钮的时候弹出的就是模态框,其本身看起来像一个网页 modal fade -> modal-dialog -> modal-content -> modal-header | ->addBookModal - footer -->
<div class="modal fade" id="{{some_part}}_addRow" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<!-- 直接用css来控制关闭模态框 -->
<button type="button" class="close" data-dismiss="modal"><i class="fa fa-window-close" aria-hidden="true"> 添加数据</i></button>
</h5>
</div>
<div id="{{some_part}}_addRowModal" class="modal-body">
<!-- k是数据库的变量,v是显示在页面上的中文 -->
<div class="form-horizontal">
{%for k,v in var_dict.items()%}
<div class="form-group">
<label for="{{some_part}}_add_{{k|e}}" class="col-sm-2 control-label">{{v|e}}:*</label>
<div class="col-sm-10">
<input class="form-control" id="{{some_part}}_add_{{k|e}}" type="text">
</div>
</div>
{% endfor%}
</div>
</div>
<div class="modal-footer">
<div class="center-block">
<button id="{{some_part}}_cancelAdd" type="button" class="btn btn-warning" data-dismiss="modal">取消</button>
<button id="{{some_part}}_addRowsInfo" type="button" class="btn btn-success" data-dismiss="modal">保存</button>
</div>
</div>
</div>
</div>
</div>
修改模态框是类似的
mod_modal.html
<div class="modal fade" id="{{some_part}}_editRow" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<!-- 直接用css来控制关闭模态框 -->
<button type="button" class="close" data-dismiss="modal"><i class="fa fa-window-close"
aria-hidden="true">修改数据</i></button>
</h5>
</div>
<div id="{{some_part}}_editRowModal" class="modal-body">
<div class="form-horizontal">
{%for k,v in var_dict.items()%}
<div class="form-group">
<label for="{{some_part}}_mod_{{k|e}}" class="col-sm-2 control-label">{{v|e}}:*</label>
<div class="col-sm-10">
<input class="form-control" id="{{some_part}}_mod_{{k|e}}" type="text">
</div>
</div>
{% endfor%}
</div>
</div>
<div class="modal-footer">
<div class="center-block">
<button id="{{some_part}}_cancelEdit" type="button" class="btn btn-warning"
data-dismiss="modal">取消</button>
<button id="{{some_part}}_saveEdit" type="button" class="btn btn-success"
data-dismiss="modal">保存</button>
</div>
</div>
</div>
</div>
</div>
4.4 数据视图返回的内容
此时的数据视图可以返回动态表格所需的所有内容了。注意数据表格的数据部分用memo_table
作为id, 而动态模态框部分用memo_dynamic_col
作为id。
@memos.route('/view0002/', methods=['GET','POST'])
def view0002():
web1 = dm.WebMsg('view0002')
df = pd.DataFrame(np.random.randn(2,3), columns=['x1','x2', 'y'])
df['_update_status'] =1
# print(df)
# 数据框部分
para_dict = {'id':'memo_table', 'class':'display'}
replace_str = a_replace_h5attr(para_dict)
res_h5 = df2dttable(df, list(df.columns), replace_str)
# print(res_h5)
# 模态框部分
# 原始的列名。不可以下划线开头
original_cols = [x for x in list(df.columns) if not x.startswith('_')]
# 模态框显示的列名:这次使用原名,根据需要可以更改
showname_cols = [x for x in list(df.columns) if not x.startswith('_')]
memo_dynamic_dict = dict(zip(df.columns, df.columns))
# 动态参数化-新增模态框
memo_dynamic_add_modal_content = render_template('/memos/interactive_table/add_modal.html',
some_part ='memo_dynamic_col', var_dict = memo_dynamic_dict)
# 动态参数化-修改模态框
memo_dynamic_mod_modal_content = render_template('/memos/interactive_table/mod_modal.html',
some_part ='memo_dynamic_col', var_dict = memo_dynamic_dict)
# 交互式表格
interactive_table_content = render_template('/memos/interactive_table/interactive_table.html',title='备忘',
row_id='memo_dynamic_row', col_id='memo_dynamic_col',
add_modal = memo_dynamic_add_modal_content,
mod_modal = memo_dynamic_mod_modal_content)
interactive_table_h5 = interactive_table_content
the_data = {'res_h5':res_h5, 'interactive_table_h5':interactive_table_h5,
'edit_cols':original_cols,'edit_cols_name':showname_cols }
web1.status = True
web1.msg = 'ok'
web1.data = json.dumps(the_data)
print(res_h5)
return jsonify(web1.to_dict())
4.5 页面的js
现在有了视图函数提供数据,也有了前端h5元素承载数据,要“动”起来还需要js。js可以分为定义部分和运行部分,类似于python的def xxx
定义函数,然后if __name__=='__main__'
来执行一个脚本。
也就是说,主页面的js调用view0002
这个视图生成的内容填充生成具体的内容。
4.5.1 初次静态载入
发现零零碎碎的js还比较多,下面就梳理必要的几个js
main.js
页面载入后发起一个请求,从view0002
获取初始的数据
<script>
// 载入页面后执行
$(function(){
// 这里不需要参数,仅仅是为了保持结构
para_dict = {}
para_dict_str = JSON.stringify(para_dict)
target_url = '/memos/view0002/'
general_ajax_post(target_url, para_dict_str, success_func= success_memo_table)
})
</script>
发起一个ajax请求,可以指定成功后的函数
general_ajax_post.js
<script>
function null_func(x){}
function general_ajax_post(target_url, para_dict_str,
success_func = null_func,
error_func = null_func,
timeout = 3000) {
$.ajax({
type:'POST',
url:target_url,
data:para_dict_str,
timeout:timeout,
processData:false,
contentType:'application/json',
success:function(res_data){
success_func(res_data)
},
error:function(){
error_func()
}
})
}
</script>
成功后执行的脚本
success_memo_table.js
<script>
function success_memo_table(res_data) {
cur_status = res_data.status
cur_msg = res_data.msg
cur_data = JSON.parse(res_data.data)
cur_interactive_table_h5 = cur_data.interactive_table_h5
cur_data_res_h5 = cur_data.res_h5
cur_data_edit_cols = cur_data.edit_cols
cur_data_edit_cols_name = cur_data.edit_cols_name
// 模态框部分更新
$('#memo_dynamic_table_col').html(cur_interactive_table_h5)
if(cur_status){
$('#memo_table_col').empty()
$('#memo_table_col').html(cur_data_res_h5)
$('#memo_table').DataTable()
format_table_col('memo_table', 0, format_checkbox)
// 增加选择样式
$('#memo_table').find('tbody').find('tr').attr('onclick','select_element_toggle(this)')
// 列数 - 格式化倒数第二列
col_num = $('#memo_table').find('tbody').find('tr').eq(0).find('td').length
format_table_col('memo_table', col_num-2, vmap)
}
else{
prepend_ele('#global_vars', warn_msg('表格数据加载失败'))
}
}
</script>
4.5.3 增删改按钮的动作
在success_memo_table.js
中,如果动态表格返回有值,那么增加这些按钮的动作
4.5.3.1 增
// --------> 按钮的交互
$('#memo_dynamic_col_btn_add').click(function(){
$('#memo_dynamic_col_addRow').modal()
})
点击保存按钮时,要把数据添加到表格末端。通过把新增模态框中的数据读过来附加到最后一行
// 增加行
$('#memo_dynamic_col_addRowsInfo').on('click', function(){
add_row_func('memo_table','memo_dynamic_col_addRowModal','memo_dynamic_col' ,cur_data_edit_cols)
})
// 对应的函数
<script>
function add_row_func(table_id, modal_id,part_id,edit_cols){
// alert(table_id)
the_table = $('#'+table_id).DataTable()
// 行号
row_num = $('#' + table_id).find('tbody').find('tr').length
// 将行的值放入列表
addRowInfos = []
// 推入固定的第一、第二列
addRowInfos.push('<input type="checkbox" checked=true>');
addRowInfos.push(row_num);
// 如果模态框内容不为空,那么执行推入
alert('sss')
if($('#'+modal_id).find('input')!==''){
// 推入编辑列
for(i=0;i<edit_cols.length;i++){
col_id = '#' + part_id + '_add_' + edit_cols[i]
if($(col_id).val()){
addRowInfos.push($(col_id).val())
}
else{
addRowInfos.push(' ')
}
}
// 倒数第二列
addRowInfos.push(vmap(0))
// 最后一列
addRowInfos.push('新增')
// 写入表
the_table.row.add(addRowInfos).draw()
}
else{
alert('不能为空')
}
}
</script>
4.5.3.2 改
改按钮需要读取现有表的内容
// 修改
$('#memo_dynamic_col_btn_edit').click(function(){
call_edit_modal('memo_table','memo_dynamic_col')
})
对应的call_edit_modal.js
如下
<script>
function call_edit_modal(table_id,part_id){
the_table = $('#'+table_id).DataTable()
if(the_table.rows('.selected').data().length){
// 唤起模态框
$('#'+part_id + '_editRow').modal()
// 行内数据
rowData = the_table.rows('.selected').data()[0]
// 模态框数据
inputs = $('#' + part_id + '_editRowModal').find('input')
for(var i=0;i<inputs.length;i++){
$(inputs[i]).val(rowData[i+2])
}
}
else{
alert('未选中行')
}
}
</script>
对于保存按钮(memo_dynamic_col_saveEdit
)的动作
// 修改行保存
$('#memo_dynamic_col_saveEdit').on('click', function(){
edit_row_func('memo_table','memo_dynamic_col' ,cur_data_edit_cols)
})
<script>
function edit_row_func(table_id, part_id,edit_cols){
curTr = $('#'+table_id).find('input[type="checkbox"]:checked').first().closest('tr')
tds = $(curTr).find('td')
// alert('working')
for(i=0;i<edit_cols.length;i++){
new_td_content = $('#' + part_id +'_mod_' +edit_cols[i]).val()
$(tds[i+2]).html(new_td_content)
}
// 倒数第二列
$(tds[tds.length-2]).html(vmap(0))
// 倒数第一列
$(tds[tds.length-1]).html('修改')
}
</script>
4.5.3.3 删
不必唤起模态框,因此没有保存按钮
// 删除
$('#memo_dynamic_col_btn_delete').click(function(){
delete_row_func('memo_table')
})
<script>
function delete_row_func(table_id){
$('#'+table_id).find('input[type="checkbox"]:checked').each(function(){
tds = $(this).closest('tr').children('td')
last_one = tds.length -1
last_two = last_one -1
$(tds).eq(last_one).html('删除')
$(tds).eq(last_two).html(vmap(0))
})
}
</script>
4.5.2 批量提交
获取勾选的数据
<script>
function updated_rows_data(table_id) {
//alert(table_id)
data_list = []
$('#'+table_id).find('input[type="checkbox"]:checked').each(function(){
curTr = $(this).closest('tr')
tem_list = []
for(i=1;i<curTr.children('td').length;i++){
the_value = $(curTr).children('td').eq(i).text()
tem_list.push(the_value)
}
data_list.push(tem_list)
})
return data_list
}
</script>
提交到后台的数据类似
{'data': [['0', '-0.756943', '-0.786949', '0.497998', '', '查询']],
'meta': {'edit_cols': ['x1', 'x2', 'y'], 'edit_cols_name': ['x1', 'x2', 'y']}}
这是和后台交互最重要的一步
获取checkbox 选中的行向后台提交
获取选中的行
<script>
function updated_rows_data(table_id) {
//alert(table_id)
data_list = []
$('#'+table_id).find('input[type="checkbox"]:checked').each(function(){
curTr = $(this).closest('tr')
tem_list = []
for(i=1;i<curTr.children('td').length;i++){
the_value = $(curTr).children('td').eq(i).text()
tem_list.push(the_value)
}
data_list.push(tem_list)
})
return data_list
}
</script>
在success_memo_table.js
中获取checkbox选中的行,向目标/memos/view0003/
发起提交请求,并约定使用success_memo_table_submit
函数处理返回的数据。
// 提交
$('#memo_dynamic_col_btn_submmit').on('click', function(){
data_list = updated_rows_data('memo_table')
//alert(data_list[0])
target_url = '/memos/view0003/'
para_dict = {}
para_dict['data'] = data_list
para_dict['meta'] = {}
para_dict['meta']['edit_cols'] = cur_data_edit_cols_name
para_dict['meta']['edit_cols_name'] = cur_data_edit_cols_name
para_dict_str = JSON.stringify(para_dict)
general_ajax_post(target_url, para_dict_str,success_func=success_memo_table_submit)
})
后台处理后返回前端
后端要和数据库交互,进行数据库更新,然后返回更新后的状态。下面构造了一个简单的例子,实际使用时用真实的数据库连接替代就可以了。
@memos.route('/view0003/', methods=['GET','POST'])
def view0003():
web1 = dm.WebMsg('view0003')
input_dict = request.get_json()
print(input_dict)
edit_cols = input_dict['meta']['edit_cols']
pre_cols = ['tmp_row_id']
suf_cols = ['_update_status','opr_type']
res_df = pd.DataFrame([['0', '-999', '-111', '222', '1', '查询']], columns = pre_cols + edit_cols + suf_cols)
res_data = {}
res_data['cols'] = pre_cols + edit_cols + suf_cols
res_data['data_list'] = json.loads(res_df.to_json(orient='records'))
res_data['update_status_col'] = '_update_status'
res_data['row_id_col'] = 'tmp_row_id'
web1.status = True
web1.msg = 'ok'
web1.data = json.dumps(res_data)
return jsonify(web1.to_dict())
前端使用返回的数据刷新表格
<script>
function success_memo_table_submit(res_data) {
cur_status = res_data.status
cur_msg = res_data.msg
cur_data = JSON.parse(res_data.data)
cur_data_list = cur_data['data_list']
cur_cols = cur_data['cols']
cur_update_status_col = cur_data['update_status_col']
cur_row_id_col = cur_data['row_id_col']
if(cur_status){
// alert('here')
for(i=0;i<cur_data_list.length;i++){
tmp_row_id = cur_data_list[i][cur_row_id_col]
cur_trs = $('#memo_table').find('tbody').find('tr')
cur_tds = $(cur_trs).eq(tmp_row_id).children('td')
for(j=0;j<cur_cols.length ;j++){
col_name = cur_cols[j]
if(col_name !== cur_update_status_col){
cur_tds.eq(j+1).html(cur_data_list[i][col_name])
}
else{
cur_tds.eq(j+1).html(vmap(cur_data_list[i][col_name]))
}
}
}
}
else{
prepend_ele('#global_vars', warn_msg('项目提交数据刷新失败'))
}
}
</script>
5 数据库的增删改操作
这块和普通的数据库读写略有差别,代码方面的内容还是有点多,另起一章。