news 2026/8/11 20:19:27

政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成

政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成

文章目录

  • 政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成
    • 为什么不能做固定行列
    • 设计器整体结构
    • 表格的创建和自由布局
      • 创建表格
      • 合并单元格 `mc()`
      • 拆分单元格 `cf()`
    • 控件拖入表格
    • 属性编辑面板
    • 代码自动生成
      • 生成的函数列表
      • 保存方法的自动生成
    • 控件拖拽(单元格间移动)
    • 设计器的两种模式
    • 决策原则

非科班野生程序员,深耕政务信息化20年。政务审批表单有多复杂?请假申请要并排三列,报销审批要有合并单元格的大标题栏,采购审批要嵌套子表——固定行列的低代码方案根本搞不定。这篇拆解基于Dojo的可视化表单设计器怎么实现自由布局。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


为什么不能做固定行列

市面上很多低代码平台的表单设计器是这样的:配置几个字段,选几列布局,自动排列。

政务审批表单不行。一张采购审批表可能是这样的:

┌─────────┬─────────┬─────────┬─────────┐ │ 申请人 │ 申请部门│ 申请日期│ │ ├─────────┼─────────┼─────────┴─────────┤ │采购事项 │ (合并4列) │ ├─────────┼─────────┬─────────┬─────────┤ │物品名称 │ 数量 │ 单价 │ 金额 │ │物品名称 │ 数量 │ 单价 │ 金额 │ ├─────────┴─────────┴─────────┴─────────┤ │ 审批意见:(合并4列,文本域 │ └───────────────────────────────────────┘

有合并、有嵌套、有不同宽度——这就是为什么必须做可合并拆分的表格布局


设计器整体结构

formEdit.jsp是三栏布局:

┌──────────┬──────────────────────┬──────────┐ │控件与容器│ 工作区域 │属性/事件 │ │ │ │ │ │可创建控件│ 编辑 | 源码 | 脚本 │属性面板 │ │ (树形) │ │事件面板 │ │ │ │ │ │ 容器结构 │ [合并][拆分][取消] │ │ │ (树形) │ [插行][追行][删行] │ │ │ │ [载入][保存JSP][保存]│ │ └──────────┴──────────────────────┴──────────┘
  • 左栏:控件树(双击创建)+ 容器结构树(显示当前组件层级)
  • 中栏:工作区(拖拽设计)+ 源码预览 + 脚本编辑
  • 右栏:属性面板(中文名/英文名/类型/校验/默认值)+ 事件面板
  • 底部:合并/拆分/插入行/追加行/删除行/载入/保存JSP/保存

scriptUI目录下的10个JS文件各司其职:

文件职责
const.js控件注册表(定义可创建的控件列表)
values.js全局状态(组件树、SQL绑定、事件绑定)
optable.js表格操作(创建/合并/拆分/行操作)
opgrid.jsGrid操作(创建数据窗口)
coms.js控件创建(TextBox/DateTextBox/ComboBox/…)
optab.jsTab页签操作
optitle.js标题容器操作
uiglobal.js属性编辑 + 代码生成
treecommon.js树数据源管理

表格的创建和自由布局

创建表格

双击控件树的"表格",弹出配置对话框(tableTitle.jsp),输入行列数、宽度、绑定的SQL语句。确定后createTable()在工作区生成一个 HTML table:

functioncreateTable(value,pid){varcols=value.cols;varrows=value.rows;vartableNode=dojo.create("table",{width:width1,id:tid1});// 生成colgroup(等宽列)varcolgroup=dojo.create("colgroup");varw=parseInt(100/cols)+"%";for(vary=0;y<cols;y++){dojo.place(dojo.create("col",{width:w}),colgroup);}// 生成rows x cols的单元格for(varx=0;x<rows;x++){vartrNode=tableNode.insertRow();for(vary=0;y<cols;y++){vartdNode=trNode.insertCell();tdNode.setAttribute("class","td1");// 绑定事件:双击选中、拖拽、鼠标悬停dojo.query("#"+tdNode.id).connect("ondblclick",tddbclick);dojo.query("#"+tdNode.id).connect("onmousedown",tdmousedown);dojo.query("#"+tdNode.id).connect("onmouseup",tdmouseup);}}// 表格放在Form里(方便数据绑定)vartf=createform(fid1,width1,rows,cols,fbinding);dojo.place(tableNode,tf.containerNode,"last");}

表格外面自动套一个browise.form.Form,用于数据绑定。

合并单元格mc()

双击选中多个单元格(同一行或同一列),点"合并":

functionmc(){vartabObj=document.getElementById(tableid);varmaxcol=getmax(tableNodes,'col');varmincol=getmin(tableNodes,'col');varmaxrow=getmax(tableNodes,'row');varminrow=getmin(tableNodes,'row');// 校验:只能合并同行或同列if(maxcol-mincol>0&&maxrow-minrow>0){MessageBox.alert({message:"无法合并!只能选择同行或者同列"});return;}if(maxcol-mincol>0){// 横向合并:计算总colSpan,删除多余tdfor(vari=maxcol;i>=mincol;i--){colspans+=tabObj.rows[minrow].cells[i].colSpan;if(i!=mincol)tabObj.rows[minrow].deleteCell(i);}tabObj.rows[minrow].cells[mincol].colSpan=colspans;}else{// 纵向合并:计算总rowSpan,删除多余tdfor(vari=minrow;i<=maxrow;){rowspans+=tabObj.rows[i].cells[mincol].rowSpan;if(i!=minrow)tabObj.rows[i].deleteCell(mincol);i+=tabObj.rows[i].cells[mincol].rowSpan;}tabObj.rows[minrow].cells[mincol].rowSpan=rowspans;}}

拆分单元格cf()

选中一个已合并的单元格,点"拆分",还原为1x1并补上新的td:

functioncf(){varrowcount=curr.rowSpan;varcolcount=curr.colSpan;// 在对应位置插入新的tdfor(vari=0;i<rowcount;i++){for(varj=0;j<colcount;j++){if(j==0&&i==0)continue;// 原单元格保留vartdNode=tabObj.rows[i+currrowindex].insertCell(currcellindex+j);tdNode.setAttribute("class","td1");// 绑定事件...}}curr.colSpan=1;curr.rowSpan=1;}

合并拆分的难点是位置计算——前面有跨行单元格时,拆分要先检查位置占用,否则会错位。cf()里有一段遍历前面所有行的代码做这个检查。


控件拖入表格

双击左侧控件树创建控件,控件出现在当前选中的表格单元格里:

functionsetconts(txt,com,div,event){if(!curr){MessageBox.alert({message:"请选择单元格!"});returnfalse;}// 创建一个包裹divdiv=dojo.create("div",{width:"100%",id:"d_"+random()});coms[div.id]=com;// 注册到全局组件树divs[div.id]=div;if(event)aevent[com.id]=event;com.startup();dojo.place(div,curr,"last");// div放进单元格com.placeAt(div,"last");// 控件放进divdojo.connect(div,"onclick",click);// 点击显示属性curr.setAttribute("align","left");// 放控件后左对齐curr=null;clear1();}

支持的控件类型(coms.js):

函数控件说明
createtextTextBox普通文本
createtdDateTextBox日期选择
createcbComboBox下拉框(绑二级代码)
createtreecomComboBoxTree下拉树
createcbtnButton按钮
createrdRadioButtonGroup单选按钮组
createcbgCheckBoxGroup复选框组
createtaTextarea文本域
createcrtRichTextEditor2富文本编辑器
createTextIconTextBoxWithIcon图标文本(前置流程选择)

创建控件后自动在前面一格填上中文名标签:

functionsave(id,ots,json){if(curr&&curr.cellIndex>=1&&ots.cls!="browise.form.Button"){if(!curr.parentNode.cells[curr.cellIndex-1].innerText){curr.parentNode.cells[curr.cellIndex-1].innerText=ots.dis+":";}}ajsons[id]=json;// 保存组件配置afothers[id]=ots;// 保存其他参数}

属性编辑面板

点击控件,右侧属性面板显示所有可编辑属性(getPara()函数):

属性说明控件类型
中文名显示的标签文本所有
英文名数据绑定字段名(binding.name)非Button
二级代码ComboBox的数据源store名ComboBox/RadioButtonGroup/CheckBoxGroup
数据类型eae009编码非Button
校验正则表达式非Button
是否必录required属性非Button
是否禁用disabled属性非Button
默认值日期/用户/部门/电话等预设非Button
含时间DateTextBox是否显示时分秒DateTextBox
字体大小td的fontSize所有
对齐方式textAlign非Button

点"应用"按钮,apply()函数把修改写回控件实例。


代码自动生成

这是设计器最省时的功能。点"生成默认脚本"按钮(impdefault1()),自动根据当前设计的组件生成完整的JS代码:

生成的函数列表

函数功能什么时候有
seach_xxxGrid的查询方法Grid绑定了SQL时
commansave统一保存方法Form绑定了SQL时
del_xxxGrid的删除方法Grid绑定了SQL时
xz_xxxGrid的新增方法(弹窗)Grid配了新增界面时
xg_xxxGrid的修改方法(弹窗)Grid配了修改界面时
xxx_dblickGrid的双击事件(弹窗)Grid配了双击事件时
open_xxx前置流程选择弹窗TextBoxWithIcon配了前置流程时

保存方法的自动生成

create_save()为例,遍历所有Form和Grid,自动生成保存JS:

functioncreate_save(nodes){myscripts+="function commansave(){\r\n";myscripts+="\tvar dc = new browise.ds.DataCenter();\r\n";for(vari=0;i<nodes.length;i++){if(asql[nodes[i].id]&&asql[nodes[i].id].seach){// Form类型:先校验if(nodes[i].declaredClass=='browise.form.Form'){myscripts+="\tvar myform=browise.byId(\""+nodes[i].id+"\");\r\n";myscripts+="\tvar isValid=myform.validate();\r\n";myscripts+="\tif(!isValid){ alert('...'); return; }\r\n";}// 取DataStore,设置up/ins/del参数myscripts+="\tvar ds"+i+"=browise.byId(\""+nodes[i].id+"\").getBinding().getDataStore();\r\n";myscripts+="\tds"+i+".setParameter(\"up\",\""+sqlId+"\");\r\n";myscripts+="\tds"+i+".setParameter(\"ins\",\""+sqlId+"\");\r\n";myscripts+="\tds"+i+".setParameter(\"del\",\""+sqlId+"\");\r\n";myscripts+="\tdc.addDataStore(ds"+i+");\r\n";}}myscripts+="\tvar URL=webRootDir+'/BusinessAction?Business=CommonSQL&Action=CommonSaveByseach';\r\n";myscripts+="\tbrowise.Action.requestData(data, dc, true);\r\n";myscripts+="}\r\n";}

生成的代码直接利用了CommonSQLcontrol.CommonSaveByseach,用SELECT语句的元数据做增删改——不需要单独配INSERT/UPDATE/DELETE。


控件拖拽(单元格间移动)

支持单元格之间拖拽控件:

// mousedown:记录拖拽源functiontdmousedown(evt){tdojb=this;this.style.cursor='move';}// mouseup:放到目标单元格functiontdmouseup(evt){if(tdojb==null||tdojb==this)return;if(this.children[0]){// 目标有内容,确认覆盖if(!window.confirm("目标单元格里存在内容!确认覆盖?"))return;}if(tdojb.children[0]){dojo.place(tdojb.children[0],this,"last");// 移控件}else{this.innerText=tdojb.innerText;// 移文本tdojb.innerText="";}tdojb=null;}

也支持从控件树拖拽到单元格——mousedown记录树节点,mouseup时在目标单元格创建控件。


设计器的两种模式

UI/formEdit.jspUI2/formEdit.jsp有两个版本:

UI版本UI2版本
myedittype“1”“2”
oneSqlfalsetrue
用途列表+表单(查询页)纯表单(编辑页)
额外功能选择SQL语句、预览按钮
代码生成查询+Grid增删改+保存仅保存

UI模式用于设计"上Grid下条件"的查询页面,UI2模式用于设计表单页面(工作流绑定的那种)。


决策原则

用业务人员能理解的方式描述表单。

政务审批表单的本质是一张表格,业务人员习惯了Excel式的布局。给他们一个可以合并拆分的表格编辑器,比教他们配置JSON schema容易得多。控件拖进去、属性填上、代码自动生成,一个表单从设计到可用不超过半小时。


你项目里的表单设计器是怎么做的?是固定布局还是自由布局?欢迎评论区聊聊。


系列导航:

  • 总纲:[政务低代码平台实战——从元数据引擎到可视化设计器的五个关键决策]
  • 上一篇:[政务低代码平台实战③:CommonSQL——一个接口扛起所有CRUD]
  • 下一篇:[政务低代码平台实战⑤:双输出模式——存DB与导出JSP的完整链路]

作者:许彰午| 非科班野生程序员,深耕政务信息化20年

标签:#Java #低代码 #表单设计器 #拖拽 #合并拆分 #Dojo #政务信息化

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 20:18:19

结构光三维重建技术:多频外差法原理与应用

1. 结构光三维重建的基本原理 结构光三维测量技术近年来在工业检测、逆向工程、医疗影像等领域获得了广泛应用。这种非接触式测量方法的核心思想是通过投影特定的光栅图案到被测物体表面&#xff0c;然后通过相机捕捉变形后的图案&#xff0c;最后通过相位计算和相位展开算法重…

作者头像 李华
网站建设 2026/8/11 20:14:30

从玩具到生产:AI时代全栈开发实战解析

1. 项目概述&#xff1a;当玩具项目遇上全栈生产去年夏天&#xff0c;我在整理旧代码时发现一个被遗忘的Python小项目——用turtle模块模拟太阳系行星运行。这个当初只花了三小时写的"玩具"&#xff0c;如今在Google AI Studio和Antigravity的加持下&#xff0c;竟演…

作者头像 李华
网站建设 2026/8/11 20:11:33

fMRI-fNIRS联合成像:实现脑功能的多模态评估(附高分文献下载)

fMRI-fNIRS联合成像功能磁共振成像&#xff08;functional magnetic resonance imaging&#xff0c;fMRI&#xff09;能够以较高空间分辨率观察全脑活动和功能网络&#xff0c;包括丘脑、海马及基底节等深部结构&#xff0c;但对头动较为敏感&#xff0c;时间分辨率受血流动力学…

作者头像 李华
网站建设 2026/8/11 20:08:10

你的外贸网站适合做SEO吗?先看这4个硬性条件

带外贸团队这些年&#xff0c;我接过不少同事和同行的问题&#xff1a;“我们也想做SEO&#xff0c;但不知道网站适不适合。”我的回答一直很直接——不是所有网站都适合马上做SEO。有些网站先做SEO是锦上添花&#xff0c;有些先做是浪费时间。先搞清楚这4个条件&#xff0c;再…

作者头像 李华