政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成
政务低代码平台实战④:可视化表单设计器——拖拽、合并拆分、代码生成
文章目录
非科班野生程序员,深耕政务信息化20年。政务审批表单有多复杂?请假申请要并排三列,报销审批要有合并单元格的大标题栏,采购审批要嵌套子表——固定行列的低代码方案根本搞不定。这篇拆解基于Dojo的可视化表单设计器怎么实现自由布局。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。
为什么不能做固定行列
市面上很多低代码平台的表单设计器是这样的:配置几个字段,选几列布局,自动排列。
政务审批表单不行。一张采购审批表可能是这样的:
┌─────────┬─────────┬─────────┬─────────┐
│ 申请人 │ 申请部门│ 申请日期│ │
├─────────┼─────────┼─────────┴─────────┤
│采购事项 │ (合并4列) │
├─────────┼─────────┬─────────┬─────────┤
│物品名称 │ 数量 │ 单价 │ 金额 │
│物品名称 │ 数量 │ 单价 │ 金额 │
├─────────┴─────────┴─────────┴─────────┤
│ 审批意见:(合并4列,文本域 │
└───────────────────────────────────────┘
有合并、有嵌套、有不同宽度——这就是为什么必须做可合并拆分的表格布局。
设计器整体结构
formEdit.jsp 是三栏布局:
┌──────────┬──────────────────────┬──────────┐
│控件与容器│ 工作区域 │属性/事件 │
│ │ │ │
│可创建控件│ 编辑 | 源码 | 脚本 │属性面板 │
│ (树形) │ │事件面板 │
│ │ │ │
│ 容器结构 │ [合并][拆分][取消] │ │
│ (树形) │ [插行][追行][删行] │ │
│ │ [载入][保存JSP][保存]│ │
└──────────┴──────────────────────┴──────────┘
- 左栏:控件树(双击创建)+ 容器结构树(显示当前组件层级)
- 中栏:工作区(拖拽设计)+ 源码预览 + 脚本编辑
- 右栏:属性面板(中文名/英文名/类型/校验/默认值)+ 事件面板
- 底部:合并/拆分/插入行/追加行/删除行/载入/保存JSP/保存
scriptUI目录下的10个JS文件各司其职:
| 文件 | 职责 |
|---|---|
const.js | 控件注册表(定义可创建的控件列表) |
values.js | 全局状态(组件树、SQL绑定、事件绑定) |
optable.js | 表格操作(创建/合并/拆分/行操作) |
opgrid.js | Grid操作(创建数据窗口) |
coms.js | 控件创建(TextBox/DateTextBox/ComboBox/…) |
optab.js | Tab页签操作 |
optitle.js | 标题容器操作 |
uiglobal.js | 属性编辑 + 代码生成 |
treecommon.js | 树数据源管理 |
表格的创建和自由布局
创建表格
双击控件树的"表格",弹出配置对话框(tableTitle.jsp),输入行列数、宽度、绑定的SQL语句。确定后 createTable() 在工作区生成一个 HTML table:
function createTable(value, pid) {
var cols = value.cols;
var rows = value.rows;
var tableNode = dojo.create("table", { width: width1, id: tid1 });
// 生成colgroup(等宽列)
var colgroup = dojo.create("colgroup");
var w = parseInt(100 / cols) + "%";
for (var y = 0; y < cols; y++) {
dojo.place(dojo.create("col", { width: w }), colgroup);
}
// 生成rows x cols的单元格
for (var x = 0; x < rows; x++) {
var trNode = tableNode.insertRow();
for (var y = 0; y < cols; y++) {
var tdNode = 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里(方便数据绑定)
var tf = createform(fid1, width1, rows, cols, fbinding);
dojo.place(tableNode, tf.containerNode, "last");
}
表格外面自动套一个 browise.form.Form,用于数据绑定。
合并单元格 mc()
双击选中多个单元格(同一行或同一列),点"合并":
function mc() {
var tabObj = document.getElementById(tableid);
var maxcol = getmax(tableNodes, 'col');
var mincol = getmin(tableNodes, 'col');
var maxrow = getmax(tableNodes, 'row');
var minrow = getmin(tableNodes, 'row');
// 校验:只能合并同行或同列
if (maxcol - mincol > 0 && maxrow - minrow > 0) {
MessageBox.alert({ message: "无法合并!只能选择同行或者同列" });
return;
}
if (maxcol - mincol > 0) {
// 横向合并:计算总colSpan,删除多余td
for (var i = 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,删除多余td
for (var i = 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:
function cf() {
var rowcount = curr.rowSpan;
var colcount = curr.colSpan;
// 在对应位置插入新的td
for (var i = 0; i < rowcount; i++) {
for (var j = 0; j < colcount; j++) {
if (j == 0 && i == 0) continue; // 原单元格保留
var tdNode = tabObj.rows[i + currrowindex].insertCell(currcellindex + j);
tdNode.setAttribute("class", "td1");
// 绑定事件...
}
}
curr.colSpan = 1;
curr.rowSpan = 1;
}
合并拆分的难点是位置计算——前面有跨行单元格时,拆分要先检查位置占用,否则会错位。cf() 里有一段遍历前面所有行的代码做这个检查。
控件拖入表格
双击左侧控件树创建控件,控件出现在当前选中的表格单元格里:
function setconts(txt, com, div, event) {
if (!curr) {
MessageBox.alert({ message: "请选择单元格!" });
return false;
}
// 创建一个包裹div
div = 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"); // 控件放进div
dojo.connect(div, "onclick", click); // 点击显示属性
curr.setAttribute("align", "left"); // 放控件后左对齐
curr = null;
clear1();
}
支持的控件类型(coms.js):
| 函数 | 控件 | 说明 |
|---|---|---|
createtext | TextBox | 普通文本 |
createtd | DateTextBox | 日期选择 |
createcb | ComboBox | 下拉框(绑二级代码) |
createtreecom | ComboBoxTree | 下拉树 |
createcbtn | Button | 按钮 |
createrd | RadioButtonGroup | 单选按钮组 |
createcbg | CheckBoxGroup | 复选框组 |
createta | Textarea | 文本域 |
createcrt | RichTextEditor2 | 富文本编辑器 |
createTextIcon | TextBoxWithIcon | 图标文本(前置流程选择) |
创建控件后自动在前面一格填上中文名标签:
function save(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_xxx | Grid的查询方法 | Grid绑定了SQL时 |
commansave | 统一保存方法 | Form绑定了SQL时 |
del_xxx | Grid的删除方法 | Grid绑定了SQL时 |
xz_xxx | Grid的新增方法(弹窗) | Grid配了新增界面时 |
xg_xxx | Grid的修改方法(弹窗) | Grid配了修改界面时 |
xxx_dblick | Grid的双击事件(弹窗) | Grid配了双击事件时 |
open_xxx | 前置流程选择弹窗 | TextBoxWithIcon配了前置流程时 |
保存方法的自动生成
以 create_save() 为例,遍历所有Form和Grid,自动生成保存JS:
function create_save(nodes) {
myscripts += "function commansave(){\r\n";
myscripts += "\tvar dc = new browise.ds.DataCenter();\r\n";
for (var i = 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:记录拖拽源
function tdmousedown(evt) {
tdojb = this;
this.style.cursor = 'move';
}
// mouseup:放到目标单元格
function tdmouseup(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.jsp 和 UI2/formEdit.jsp 有两个版本:
| UI版本 | UI2版本 | |
|---|---|---|
myedittype | “1” | “2” |
oneSql | false | true |
| 用途 | 列表+表单(查询页) | 纯表单(编辑页) |
| 额外功能 | — | 选择SQL语句、预览按钮 |
| 代码生成 | 查询+Grid增删改+保存 | 仅保存 |
UI模式用于设计"上Grid下条件"的查询页面,UI2模式用于设计表单页面(工作流绑定的那种)。
决策原则
用业务人员能理解的方式描述表单。
政务审批表单的本质是一张表格,业务人员习惯了Excel式的布局。给他们一个可以合并拆分的表格编辑器,比教他们配置JSON schema容易得多。控件拖进去、属性填上、代码自动生成,一个表单从设计到可用不超过半小时。
你项目里的表单设计器是怎么做的?是固定布局还是自由布局?欢迎评论区聊聊。
系列导航:
- 总纲:[政务低代码平台实战——从元数据引擎到可视化设计器的五个关键决策]
- 上一篇:[政务低代码平台实战③:CommonSQL——一个接口扛起所有CRUD]
- 下一篇:[政务低代码平台实战⑤:双输出模式——存DB与导出JSP的完整链路]
作者:许彰午 | 非科班野生程序员,深耕政务信息化20年
标签: #Java #低代码 #表单设计器 #拖拽 #合并拆分 #Dojo #政务信息化
更多推荐
所有评论(0)