树型表格的增删改查功能与数据表格的是完全一致,就是调用layui-form表单组件实现数据输入再提交,比较大的区别是树型节点的编辑,都需要有上级节点的输入,而这个上级节点的展示,必须是以树型方式展示出来。当然,树型也有多种模式展示,比如最简单,仍然是select选项域,只是选项用一些特征字符作为前缀,以显示出树型的错落出来。而复杂的展示模式,就是用真正的树型组件来展示,而layui也正好提供了树型组件Tree。
所以,本节树型表格的编辑功能重点还是介绍级节点树型输入域的构建过程,我构建是一个组合的输入,即一个基本的select选项树型和一个通过按钮点开的Tree组件树型。
程序主要包括四个部分,前端列表实现编辑页面展示、表单编辑页面、后端数据提取及编辑处理服务程序和树型数据生成程序。
第一个是上一节树型列表的表头工具栏和行内工具栏的具体处理程序,内容如下。这部分程序与数据列表的处理没有区别,不再详述。
<script>
layui.use(['jquery','layer','treeTable'], function(){
var $=layui.jquery
,layer=layui.layer
,table=layui.treeTable;
var cur_row = null;
var url_tree = '{{url_for("sysadm.branch_list")}}';
var url_edit = '{{url_for("sysadm.branch_edit")}}';
table.render({
elem: '#table_tree'
,height: 'full'
,url: url_tree
,toolbar: '#toolBar'
,method: 'POST'
,page: false
,limits: [13, 26, 39, 52, 65]
,limit : 13
,even : true
,size : 'sm'
,cols: [[
{ type: 'checkbox', fixed: 'left' }
,{field: 'id', title: 'ID', width:40, sort: true, fixed: 'left'}
,{field: 'name', title: '机构名称', width:140}
,{field: 'parent_id', title: 'PID', width:40, sort: true}
,{field: 'short_name', title: '简称', width:80}
,{field: 'branch_cd', title: '编码', width:60}
,{field: 'status_name', title: '状态', width:60}
,{field: 'category_name', title: '业务条线', width: 100}
,{field: 'type_name', title: '机构类型', width: 100}
,{field: 'regtime', title: '注册时间', width: 140}
,{fixed: 'right', width:200, align:'center', toolbar: '#linetoolBar'}
]]
,done: function () {
table.expandAll('table_tree', true);
}
});
//表头工具栏事件
table.on('toolbar(table_tree)', function (obj) {
switch (obj.event) {
case 'search':
tree_refresh(1);
break;
case 'add':
cur_row=null;
tree_edit('add','新增机构节点',0);
break;
case 'mdel':
tree_mdelete(1)
break;
};
});
//table行内工具栏事件
table.on('tool(table_tree)', function (obj) {
//obj是指这张表中的数据,将表中选中的行数据赋给cur_row变量
cur_row = obj.data;
var rid = cur_row.id;
switch(obj.event) {
case 'edit':
tree_edit('upd',"修改机构节点",rid);
break;
case 'adds':
tree_edit('adds',"新增下级机构",rid);
break;
case 'del':
if (cur_row.isParent == true) {
layer.msg(cur_row.name + cur_row.id + '有子机构结点,不能删除!!');
return false;
}
layer.confirm('确认删除吗?', {icon: 3, title:'提示'}, function(index){
$.post(url_edit + '?opr=del',{id:rid},function(rs){
if(rs.success){
//调用查询方法刷新数据
layer.msg(rs.msg,function(){});
tree_refresh(1);
}else{
layer.msg(rs.msg,function(){});
}
},'json');
layer.close(index);
});
break;
}
});
//弹出层显示记录编辑界面
function tree_edit(opr,title,uid){
layer.open({
type: 2,
title:title,
area: ['660px', '540px'], //宽高
skin: 'layui-layer-rim', //样式类名
content: url_edit + '?opr=' + opr + '&&id=' + uid, //编辑页面
btn:['保存','关闭'],
yes: function(index, layero){
tree_save(layero,opr);
},
success: function(layero, index, that){
},
btn2: function(index, layero){
layer.closeAll();
},
});
}
//存储表单数据
function tree_save(layero,opr) {
var iframeWin = window[layero.find('iframe')[0]['name']];
var vform = iframeWin.layui.form;
console.log('vform:' + JSON.stringify(vform));
vform.submit('edit-form',function(data){
console.log('data:' + JSON.stringify(data));
$.post(url_edit + '?opr=' + opr,
data.field,function(rs){
if(rs.success){
layer.closeAll();
layer.msg(rs.msg,function(){});
tree_refresh(1);
}else{
layer.msg(rs.msg,function(){});
}
},'json');
});
}
function tree_mdelete(cpage) {
var checkData = table.checkStatus('table_tree').data; //得到选中的数据
if (checkData.length === 0) {
layer.msg('请选择数据');
return false;
}
var idArr = [];
for (var i = 0; i < checkData.length; i++) {
idArr.push(checkData[i].id);
}
layer.confirm('确认批量删除吗?', {icon: 3, title:'提示'}, function(index){
$.post(url_edit + '?opr=mdel' ,
{id:idArr.join(',')},function(rs){
if(rs.success){
tree_refresh(1);
layer.msg(rs.msg);
}else{
layer.msg(rs.msg);
}
},'json');
layer.close(index);
});
}
function tree_refresh(cpage) {
table.reload('table_tree', {
where: {
'searchtext':$('#searchtext').val()
},
page: { curr: cpage },
},true);
}
});
</script>
第二部分是前端树型编辑的页面程序,包括Html+JavaScript,程序文件名为branch_edit.html.j2,主要内容如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>栏目编辑</title>
<link rel="stylesheet" href="/static/layui/css/layui.css" media="all">
</head>
<style>
.layui-form-select dl{
max-height:150px;
}
</style>
<body>
<div style="padding:10px;">
<form class="layui-form layui-form-pane" lay-filter="edit-form" action="">
<input type="hidden" id="id" name="id"/>
<div class="layui-form-item" >
<div class="layui-inline" style="width:70%">
<label class="layui-form-label">上级机构</label>
<div class="layui-input-block">
<select id="parent_id" name="parent_id" lay-search="">
<option value="0">---请选择---</option>
</select>
</div>
</div>
<div class="layui-inline">
<button id="btn_tree" type="button" class="layui-btn layui-btn-sm">
<i class="layui-icon layui-icon-cols"></i>树型
</button>
</div>
<div id="parent_tree" style="border:1px solid;border-color:#eee"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">机构名称</label>
<div class="layui-input-block">
<input type="text" id="branch_name" name="branch_name" autocomplete="off" placeholder="请输入机构名称" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">机构编码</label>
<div class="layui-input-block" >
<input type="text" id="branch_cd" name="branch_cd" autocomplete="off" placeholder="请输入机构编码" class="layui-input">
</div>
</div>
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">机构简称</label>
<div class="layui-input-block" >
<input type="text" id="short_name" name="short_name" autocomplete="off" placeholder="请输入机构简称" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">办公地址</label>
<div class="layui-input-block">
<input type="text" id="address" name="address" autocomplete="off" placeholder="请输入办公地址" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">电话</label>
<div class="layui-input-block" >
<input type="text" id="phone" name="phone" autocomplete="off" placeholder="请输入联系电话" class="layui-input">
</div>
</div>
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block" >
<input type="text" id="email" name="email" autocomplete="off" placeholder="请输入电子邮箱" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">业务条线</label>
<div class="layui-input-block" >
<select id="branch_cat" name="branch_cat">
<option value="00">---请选择---</option>
<option value="10">10_分支机构</option>
<option value="20">20_管理部门</option>
</select>
</div>
</div>
<div class="layui-inline" style="width:48%">
<label class="layui-form-label">机构类型</label>
<div class="layui-input-block" >
<select id="branch_type" name="branch_type">
<option value="00">---请选择---</option>
<option value="10">10_管理机构</option>
<option value="20">20_营业机构</option>
</select>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<select name="status">
<option value="">---请选择---</option>
<option value="0">0_启用</option>
<option value="1">1_停用</option>
</select>
</div>
</div>
</form>
</div>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['layer','form','jquery','tree'],function(){
var $=layui.jquery,
layer=layui.layer,
form=layui.form,
tree=layui.tree;
initDimension();
initFormData();
//由UID从服务器数据库中取出数据作为原始数据
function initFormData(){
rscode = null;
{% if rsdata %}
rscode = {{ rsdata.success }};
var rsmsg = '{{ rsdata.msg | safe }}';
var rsdata = {{ rsdata.data | tojson}};
if (rscode != null) {
form.val('edit-form',$.extend({}, rsdata||{}));//将父页面传递的行数据赋值到表单中
}
{% endif %}
}
//由后台取出选项条目数据对选项进行动态刷新
function initDimension() {
{% if rsdim %}
var opr_dim = {{ rsdim.opr_dim | safe }};
var branch_tlist = {{ rsdim.branch_tlist | safe }};
var branch_tree = {{ rsdim.branch_tree | safe }};
var status_dim = {{ rsdim.status_dim | safe }};
var category_dim = {{ rsdim.category_dim | safe }};
var type_dim = {{ rsdim.type_dim | safe }};
if (branch_tlist != null) set_select_tree(branch_tlist,'parent_id');
if (branch_tree != null) set_tree_render(branch_tree,'parent_tree');
if (status_dim != null) set_select_option(status_dim,'status');
if (category_dim != null) set_select_option(category_dim,'branch_cat');
if (type_dim != null) set_select_option(type_dim,'branch_type');
if (opr_dim.opr_funt == 'adds') {
disable_parent_tree(opr_dim.rid);
}
form.render('select');
{% endif %}
}
//设置select中的选项条目
function set_select_option(select_dim,sname) {
var $select = $('[name="'+ sname + '"]');
$select.empty();
for (var i = 0; i<select_dim.length; i++ ) {
option_item = select_dim[i];
$select.append($('<option>').text(option_item[0] + '_' + option_item[1]).attr('value', option_item[0]));
}
}
//设置select中的树型选项条目
function set_select_tree(select_dim,sname) {
var $select = $('[name="'+ sname + '"]');
$select.empty();
$select.append($('<option>').text('根结点_0').attr('value', 0));
for (var i = 0; i<select_dim.length; i++ ) {
option_item = select_dim[i];
let level = option_item[3];
let lstr ='├' + '─'.repeat(level)
$select.append($('<option>').text(lstr + option_item[1] + '_' + option_item[0]).attr('value', option_item[0]));
}
}
//绑定树型展开按钮的点击事件
$('#btn_tree').click(function(){
pt_elem = $('#parent_tree');
if (pt_elem.is(":hidden"))
pt_elem.show();
else
pt_elem.hide()
});
//渲染树型组件
function set_tree_render(itree,ielem) {
//console.log('tree:' + JSON.stringify(itree));
tree.render({
elem: '#'+ielem,
data: itree,
onlyIconControl: true, // 是否仅允许节点左侧图标控制展开收缩
size : 'sm',
click: function(obj){
//layer.msg(JSON.stringify(obj.data));
$('#parent_id').val(obj.data.id);
form.render('select');
$('#' + ielem).hide();
}
});
$('#'+ielem).hide();
}
//加子节点功能时,将父节点输入域及按钮设置为不可操作
function disable_parent_tree(uid) {
$('#parent_id').val(uid);
$('#parent_id').attr('disabled','disabled');
$('#btn_tree').attr('disabled','disabled');
}
});
</script>
</body>
</html>
编辑页面程序需要特别说明的是上级机构输入域的构成,前面说了,这是一个输入组合,可以通过普通select选项域选择机构,也可以展开layui-Tree树型,点击相应机构分支。
普通select选择输入域如下图所示,在前端选项初始化时,添加了所有选项的前置字符,以展现出树型的样式来,而实质仍然是select选择域。
机构条目可能很多,所以,建议在select定义中加入lay-search="",这样普通选项树型不但可以进行选择标准操作,还可以在选项里输入关键字进行检索匹配操作,以快速确定机构选项。
layui-Tree树型在初始状态是隐藏的,通过点击树型button可以展现出来,然后可以进行树型展开操作,并点击相应的机构条目选定机构,点击条目后树型自动关闭。再次展现需要再次点击树型button。
对于单选树型来说,加入layui-Tree展示只是提供更友好的界面。不过,如果是在树型里多选的话,普通select选项域是无法满足要求的,必须用加checkbox的树型才能实现这个功能。而layui-Tree组件无论是多选还是单选都能支持。多选树型的数据准备和控制比单选树型要复杂一些,但单选树型通了,多选也基本没啥难度,大家可以自己研究一下。
页面的JavaScript部分主要包括表单数据的初始化、表单选项域和树型的初始化,上述初始化所要求的数据都是由后端服务程序提供的,处理过程也与数据表格dataTalbe的处理类似。需要特殊说明的是disable_parent_tree(),这个函数是为增加子节点的功能而设置的,增加子节点功能要求上级机构是固定的值,不能改变。所以用disable_parent_tree()将相应的选择域和button都置成不能操作。
第三部分是后端服务处理程序,内容如下:
#机构信息编辑
@bp.route('/branch_edit/',methods=['GET','POST'])
@login_required
#@admin_auth
def branch_edit():
if request.method == 'GET':
opr = request.values.get('opr')
if opr == None:
opr = 'upd'
rid= request.values.get('id')
if rid == None:
rid = 0
branchTree = Branch_Tree()
udim = {
'opr_dim' : json.dumps({'opr_funt':opr,'rid':rid}),
'branch_tlist': json.dumps(branchTree.get_tlist()),
'branch_tree' : json.dumps(branchTree.get_tree()),
'status_dim' : json.dumps(Branch_Status().get_list()),
'category_dim' : json.dumps(Branch_Category().get_list()),
'type_dim' : json.dumps(Branch_Type().get_list()),
}
if opr != 'upd':
return render_template('admin/branch_edit.html.j2',rsdim=udim)
else:
irow = db.session.query(Branchs).filter_by(id=rid).first()
udata = dict(id=irow.id,branch_name=irow.branch_name,parent_id=irow.parent_id,
short_name= irow.short_name,branch_cd=irow.branch_cd,
email=irow.email,phone=irow.phone,address=irow.address,
branch_cat=irow.branch_cat,branch_type=irow.branch_type,
status=irow.status)
rsdata = {
"success": 1,
"msg": "取机构数据成功",
"data":udata
}
return render_template('admin/branch_edit.html.j2',rsdim=udim,rsdata=rsdata)
else :
opr = request.values.get('opr')
rid = request.values.get('id')
try :
if opr == 'add' or opr == 'adds':
rs_data = branch_add()
elif opr == 'upd' :
rs_data = branch_update(rid)
elif opr == 'del' :
rs_data = branch_delete(rid)
elif opr == 'mdel':
rs_data = branch_mdelete(rid)
else :
rs_data = {
'success':0,
'msg':'错误的操作码' + opr,
'status':200
}
except SQLAlchemyError as e:
db.session.rollback()
rs_data = {
'success':0,
'msg':'更新机构数据错误:' + str(e.orig),
'status':200
}
if rs_data.get('success') == 1:
Branch_Tree.init_dim()
return json.dumps(rs_data)
#新增机构
def branch_add():
logging.debug('Add Branch....')
parent_id = request.values.get('parent_id')
branch_cd = request.values.get('branch_cd')
branch_cat = request.values.get('branch_cat')
branch_type = request.values.get('branch_type')
branch_name = request.values.get('branch_name')
short_name = request.values.get('short_name')
email = request.values.get('email')
phone = request.values.get('phone')
address = request.values.get('address')
status = request.values.get('status')
rowadd = Branchs( branch_name=branch_name,parent_id=parent_id,
short_name= short_name,branch_cd=branch_cd,
email=email,phone=phone,address=address,
branch_cat=branch_cat,branch_type=branch_type,
status=status)
db.session.add(rowadd)
db.session.commit()
rs_data = {
'success':1,
'msg':'增加机构成功' + branch_cd + branch_name,
'status':200
}
return rs_data
#修改机构
def branch_update(uid):
logging.debug('update Branch %s....' % uid)
irow = db.session.query(Branchs).filter_by(id=uid).first()
irow.parent_id = request.values.get('parent_id')
irow.branch_cd = request.values.get('branch_cd')
irow.branch_cat = request.values.get('branch_cat')
irow.branch_type = request.values.get('branch_type')
irow.branch_name = request.values.get('branch_name')
irow.short_name = request.values.get('short_name')
irow.email = request.values.get('email')
irow.phone = request.values.get('phone')
irow.address = request.values.get('address')
irow.status = request.values.get('status')
db.session.commit()
rs_data = {
'success':1,
'msg':'修改机构信息成功',
'status':200
}
return rs_data
#删除机构--设置status标志为'9'
def branch_delete(uid):
logging.debug('Branch delete ' + uid)
irow = db.session.query(Branchs).filter_by(id=uid).first()
db.session.delete(irow)
db.session.commit()
rs_data = {
'success':1,
'msg':'删除机构成功' + uid,
'status':200
}
return rs_data
#批量删除机构
def branch_mdelete(ridstr):
logging.debug('Branch muli delete ' + ridstr)
ridlist = list(map(int,ridstr.split(',')))
rows = db.session.query(Branchs).filter(Branchs.id.in_(ridlist)).all()
for irow in rows:
db.session.delete(irow)
db.session.commit()
rs_data = {
'success':1,
'msg':'删除机构列表成功' + ridstr,
'status':200
}
return rs_data
后端数据获取和处理服务程序的路由命名为'/branch_edit/'。分为get和post两部分。
get部分是为编辑表单提供初始化数据的,提供的数据主要包括表单初值数据和表单选项数据两部。由于树型表单的特殊要求,服务程序将操作方式也表单选项数据下传,以便表单能够针对操作做特殊处理。
post部分是对数据增删改等操作进行统一的处理,需要特别说明的是在编辑功能完成后加了Branch_Tree.init_dim(),这是为了在修改机构数据后可以同步机构树型的数据,相应的数据处理是第四部分的内容。
第四部分是机构树型构建及树型选项列表以及Layui-Tree树型的数据生成程序,内容如下 :
#############机构树型维表处理
class Branch_Tree(Tree_Dimension):
dim_dict = None
def __init__(self) :
if self.dim_dict == None:
self.dim_dict = current_app.config.get('BRANCH_DIM')
if self.dim_dict ==None :
self.dim_dict = Branch_Tree.init_dim()
#初始化dim_dict变量
@staticmethod
def init_dim():
logging.debug('Initial Branch dim....')
rows = db.session.query(Branchs).filter_by(status=0).order_by(Branchs.id.desc()).all()
rows_info = {}
for irow in rows:
rows_info[irow.id] = dict(name=irow.short_name,pid=irow.parent_id)
current_app.config['BRANCH_DIM'] = rows_info
return rows_info
class Tree_Dimension(object) :
dim_dict = {}
# 初始化方法
def __init__(self,v_dim=None):
# 实例属性
if v_dim :
#logging.debug('dimention %s' % str(v_dim))
self.dim_dict = v_dim
def get_name(self,id) :
return self.dim_dict.get(id).get('name')
def get_list(self) :
auth_list = []
for (k,v) in self.dim_dict.items():
auth_item = [k,v['name'],v['pid'],0]
auth_list.append(auth_item)
return sorted(auth_list)
def id_format(self,id):
idname = self.dim_dict.get(id).get('name')
if isinstance(id, str) :
return id + '_' + idname
else :
return str(id) + '_' + idname
#为普通select输入域构建选项列表
def get_tlist(self) :
auth_dim = self.get_list()
auth_tlist = self.build_tlist(auth_dim,0,0)
return auth_tlist
def build_tlist(self,tlist,p_id,level):
treelist = []
for row in tlist:
if row[2] != p_id:
continue
row[3] = level
treelist.append(row)
treechild = self.build_tlist(tlist, row[0], level+1)
if treechild:
treelist.extend(treechild)
return treelist
#为LayUI Tree 生成数据
def get_tree(self) :
auth_dim = self.get_list()
auth_tree = self.build_tree(auth_dim,0,0)
return auth_tree
def build_tree(self,data,p_id,level=0):
tree = []
row = {}
for item in data:
if item[2] ==p_id:
row = dict(id = item[0], title= item[1] + '_' + str(item[0]), parent_id=item[2],level= level)
child = self.build_tree(data, row['id'], level+1)
row['children'] = []
if child:
row['children'] += child
tree.append(row)
return tree
树型选项采用了类方式来构建,tree_dimension是树型基础类,构建了一组功能函数,包括取树型节点的名称、取树型数据节点的条目。get_tlist用于构建普通选择域的选项列表,get_tree用于构建Layui-Tree组件所需要的数据。
通过上面几部分的程序,一个基本的树型表单编辑功能就完成了,主要的界面如下: