表单拖拽设计器(Form Designer)设计方案¶
表单拖拽设计器是 OBPM 设计态 的可视化表单模板编辑器:通过左侧控件面板拖拽、中间画布排版、右侧属性面板配置,生成与运行时兼容的表单模板(持久化为 Form.templatecontext)。模板的规范形态为 JsonTemplate(根级 fields 存放字段定义 + layout.pc/mobile 存放布局树,布局内用 fieldRef 关联字段);HtmlTemplate 由 getHtmlTemplate() 从画布 DOM 导出,供源码模式与运行时使用。产物以 npm 包 obpm-form-designer 发布,嵌入 Vue3 设计器(FormFormat.vue)或独立调试页使用。
| 层次 | 实现位置 |
|---|---|
| 后端 / Java | cn.myapps.core.runtime.dynaform.form.ejb.Form(templatecontext 字段持久化) |
FormJsonTemplateCompiler(JsonTemplate → HtmlTemplate 编译,供 Form.inited() / 运行时解析) |
|
| 设计器宿主(Vue3) | FormFormat.vue(obpm-designer-vue3/src/components/modulesDetail/) |
HostRequestBridgeImpl.js(设计态 HTTP 门面) |
|
| 表单设计器本体 | obpm-designer-web-formbuilder2(本仓库,npm 名 obpm-form-designer) |
入口 src/lib.ts → mountObpmFormDesigner(root, hostActionBridge, hostRequestBridge)(ESM;Vite 构建) |
|
Vue 壳:DesignerApp(DndProvider)+ PalettePanel / CanvasHost / PropsPanel;Pinia historyStore |
|
画布内核仍为命令式:FormApp.js、FormPanel.js、Container.js、各 *Field.js |
|
| 运行时消费 | 普通表单运行时解析 templatecontext 渲染字段(见 dyna-form.md) |
本文档描述 新版拖拽设计器(showType !== 'old');旧版 FCK 设计器仍通过 FormFormat.vue 内 iframe 加载,不在本文展开。
产品定位与核心场景¶
- 可视化排版:容器 / 多栏布局 + 30+ 字段控件,支持嵌套、排序、删除;拖放基于 vue3-dnd(面板 → 画布、画布内移动、Relstr 页签排序)。
- 撤回 / 重做:Pinia 命令栈覆盖拖入、移动、删除与常规属性写回;
form-content-barSVG 图标按钮 +Ctrl+Z/Ctrl+Y(见下文「撤回与重做」)。 - 属性配置:字段名、布局宽度(
myselfrows百分比)、值脚本、校验、隐藏/只读/打印脚本等,与后端FormField元数据对齐。 - 模板往返:导出 JsonTemplate(
{ fields, layout })写入Form.templatecontext;打开表单时由宿主拉取并setJsonTemplate还原画布(仍兼容 HtmlTemplate / 历史 HTML 存量;**不兼容**旧根级{ formPanel }JsonTemplate)。 - 宿主协作:脚本编辑、图标选择、选项卡子表单/视图设计等弹窗由宿主实现,设计器通过
hostActionBridge回调。 - 多语言:内置
i18n/strings_*.properties,URL 参数lang/locale/language切换文案。
整体架构¶
flowchart LR
subgraph host [宿主 Vue3 FormFormat]
HAB[hostActionBridge]
HRB[HostRequestBridgeImpl]
SE[ScriptEditor / IconSelect]
end
subgraph pkg [obpm-form-designer]
M[mountObpmFormDesigner]
DA[DesignerApp + DndProvider]
HS[Pinia historyStore]
FA[FormApp]
FP[FormPanel / Container]
PP[PropsPanel Vue]
BR[canvasDndBridge]
end
subgraph backend [设计态 API]
API["/designer/api/designtime/..."]
end
M --> DA
M --> FA
DA --> HS
DA --> BR
BR --> FP
FA --> FP
FA --> PP
HAB --> SE
FA --> HAB
FA --> HRB
HRB --> API
PP --> HRB
FA -->|getJsonTemplate| host
host -->|setJsonTemplate| FA
入口与挂载¶
import { mountObpmFormDesigner } from 'obpm-form-designer';
import 'obpm-form-designer/formDesign.css';
const { formApp, hostApi } = mountObpmFormDesigner(
document.getElementById('formDesignRoot'),
hostActionBridge,
hostRequestBridge
);
- 产物为 ESM(Vite 库模式);无 UMD。调试页经
src/main.ts调用同一入口。 mountObpmFormDesigner内createApp(DesignerApp)→app.use(createPinia())→ 挂载后初始化FormApp;根节点类名obpm-form-designer。hostActionBridge/hostRequestBridge均必传(缺一或非法类型抛TypeError)。
页面 DOM 约定¶
设计器工作区由 FormBuilderHtmlCreator 自动创建;宿主仅需提供挂载根 #formDesignRoot。以下隐藏控件供工具栏 / 保存 / 重置与宿主联动(FormFormat.vue 已按此约定放置):
| 元素 id | 说明 |
|---|---|
#formDesignRoot |
设计器挂载根 |
#content |
与宿主约定的隐藏域 |
#formid |
当前表单 id,建议与 hostRequestBridge.formId 同步 |
#save_btn |
bindEvent 内触发 getHtmlTemplate()(默认不处理返回值) |
#resetid |
触发 formApp.resetAllId() |
工作区内部关键节点:#formContainer(画布)、#propsContainer / #propsBoard(属性面板)、#view_source_btn + #form_source_editor(源码模式)、#show_container_border_cb(容器边框开关)。
界面布局¶
三栏结构(类名见 css/style.scss,样式限定在 .obpm-form-designer 内):
| 区域 | DOM / 类名 | 职责 |
|---|---|---|
| 左侧控件面板 | .board → .component-board(PalettePanel / PaletteListItem) |
布局 / 输入 / 展示按钮 / 包含控件四类列表;vue3-dnd useDrag(PALETTE_CONTROL) |
| 中间画布 | #diagramFlow → #formContainer(CanvasHost) |
FormPanel 根容器;useDrop + canvasDndBridge;顶栏 .form-content-bar |
| 画布顶栏 | .form-content-bar |
撤回 / 重做(SVG 图标,随 canUndo / canRedo 禁用);「显示容器边框」 |
| 右侧属性面板 | #propsContainer(Vue PropsPanel) |
按 getPropsDesc() 分组渲染;部分写回经 applyPropChange 记入历史 |
| 工具条 | .component-toolbar |
「查看源代码」等 |
控件面板配置源:FormBuilderHtmlCreator.js 中 BASIC_CONTROL_ITEMS、DISPLAY_BUTTON_CONTROL_ITEMS、INCLUDE_CONTROL_ITEMS、LAYOUT_CONTROL_ITEMS。
领域模型¶
类层次¶
AbstractElement # 属性读写、HTML 属性同步、parseHtml 基类
├── Container # 布局容器:拖拽、子元素树、占位线
│ ├── FormPanel # scope=formPanel,表单根面板
│ ├── TwoColumnContainer / ThreeColumnContainer / FourColumnContainer
│ └── Container # scope=container,通用 flex 容器
└── AbstractField # 字段基类:标题栏、padding、选中、属性校验
├── InputField, TextareaField, …(各 *Field.js)
formApp.allElements:当前画布上所有字段实例(扁平列表,含嵌套在选项卡/包含元素内的字段)。formPanel.childs:根面板下直接子节点(容器或字段)。- 每个可设计节点对应一块
htmlDOM,通过scope属性与 JS 类绑定。
scope 与运行时¶
DOM 节点上的 scope 是解析与导出的主键。常见取值:
| scope | 类 / 控件 |
|---|---|
formPanel |
表单根面板(位于 layout.pc.formPanel) |
container |
通用容器 |
twoColumnContainer / threeColumnContainer / fourColumnContainer |
多栏容器 |
fieldRef |
布局占位(仅 JsonTemplate 布局树;fieldId 指向 fields 中字段 id) |
inputField |
单行文本 |
textareaField |
多行文本 |
radioField / checkboxField / selectField |
单选 / 多选 / 下拉 |
dataField |
日期 |
deptField / treedepartmentField / userField |
部门 / 树形部门 / 用户 |
tabField |
选项卡 |
includeField |
包含元素 |
viewdialogField |
视图选择框 |
buttonField |
按钮 |
| … | 其余见 src/form/field/*.js |
propValues 为设计器内存模型;变更时 renderHtml(obj, 'prop'|'style') 写回 DOM。导出 JsonTemplate 时字段完整定义进根 fields,布局树中只留 fieldRef;编译 HtmlTemplate 时写为 DOM 属性与 inline style。
字段 HTML 结构约定¶
典型字段(以 InputField 为例):
<div class="baseField" scope="inputField" id="..." name="..." draggable="true" ...>
<div class="baseLabel"><span class="baseLabel-title">标题</span></div>
<div class="baseCon fieldId" fieldid="..."><input class="base-field" readonly></div>
</div>
showTitle:false时导出会移除.baseLabel(FormApp._cleanupExportedTemplateDom)。myselfrows:占行宽百分比(如100→width:100%)。- 设计态专用节点(导出前移除):
.placeholder、.delete/.btn-delete、data-empty-tip、选项卡.baseCon预览区等。
控件清单¶
与左侧面板一致(FormBuilderHtmlCreator.js):
布局¶
| 面板 id | 名称 |
|---|---|
Container |
通用容器 |
TwoColumnContainer |
两栏容器 |
ThreeColumnContainer |
三栏容器 |
FourColumnContainer |
四栏容器 |
输入控件¶
单行文本、多行文本、单选框、多选框、下拉框、日期选择框、部门选择框、树形部门选择框、用户选择框、左右选择框、智能提示搜索框、调查控件、文件上传、km 资料库、图片上传、在线拍照、微信 GPS、微信录音、地图、通用 WORD 编辑器、HTML 编辑器。
展示 / 按钮¶
流水号、分割行(仅移动端)、标签、二维码、计算脚本、流程历史、流程催办历史、按钮、视图选择框。
包含控件¶
包含元素、选项卡。
各字段的 getPropsDesc() 定义属性面板分组(如 base / value / check / hidden / readonly);PropsPanel 按类型渲染 TEXT、SELECT、CHECKBOX、脚本按钮等,并调用 hostRequestBridge 填充下拉数据。
拖拽与选中¶
设计器拖放已由原生 HTML5 ondrag* / dataTransfer 全量替换**为 **vue3-dnd(DndProvider + HTML5Backend)。画布业务仍在 Container / 字段类中;Vue 层通过 canvasDndBridge 把 hover/drop 坐标与 item 接到 placeholder / 创建 / 移动。
DnD 分层¶
| 层 | 位置 | 职责 |
|---|---|---|
| Provider | DesignerApp.vue |
DndProvider + HTML5Backend,面板 / 画布 / Relstr 共用上下文 |
| 拖源 / 落点 | PaletteListItem、CanvasHost、RelstrRow |
useDrag / useDrop |
| Bridge | src/vue/dnd/canvasDndBridge.ts |
hover / drop → 容器 placeholder、工厂创建、addElement / remove |
| 命令式内核 | Container.js、AbstractField.js、多栏容器 |
插入算法、占位线、选中;**不再**绑定原生 ondrag* |
Item 类型¶
| type | 含义 | payload |
|---|---|---|
PALETTE_CONTROL |
面板拖出新控件 | { controlId } |
CANVAS_ELEMENT |
画布已有元素移动 | { elementId } |
RELSTR_ROW |
选项卡页签配置行排序 | { index } |
画布 drop 只接受前两类;Relstr 只接受 RELSTR_ROW;两类区域互不接受对方 type。
从面板拖入¶
PaletteListItemuseDrag→ typePALETTE_CONTROL,item{ controlId }(如Input)。CanvasHostuseDrophover:按clientOffset解析目标容器,展示 placeholder。- drop:
canvasDndBridge.dropPalette→ 按 controlId 工厂创建字段/容器 →addElement;成功后record(AddElementCommand)。
画布内移动¶
- 画布元素经
dndManager绑定拖源(CANVAS_ELEMENT);**多栏容器的内列**不可独立拖动。 - drop:
dropCanvasElement→ 原容器remove→ 目标addElement+ 插入位置(支持首位 / 中间间隙;边缘命中带 slack)。 - 成功后
record(MoveElementCommand)。
Relstr 页签排序¶
- 仅 Name 行 为拖动手柄(
RelstrRow);排序后同步画布 tab DOM(syncTabDomFromRelstr/tabFieldUi.js)。 - 属性变更走
SetPropsCommand(含 Relstr undo 后 DOM 对齐)。
选中与删除¶
- 点击字段:容器
resetAllElemandContainer后setChecked(true),右侧渲染属性。 - 删除按钮:设计态
.delete区域(designerDeleteButton.js),导出时剥离;成功后record(RemoveElementCommand)(含子树purgeAllElementsInSubtree)。 - 选项卡 / 包含元素 / 视图选择框等有专用 UI 模块(
tabFieldUi.js、includeFieldUi.js、viewdialogFieldUi.js)。
撤回与重做¶
采用 命令模式 + Pinia(非全量 HTML 快照栈)。挂载时 app.use(createPinia());历史状态在 src/vue/stores/historyStore.ts。
Store 与约定¶
| 状态 / 方法 | 说明 |
|---|---|
past / future |
已执行可撤回 / 已撤回可重做命令 |
isRestoring |
undo/redo 进行中,业务钩子不得 record |
canUndo / canRedo |
驱动按钮禁用 |
record(cmd) |
record-after-success:业务变更成功后再入栈,清空 future;上限 50 |
undo / redo |
调用 cmd.undo() / cmd.execute();失败打日志并尽量保持栈一致 |
clear |
加载模板(setJsonTemplate / reloadDesignerFromHtml)等路径清空 |
命令清单¶
| 命令 | 覆盖 | 路径 |
|---|---|---|
AddElementCommand |
面板拖入 | canvasDndBridge |
MoveElementCommand |
画布移动 | canvasDndBridge |
RemoveElementCommand |
删除字段/容器 | AbstractField / Container 删除路径 |
SetPropsCommand |
属性差量 | applyPropChange、Relstr 等 |
统一入口:src/vue/history/recordHooks.ts(检查 isRestoring)。
UI 与快捷键¶
form-content-bar:撤回 / 重做按钮为内联 SVG(currentColor,悬停#0db3a6,禁用#bbb),title/aria-label为「撤回」「重做」。- 快捷键(设计器根
keydowncapture,historyKeydown.ts): Ctrl+Z/Meta+Z→ 撤回Ctrl+Y/Ctrl+Shift+Z/Meta+Shift+Z→ 重做- 焦点在
input/textarea/select/[contenteditable]时不拦截 - 不进历史:源码编辑器全文替换;部分仍直接
setPropValues的自定义编辑器(Mapping、ValidateLibs 等)首发未挂接。 - 已知跟进:部分布局属性(如
direction/myselfrows)撤回可能只还原模型、未重放 style 副作用;resetAllId建议同步clearHistory(模板加载已清栈)。
仓库设计稿:obpm-designer-web-formbuilder2/docs/superpowers/specs/2026-07-16-pinia-undo-redo-design.md。
属性面板(PropsPanel)¶
- 挂载容器:
#propsBoard(VuePropsPanel写入#propsContainer;旧PropsPanel.js已由 Vue/TS 替代)。 renderPropsPanel(propsDesc, propValues, scope):按分组渲染;布局类属性经reorganizeLayoutGroup归入layout组。- 写回:常规属性经
applyPropChange→setPropValues/ changeHandler,并record(SetPropsCommand);Relstr 等复杂编辑器可走recordPropPatch。 - 异步下拉:
getModules、getModuleViews、getFormsList、getEventMappingField等,均await hostRequestBridge.*,响应为 JSON 字符串(与历史 AJAX 一致)。 - 脚本属性:展示编辑按钮,调用
hostActionBridge.openIscript(code, scriptFieldName, fieldId);保存由宿主回调hostApi.handleScriptEditor/formApp.setScript。 - 图标:
openAddIcon(icontype, fieldId)→onBtnSelectIconOk/onBtnSelectFontOk写回imgpath/iconpath。 - 约定:属性面板逻辑尽量不直接改画布 DOM;画布结构变更集中在
AbstractElement/ 各*Field的renderHtml中(见仓库README.md)。
宿主桥接契约¶
hostActionBridge(必填回调)¶
FormApp 启动时校验以下 3 个键必须为函数(缺一则 TypeError):
| 方法 | 参数 | 说明 |
|---|---|---|
openIscript |
(code, scriptFieldName, fieldId) |
打开脚本编辑器 |
openTabForm |
(type, formId, moduleId, currentFormId, relFormId) |
选项卡关联表单/视图设计 |
openAddIcon |
(icontype, fieldId) |
选择图片或字体图标 |
宿主常额外注入(设计器内部按需调用,非 init 硬校验):openNewForm(flag)、getModule()、handleSpanClick() 等。FormFormat.vue 在 mountEmbedDesigner 中提供完整实现。
hostRequestBridge(设计态 HTTP)¶
宿主传入 普通对象;设计器挂到 formApp.hostRequestBridge,**不向该对象写入**运行期字段(除宿主自行维护 formId / moduleId 等)。
通用约定
- 方法均为 async,返回
Promise<string>(JSON 字符串 body);多处JSON.parse(result)。 - 成功响应体通常含
data或data.datas等字段;具体见下表。 - 调试:
createFakeHostRequestBridge(appId, moduleId, formId)(FakeHostRequestBridge.js)。
须实现的方法(名称一致,缺一会在对应功能报 is not a function):
| 方法 | 设计器用途 | 解析要点 |
|---|---|---|
getFormDetail(formId?) |
加载模板;选项卡预览子表单 | data.templatecontext:JsonTemplate 字符串(或历史 HTML) |
getModules() |
模块下拉 | data[]:{ id, value } |
getModuleViews(moduleKey, filterType?) |
视图列表、radio/checkbox 的 module | data.datas[]:{ id, name } |
getViewsColumnsList(viewId) |
视图列映射 | data[]:{ id, name } |
getViewColumnList(viewId) |
选项卡内视图预览列 | 同上 |
getEventMappingField() |
事件映射 | data[]:{ id, name } |
getDataMappingField() |
数据映射 | data[]:{ id, name } |
getStateLabels() |
状态显示 | data.data[] |
getSummarys() |
摘要模板(type==="01") |
data.data[] |
getFormsList(moduleId, type?, …) |
关联表单、选项卡 relstr | data[]:{ id, name } |
getReportList(isPrint) |
打印模板 | data.datas[] |
getVerification() |
校验库 | data.datas[] |
Vue3 实现见 HostRequestBridgeImpl.js,请求前缀 /{contextPath}/designtime/applications/{appId}/...。
可选:getFormFieldNames() — 画布仅一个控件时,映射类下拉可拉取全表单字段名补全。
模板加载与保存¶
加载(当前集成方式)¶
FormApp.init 内 load() 已注释;由宿主在 render 之后主动拉取并注入:
hostRequestBridge.getFormDetail(formId)- 解析
data.templatecontext(JsonTemplate JSON 字符串;兼容data.htmlTemplate与历史 HTML,见FormFormat.parseFormDetailTemplate) formApp.setJsonTemplate(json)(或setHtmlTemplate(html)兼容路径)→ 重建控件树
新建表单:getFormDetail 返回空串或空 templatecontext 时保持空白 FormPanel。
保存 / 导出¶
| API | 返回 | 说明 |
|---|---|---|
getHtmlTemplate() |
{ ok, html } |
先 checkField() + formPanel.checkConProp();成功则克隆画布、_cleanupExportedTemplateDom、清空 include 预览、& → & |
getJsonTemplate() |
{ ok, json } |
校验通过后,自内存控件树序列化为 JsonTemplate 再 JSON.stringify(见下文编解码实现) |
setHtmlTemplate(html) |
{ ok } |
源码模式开启时失败;reloadDesignerFromHtml 解析 HtmlTemplate(兼容历史存量) |
setJsonTemplate(json) |
{ ok } |
parseJsonTemplateInput 解析后,经 applyJsonTemplateToFormApp **直接重建**控件树(不经空壳 HTML) |
宿主保存流程(目标形态):getJsonTemplate() → 校验 ok → 将 json 字符串写入 Form.templatecontext → 提交设计态保存 API。当前 Vue3 宿主仍通过 getHtmlTemplate() 写 HTML 存量,见下文兼容说明。
JsonTemplate 格式¶
命名说明:表单设计器的 JsonTemplate 即
templatecontext字段所承载的 JSON 本体(字段定义 + PC/Mobile 布局),与 Excel 导入设计器中的JsonTemplate(sheets+config)不是同一模型。
仓库设计稿:obpm-designer-web-formbuilder2/docs/superpowers/specs/2026-07-16-jsontemplate-fields-layout-design.md。
定位¶
| 概念 | 含义 |
|---|---|
| JsonTemplate | 根级 fields(全部 *Field 定义)+ layout(pc / mobile 布局树);不包含 errcode、data、表单 name/type 等 API 或 Form VO 元数据 |
Form.templatecontext |
持久化字段;内容为 JsonTemplate 的 JSON 字符串(目标形态)或历史 HtmlTemplate HTML 字符串(兼容) |
| HtmlTemplate | 自 #formContainer 克隆并清理后的 HTML 片段;getHtmlTemplate() 产出,setHtmlTemplate() / 源码模式消费 |
getJsonTemplate() 与 setJsonTemplate() 在 内存模型 ↔ JsonTemplate 之间编解码;getHtmlTemplate() / setHtmlTemplate() 在 画布 DOM ↔ HtmlTemplate 之间编解码。两条路径相互独立,JsonTemplate 写回画布不得经「编译为空属性 HTML + preserve DOM」,否则字段会失去 baseLabel / baseCon 结构导致画布空白。
兼容边界:仍接受 HtmlTemplate 字符串与 { data: { templatecontext } } 包装;**不再接受**旧根级 { formPanel: { ... } } JsonTemplate(设计器 setJsonTemplate 返回 { ok: false })。
编解码实现(源码)¶
| 模块 | 路径 | 职责 |
|---|---|---|
| 序列化 | utility/formJsonTemplate.js |
serializeFormPanelToJsonTemplate(DFS → fields + layout) |
| 反序列化 | utility/applyJsonTemplate.js |
applyJsonTemplateToFormApp(fields Map + fieldRef 解析) |
| 入口 | FormApp.js |
getJsonTemplate / setJsonTemplate |
getJsonTemplate() 流程
formPanel.checkField()&&formPanel.checkConProp(),失败则{ ok: false, json: null }。- DFS 遍历
formPanel树(serializeLayoutElement): - 布局类(
formPanel/container/ 多栏容器):propValues平铺到节点(剔除formsOptions、viewsoptions、optionstextoptions、editProp、processprevalue等 UI 缓存),附带style,children每项为{ "<scope>": { ... } }单键包裹; - 字段类:完整节点 push 到根
fields({ [scope]: { scope, id, ... } });布局树该位置只写fieldRef占位。 - 输出
{ fields, layout: { pc: { formPanel }, mobile: null } }后JSON.stringify。
setJsonTemplate(json) 流程
- 源码模式开启 →
{ ok: false }。 parseJsonTemplateInput(json)归一化入参:- JsonTemplate:
fields为数组,且layout.pc.formPanel为对象; - 拒绝:根级
{ formPanel }(旧格式); - 兼容:
{ data: { templatecontext } }/ 扁平templatecontext;内容为 JSON 则继续解析(同样只认新格式),内容为 HTML(以<开头)则转 HtmlTemplate 路径。 - JsonTemplate 路径(
applyJsonTemplateToFormApp): - 用
fields建Map<fieldId, fieldNode>(unwrapChildEntry,键为节点id); resetDesignerCanvas:清空allElements、formPanel.childs与#formContainer;- 取
layout.pc.formPanel:formPanel.rendTo()+applyLayoutNodeProps; - 遍历布局
children:- 布局 scope →
Container/ 多栏容器 →rendTo()→ 递归;多栏无子节点时ensureColumns(); scope === "fieldRef"→ 按fieldId查 Map →mountFieldFromNode(creat()生成完整设计态 DOM);缺失则console.warn并跳过;
- 布局 scope →
resetAllElemandContainer()、syncAllViewdialogEventMappings()。- HtmlTemplate 路径:
setHtmlTemplate(html)→reloadDesignerFromHtml(parseHtml;根为scope="formPanel"时可走 preserve DOM)。
flowchart LR
subgraph export [导出]
M[formPanel.childs 内存树]
S[serialize → fields + layout]
J["JsonTemplate { fields, layout }"]
M --> S --> J
end
subgraph import [导入 JsonTemplate]
P[parseJsonTemplateInput]
A["apply: fields Map + fieldRef"]
C[creat / rendTo 完整 DOM]
P --> A --> C
end
subgraph htmlPath [HtmlTemplate 兼容]
H[getHtmlTemplate / setHtmlTemplate]
R[reloadDesignerFromHtml / parseHtml]
H --> R
end
J --> P
顶层结构¶
JsonTemplate 根对象 有且仅有 fields 与 layout 两个键:
{
"fields": [],
"layout": {
"pc": {
"formPanel": {
"scope": "formPanel",
"id": "uuid",
"name": "表单1",
"nameIndex": 0,
"myselfrows": 100,
"containerwidth": 500,
"style": {
"border": "none",
"padding": "20px 20px 40px",
"margin": "0 auto",
"width": "100%"
},
"children": []
}
},
"mobile": null
}
}
约定
| 项 | 约定 |
|---|---|
| 根键 | 仅 fields + layout;**不再**有根级 formPanel |
fields |
数组;元素为 { [scope]: { scope, id, props..., style? } }(与历史 children 单键包裹一致) |
layout.pc |
{ formPanel: formPanelNode };容器与布局关系在此树中 |
layout.mobile |
本阶段固定 null(不做序列化) |
| 布局内字段 | { fieldRef: { scope: "fieldRef", fieldId } };fieldId === field 节点 id |
| 节点属性 | 业务属性与 propValues 同名,**平铺**在节点上(不写 props 包裹层) |
children |
仅布局节点;每项为单键对象,键名为子 scope(container / fieldRef / 多栏等) |
| 省略 | 设计态专有 DOM(placeholder、delete、选项卡预览等)与 UI 缓存键不出现 |
节点通用字段¶
| 字段 | 类型 | 适用 | 说明 |
|---|---|---|---|
scope |
string |
全部 | 控件类型;布局占位为 fieldRef |
id |
string |
字段 / 布局节点 | 控件 UUID;fields 中字段必填以供关联 |
fieldId |
string |
仅 fieldRef |
指向 fields 中目标字段的 id |
name |
string |
字段 / 布局节点 | 设计器显示名 / 字段名 |
myselfrows |
number |
字段 / 布局节点 | 占父容器行宽百分比 |
style |
object |
可选 | CSS 键值对(编译 HTML 时归一) |
children |
array |
容器 / 表单根 | 子节点列表(单键包裹) |
容器类额外常见字段:direction、containerHeight、borderwidth、paddingtop / paddingright / paddingbottom / paddingleft、containerwidth(仅 formPanel)。
字段类额外属性见各控件 getPropsDesc() 分组(如 fieldtype、hiddenscript、relstr 等),均平铺在 fields 对应节点上(不在布局树内嵌完整 field)。
极简示例(含一个单行文本框)¶
{
"fields": [
{
"inputField": {
"scope": "inputField",
"id": "fld-001",
"name": "单行文本框1",
"myselfrows": 100,
"showTitle": true,
"layout": "horizontal",
"fieldtype": "VALUE_TYPE_VARCHAR",
"texttype": "text",
"width": 100,
"widthunit": "%",
"hiddenscript": "",
"readonlyscript": "",
"validatelibs": [],
"refreshfields": [],
"style": {
"display": "inline-block",
"width": "100%",
"padding": "5px 10px 10px 10px"
}
}
}
],
"layout": {
"pc": {
"formPanel": {
"scope": "formPanel",
"id": "fp-001",
"name": "表单1",
"nameIndex": 1,
"myselfrows": 100,
"containerwidth": 500,
"style": {
"border": "none",
"padding": "20px 20px 40px",
"margin": "0 auto",
"width": "100%"
},
"children": [
{
"fieldRef": {
"scope": "fieldRef",
"fieldId": "fld-001"
}
}
]
}
},
"mobile": null
}
}
布局容器与子节点¶
两栏容器示例:layout.pc.formPanel 下 twoColumnContainer 的 children 为多个 container 列;列内字段用 fieldRef 占位(字段定义在根 fields):
{
"fields": [],
"layout": {
"pc": {
"formPanel": {
"scope": "formPanel",
"id": "fp-001",
"name": "表单1",
"myselfrows": 100,
"children": [
{
"twoColumnContainer": {
"scope": "twoColumnContainer",
"id": "tc-001",
"name": "两栏容器1",
"myselfrows": 100,
"style": { "display": "flex", "flexDirection": "row", "width": "100%" },
"children": [
{
"container": {
"scope": "container",
"id": "col-1",
"name": "容器1",
"direction": "row",
"containerHeight": 60,
"borderwidth": 0,
"children": []
}
},
{
"container": {
"scope": "container",
"id": "col-2",
"name": "容器2",
"direction": "row",
"children": []
}
}
]
}
}
]
}
},
"mobile": null
}
}
特殊控件¶
| scope | JsonTemplate 要点 |
|---|---|
tabField |
完整定义在 fields;页签配置在 relstr(name、formId、type、hiddenScript 等);不用 children 承载页签内容;布局中为 fieldRef |
includeField |
定义在 fields;module、viewid、includetype 等平铺;不含设计态 .include-view-preview、viewsoptions |
viewdialogField |
定义在 fields;dialogview、mapping 等平铺 |
buttonField |
定义在 fields;actiontype、relatedformid、acttype 等动作属性平铺 |
fieldRef |
仅出现在 layout.*.formPanel 树;{ scope: "fieldRef", fieldId } |
以下字段样例为 fields 数组中的单键包裹节点(完整 JsonTemplate 还须含 layout)。序列化时会剔除 formsOptions、viewsoptions、editProp 等 UI 缓存。
选项卡(tabField + relstr)¶
{
"tabField": {
"scope": "tabField",
"id": "tab-001",
"name": "选项卡1",
"tabName": "选项卡1",
"myselfrows": 100,
"showmode": 0,
"openAll": true,
"tabselwidth": "",
"tabselheight": "",
"selectedscript": "",
"relstr": [
{
"name": "页签1",
"type": "form",
"moduleId": "module-uuid-1",
"formId": "form-uuid-a",
"recalculate": true,
"relate": false,
"hiddenScript": "",
"readOnlyScript": "",
"hiddenPrintScript": "",
"selectRefreshScript": ""
},
{
"name": "页签2",
"type": "form",
"moduleId": "module-uuid-1",
"formId": "form-uuid-b",
"recalculate": true,
"relate": false,
"hiddenScript": "",
"readOnlyScript": "",
"hiddenPrintScript": "",
"selectRefreshScript": ""
}
],
"style": {
"display": "inline-block",
"width": "100%",
"padding": "5px 10px 10px 10px"
}
}
}
包含元素(includeField)¶
{
"includeField": {
"scope": "includeField",
"id": "inc-001",
"name": "包含元素1",
"myselfrows": 100,
"includetype": 0,
"module": "module-uuid-1",
"viewid": "view-uuid-1",
"relate": true,
"fixation": false,
"fixationheight": 0,
"includeelwidth": "",
"includepercentage": "px",
"refreshonchanged": false,
"calculateonrefresh": false,
"refreshmode": "0",
"refreshfields": [],
"hiddenscript": "",
"readonlyscript": "",
"style": {
"display": "inline-block",
"width": "100%",
"padding": "5px 10px 10px 10px"
}
}
}
按钮(buttonField)¶
{
"buttonField": {
"scope": "buttonField",
"id": "btn-001",
"name": "按钮1",
"label": "保存",
"myselfrows": 100,
"colorType": "default",
"actiontype": "",
"acttype": "",
"relatedformid": "",
"jumpmode": "",
"targetlist": {},
"dispatcherurl": "",
"dispatcherparams": "",
"beforeactionscript": "",
"actionscript": "",
"afteractionscript": "",
"refreshonchanged": false,
"calculateonrefresh": false,
"refreshmode": "0",
"refreshfields": [],
"statetoshow": [],
"style": {
"display": "inline-block",
"width": "100%",
"padding": "5px 10px 10px 10px"
}
}
}
视图选择框(viewdialogField)¶
{
"viewdialogField": {
"scope": "viewdialogField",
"id": "vd-001",
"name": "视图选择框1",
"caption": "选择",
"myselfrows": 100,
"module": "module-uuid-1",
"dialogview": "view-uuid-1",
"opentype": "div",
"eventmapping": "",
"maximization": false,
"divwidth": "",
"selectone": true,
"mutilselect": false,
"allowviewdoc": false,
"mobile": true,
"isshowpic": false,
"icontype": "",
"icon": "",
"mapping": [
{ "formField": "fld-name", "viewColumn": "col-name" }
],
"refreshonchanged": false,
"calculateonrefresh": false,
"refreshmode": "0",
"refreshfields": [],
"style": {
"display": "inline-block",
"width": "100%",
"padding": "5px 10px 10px 10px"
}
}
}
综合样例(多字段 + 布局 + 选项卡)¶
字段定义集中在 fields,布局在 layout.pc.formPanel,便于联调 setJsonTemplate / 后端 FormJsonTemplateCompiler:
{
"fields": [
{
"inputField": {
"scope": "inputField",
"id": "fld-title",
"name": "标题",
"myselfrows": 50,
"showTitle": true,
"layout": "horizontal",
"fieldtype": "VALUE_TYPE_VARCHAR",
"texttype": "text",
"width": 100,
"widthunit": "%",
"hiddenscript": "",
"readonlyscript": "",
"validatelibs": [],
"refreshfields": [],
"style": { "display": "inline-block", "width": "50%", "padding": "5px 10px 10px 10px" }
}
},
{
"textareaField": {
"scope": "textareaField",
"id": "fld-desc",
"name": "说明",
"myselfrows": 100,
"showTitle": true,
"layout": "horizontal",
"fieldtype": "VALUE_TYPE_TEXT",
"hiddenscript": "",
"readonlyscript": "",
"validatelibs": [],
"refreshfields": [],
"style": { "display": "inline-block", "width": "100%", "padding": "5px 10px 10px 10px" }
}
},
{
"selectField": {
"scope": "selectField",
"id": "fld-status",
"name": "状态",
"myselfrows": 100,
"showTitle": true,
"fieldtype": "VALUE_TYPE_VARCHAR",
"optionscript": "",
"hiddenscript": "",
"readonlyscript": "",
"validatelibs": [],
"refreshfields": [],
"style": { "display": "inline-block", "width": "100%", "padding": "5px 10px 10px 10px" }
}
},
{
"dataField": {
"scope": "dataField",
"id": "fld-date",
"name": "日期",
"myselfrows": 100,
"showTitle": true,
"fieldtype": "VALUE_TYPE_DATE",
"hiddenscript": "",
"readonlyscript": "",
"validatelibs": [],
"refreshfields": [],
"style": { "display": "inline-block", "width": "100%", "padding": "5px 10px 10px 10px" }
}
},
{
"tabField": {
"scope": "tabField",
"id": "tab-demo",
"name": "选项卡1",
"tabName": "选项卡1",
"myselfrows": 100,
"showmode": 0,
"openAll": true,
"relstr": [
{
"name": "基本信息",
"type": "form",
"moduleId": "module-uuid-1",
"formId": "form-uuid-a",
"recalculate": true,
"relate": false,
"hiddenScript": "",
"readOnlyScript": "",
"hiddenPrintScript": "",
"selectRefreshScript": ""
}
]
}
},
{
"buttonField": {
"scope": "buttonField",
"id": "btn-demo",
"name": "按钮1",
"label": "提交",
"myselfrows": 100,
"actiontype": "",
"relatedformid": "",
"statetoshow": [],
"refreshfields": []
}
}
],
"layout": {
"pc": {
"formPanel": {
"scope": "formPanel",
"id": "fp-demo",
"name": "表单1",
"nameIndex": 4,
"myselfrows": 100,
"containerwidth": 500,
"style": {
"border": "none",
"padding": "20px 20px 40px",
"margin": "0 auto",
"width": "100%"
},
"children": [
{ "fieldRef": { "scope": "fieldRef", "fieldId": "fld-title" } },
{ "fieldRef": { "scope": "fieldRef", "fieldId": "fld-desc" } },
{
"twoColumnContainer": {
"scope": "twoColumnContainer",
"id": "tc-demo",
"name": "两栏容器1",
"myselfrows": 100,
"style": { "display": "flex", "flexDirection": "row", "width": "100%" },
"children": [
{
"container": {
"scope": "container",
"id": "col-l",
"name": "容器1",
"direction": "column",
"containerHeight": 60,
"borderwidth": 0,
"children": [
{ "fieldRef": { "scope": "fieldRef", "fieldId": "fld-status" } }
]
}
},
{
"container": {
"scope": "container",
"id": "col-r",
"name": "容器2",
"direction": "column",
"children": [
{ "fieldRef": { "scope": "fieldRef", "fieldId": "fld-date" } }
]
}
}
]
}
},
{ "fieldRef": { "scope": "fieldRef", "fieldId": "tab-demo" } },
{ "fieldRef": { "scope": "fieldRef", "fieldId": "btn-demo" } }
]
}
},
"mobile": null
}
}
API 与持久化中的位置¶
设计器 getJsonTemplate / setJsonTemplate 只处理上文的 JsonTemplate 对象(字符串化后即为 templatecontext 内容)。
getFormDetail 响应(HTTP 整包,不是 JsonTemplate):
{
"errcode": 0,
"errmsg": "",
"data": {
"id": "表单 UUID",
"name": "表单名称",
"templatecontext": "{ \"fields\": [], \"layout\": { \"pc\": { \"formPanel\": { ... } }, \"mobile\": null } }"
}
}
此处 data.templatecontext 为 字符串:JsonTemplate 经 JSON.stringify 后的结果(或历史 HTML 存量)。解析时先 JSON.parse(templatecontext) 得到 JsonTemplate 对象,再 setJsonTemplate;若解析失败则按 HtmlTemplate 走 setHtmlTemplate。
setJsonTemplate 入参¶
// JSON 字符串:根须为 { fields, layout }
formApp.setJsonTemplate('{"fields":[],"layout":{"pc":{"formPanel":{"scope":"formPanel","children":[]}},"mobile":null}}');
// 或已解析对象(同样仅 JsonTemplate,无 data 包裹)
formApp.setJsonTemplate({
fields: [],
layout: { pc: { formPanel: { scope: "formPanel", children: [] } }, mobile: null },
});
失败条件:源码模式开启、空串、JSON 无效、缺少 fields / layout.pc.formPanel、formPanel.scope !== "formPanel"、旧根级 { formPanel }、或 applyJsonTemplateToFormApp 执行失败。
兼容入参示例:
// 标准 JsonTemplate(fields + layout)
formApp.setJsonTemplate('{"fields":[],"layout":{"pc":{"formPanel":{"scope":"formPanel","children":[]}},"mobile":null}}');
// API 包装(templatecontext 内为新格式 JsonTemplate 字符串)
formApp.setJsonTemplate('{"data":{"templatecontext":"{\\"fields\\":[],\\"layout\\":{...}}"}}');
// 历史 HtmlTemplate(自动走 setHtmlTemplate)
formApp.setJsonTemplate('<div class="formPanel" scope="formPanel">...</div>');
// 旧根级 formPanel JsonTemplate → 失败(不兼容)
formApp.setJsonTemplate('{"formPanel":{"scope":"formPanel","children":[]}}'); // { ok: false }
调试页 「获取 JSON」/「设置 JSON」 经 hostApi.getJsonTemplate / setJsonTemplate 读写调试输出,对应上述 JsonTemplate 路径。
HtmlTemplate(DOM 导出与兼容)¶
HtmlTemplate 是 画布 DOM 的导出结果(非 JsonTemplate 的常规写回路径),对应历史 templatecontext 存 HTML、源码模式与运行时 TemplateParser 输入。
getHtmlTemplate():克隆#formContainerinnerHTML →_cleanupExportedTemplateDom(移除 placeholder、删除钮、设计预览等)→ HTML 字符串。setHtmlTemplate(html):reloadDesignerFromHtml→parseHtml按scope解析;与setJsonTemplate的applyJsonTemplateToFormApp并行存在,互不替代。- 源码模式编辑的是 HtmlTemplate;关闭时
exitSourceMode()→setHtmlTemplate。 - 运行时
TemplateParser当前以 HTML 为输入;持久化 JsonTemplate 时,后端Form.inited()会通过FormJsonTemplateCompiler.resolveHtmlTemplate自动把 JsonTemplate 编译为 HtmlTemplate,无需宿主或额外服务层介入(详见下文「JsonTemplate 后端编译」)。
HtmlTemplate 根节点(编译后)示例:
<div class="formPanel" scope="formPanel" id="..." name="表单1" myselfrows="100"
style="border:none;padding:20px 20px 40px;margin:0 auto;width:100%;">
<div class="baseField" scope="inputField" ...>...</div>
</div>
编译/导出时 style 写入 inline style;showTitle=false 时不输出 .baseLabel;字段 myselfrows 转为 width / min-width 百分比。
运行时样式(form-export-runtime.css)¶
HtmlTemplate 依赖类名(.formPanel、.baseField、.baseCon 等)。外链 form-export-runtime.css 或 import 'obpm-form-designer/form-export-runtime.css'。设计器 UI 样式(左侧板、属性板、删除钮)不进入模板。
校验规则(导出前)¶
- 当前选中字段:
checkSelf()/checkSpeProp()(如宽度非负)。 - FormPanel:
checkConProp()(边框、容器高宽、myselfrows、padding 非负等)。 - 失败时
LightToast提示,ok: false。
源码模式¶
enterSourceMode():将getHtmlTemplate()结果写入#form_source_editor,隐藏#formContainer。exitSourceMode():setHtmlTemplate(textarea.value)重建模型。- 若根节点带
scope="formPanel",走 preserve DOM 路径,避免误改表单外壳name/nameIndex(reloadDesignerFromHtml第二参restoreFormPanelMeta)。
JsonTemplate 后端编译(FormJsonTemplateCompiler)¶
设计态保存的 templatecontext 既可能是 JsonTemplate JSON 字符串(规范形态),也可能是历史 HtmlTemplate HTML 字符串。后端通过 cn.myapps.core.runtime.dynaform.form.ejb.FormJsonTemplateCompiler 在 TemplateParser 入口前自动归一化,使运行时 Form.getHtmlTemplate / 字段解析 无需宿主或额外服务层介入 即可消费 JsonTemplate。
入口与调用点
| API | 调用方 | 说明 |
|---|---|---|
FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext) |
Form.inited()、Form.getDesignHtmlTemplate() |
入参为 templatecontext 原值;HtmlTemplate 原样返回,JsonTemplate 编译后返回 |
FormJsonTemplateCompiler.compileJsonTemplateToHtml(json) |
工具/测试 | 仅当入参为合法 JsonTemplate 时编译;否则返回 null |
FormJsonTemplateCompiler.isFormJsonTemplate(text) |
工具 | 判断是否为合法 JsonTemplate(含 fields + layout.pc.formPanel) |
FormJsonTemplateCompiler.looksLikeHtml(text) |
工具 | 首字符是否为 <,识别 HtmlTemplate |
Form 侧改动:
// Form.inited()
if (!StringUtil.isBlank(templatecontext)) {
String htmlTemplate = FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext);
TemplateParser.parseTemplate(this, htmlTemplate);
}
// Form.getHtmlTemplate(doc, runner, user)
public String getHtmlTemplate(Document doc, IRunner runner, IFrontEndUser user) throws Exception {
this.inited(); // 确保字段树已按 templatecontext 解析(含 JsonTemplate 自动编译)
...
}
// 新增:对外暴露设计态 HtmlTemplate
public String getDesignHtmlTemplate() {
return FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext);
}
识别与归一化策略(resolveHtmlTemplate)
templatecontext为空 → 原样返回。trim()后首字符为<→ 视为 HtmlTemplate,原样返回(兼容历史存量与源码模式产物)。- 否则调用
compileJsonTemplateToHtml;编译失败(非 JsonTemplate)返回原trim()串,交由TemplateParser自行处理。
JsonTemplate 识别(parseFormJsonTemplateRoot)支持以下入参形态:
| 形态 | 说明 |
|---|---|
{ "fields": [...], "layout": { "pc": { "formPanel": { ... } }, "mobile": null } } |
标准设计器导出(规范形态) |
{ "data": { "templatecontext": "<JsonTemplate 字符串>" } } |
API 包装(内层须为新格式) |
{ "data": { "htmlTemplate": "<HTML 字符串>" } } |
HTML 包装 → 视为 HtmlTemplate,返回 null 由 resolveHtmlTemplate 兜底 |
扁平 templatecontext |
直接递归 |
{ "formPanel": { ... } } |
旧根级形态:设计器前端已拒绝;后端编译器若尚未跟进新格式,须同步升级后才能编译新存量 |
要求:fields 为数组,且 layout.pc.formPanel.scope === "formPanel"(或缺省为 formPanel),否则视为非 JsonTemplate。
编译规则(compileNodeToHtml,与设计器导出结构对齐;后端须跟进 fields + fieldRef)
- 用根
fields建id → fieldNode索引。 - 从
layout.pc.formPanel起递归(忽略mobile): - 布局类 scope(
formPanel/container/twoColumnContainer/threeColumnContainer/fourColumnContainer):- 根
formPanel输出class="formPanel",其余多栏/列容器无class; children数组递归编译,每项为单键对象,键名 = 子节点scope。
- 根
fieldRef:按fieldId查fields索引,命中则按字段类编译;缺失则跳过。- 字段类 scope(来自
fields):输出<div class="baseField" scope="..." ...>,内层结构:showTitle !== false(兼容showfieldtitle)时输出<div class="baseLabel"><span class="baseLabel-title" data-lang-field="..." nameIndex="...">字段名</span></div>;- 输出
<div class="baseCon fieldId" fieldid="...">…</div>,内部按 scope 产出对应控件; - 特例:
buttonField渲染baseCon-btn;tabField渲染normalTabDiv+normalTabPanelDiv(页签配置走relstr);labelField仅渲染baseLabel。
- 属性编码:剔除
scope/style/children/fieldId(占位键)后平铺为 DOM 属性;特殊属性: refreshfields/validatelibs:JSONArray 用;连接;mapping:JSONArray 去括号/引号后用;分隔;relstr:JSONArray 内逐项移除formsOptions,脚本字段做 HTML 编码,整体引号转为单引号;targetlist:{formselect, moduleselect}→"form|module";processdescription:前置processprevalueJSON(HTML 编码)+;[描述];dispatcherparams:JSON 串做 HTML 编码;processprevalue:不单独输出(由processdescription内联)。- style 对象:camelCase 键转连字符,拼成 inline style。
- 转义:属性值统一
&/"/<转义。
跟进提醒:设计器前端已切换为
{ fields, layout };后端FormJsonTemplateCompiler须同步支持fieldsMap +fieldRef解析后,新 JsonTemplate 存量才能在Form.inited()路径正确编译为 HtmlTemplate。
测试覆盖(obpm-core/src/test/.../FormJsonTemplateCompilerTest)
| 用例 | 断言要点 |
|---|---|
compileJsonTemplateToHtml_outputsFormPanelRoot |
formPanel 根、baseField / baseLabel / baseCon fieldId 结构、data-lang-field="Input"、nameIndex |
compileJsonTemplate_showTitleFalse_omitsBaseLabel |
viewdialogField + showTitle:false 时不输出 baseLabel,仍保留 baseCon 与 viewdialog-btn |
resolveHtmlTemplate_keepsLegacyHtml |
HtmlTemplate 输入原样透传 |
formWithJsonTemplate_getHtmlTemplate_outputsRuntimeHtml |
JsonTemplate → Form.inited() → getHtmlTemplate(doc, null, null) 输出 <o-input ...> 运行时标签 |
jsonTemplate_parsesThroughTemplateParser |
编译产物经 TemplateParser.parseTemplate 后 form.getFields() 非空 |
其它工具 API¶
| 方法 | 说明 |
|---|---|
resetAllId() |
重编号 formPanel、allElements、布局容器子树 id |
parseHtml(htmlNode) |
将已有模板 DOM 吃进设计器(进阶集成) |
formDesignerHostApi / hostApi |
get/set Html/JsonTemplate、handleScriptEditor、onBtnSelectIconOk 等写回门面 |
与 Vue3 设计器集成¶
FormFormat.vue(showType === 'new')职责摘要:
import { mountObpmFormDesigner } from "obpm-form-designer"(及 CSS),挂载到#formDesignRoot。- 构造
hostActionBridge,转发到脚本编辑 / 选项卡表单 / 图标选择等宿主能力。 HostRequestBridgeImplInit(appId, moduleId, formId)作为hostRequestBridge。watch(formId/moduleId/appId)→ 拉取详情并setJsonTemplate/setHtmlTemplate。defineExpose({ getHtmlTemplate, resetId, notifyVisible })供父级Form.vue保存与 Tab 切换时刷新。
showType === 'old' 仍用 iframe 加载 formHtml/fcktest2.html,与新版并行。
与后端 Form 的关系¶
- Java
Form.templatecontext(CDATA)存储 JsonTemplate JSON 字符串(规范形态)或历史 HtmlTemplate HTML 字符串(兼容)。 - 运行时
TemplateParser.parseTemplate输入仍是 HTML;Form.inited()在调用前通过FormJsonTemplateCompiler.resolveHtmlTemplate自动把 JsonTemplate 编译为 HtmlTemplate(详见上文「JsonTemplate 后端编译」),因此templatecontext可直接以 JsonTemplate 形态持久化。 - 设计器 只产出模板片段,不直接写库;持久化由宿主
getJsonTemplate()/getHtmlTemplate()写入templatecontext后提交保存 API。 - 字段
name、fieldtype、relstr(选项卡)、mapping等属性在 JsonTemplate 的fields节点上平铺,与FormFieldXML 字段对应;布局位置由layout.pc中的fieldRef表达。
国际化¶
- 语言包:
i18n/strings_zh.properties、strings_cn.properties、strings_en.properties(构建时复制到dist/i18n/)。 initFormDesignerI18n(urlParams, formApp):读取 query 中lang/locale/language,加载内置包;面板与控件文案通过data-lang-field+t()刷新。- 切换语言后可刷新当前选中控件的属性面板。
构建与发布¶
| 命令 | 说明 |
|---|---|
npm install |
安装依赖(Node ≥ 18) |
npm run dev |
Vite Dev Server(调试页 index.html + src/main.ts) |
npm run build |
vue-tsc --noEmit + Vite 库构建 → dist/formDesign.js(ESM)+ CSS / 类型 |
npm run preview |
预览构建产物 |
npm pack / npm publish |
发布前 prepublishOnly 自动 build |
产物
| 文件 | 用途 |
|---|---|
dist/formDesign.js |
ESM 主入口(exports["."]) |
dist/formDesign.d.ts |
类型声明 |
dist/formDesign.css |
设计器 UI 样式 |
dist/form-export-runtime.css |
导出模板可选运行时样式 |
主要依赖:vue、pinia、vue3-dnd、react-dnd-html5-backend。
仓库内文档:README.md(简述)、component-usage.md(嵌入契约);设计稿见 docs/superpowers/specs/(Vite+Vue3、vue3-dnd、Pinia undo/redo、JsonTemplate fields/layout)。
源码目录(核心)¶
obpm-designer-web-formbuilder2/
├── src/
│ ├── lib.ts # mountObpmFormDesigner 入口(Pinia + Vue 壳)
│ ├── main.ts # 调试页挂载
│ ├── FormApp.js # 生命周期、导出、源码模式、宿主写回
│ ├── FormBuilderHtmlCreator.js# 工作区 DOM + 控件面板配置
│ ├── form/
│ │ ├── FormPanel.js # 表单根容器
│ │ ├── Container.js # 布局容器、插入 / 占位(无原生 DnD)
│ │ ├── AbstractElement.js
│ │ └── field/ # 各字段 *Field.js + tab/include/viewdialog UI
│ ├── vue/
│ │ ├── DesignerApp.vue # DndProvider + 三栏布局
│ │ ├── CanvasHost.vue # 画布落点 + form-content-bar(撤回/重做)
│ │ ├── PalettePanel.vue / PaletteListItem.vue
│ │ ├── dnd/ # canvasDndBridge、dndTypes、dndManager、relstrReorder
│ │ ├── history/ # 命令、recordHooks、elementPlacement、historyKeydown
│ │ ├── stores/historyStore.ts
│ │ └── props/ # PropsPanel Vue + applyPropChange + editors
│ └── utility/
│ ├── formJsonTemplate.js # JsonTemplate 序列化、parseJsonTemplateInput
│ ├── applyJsonTemplate.js # setJsonTemplate:applyJsonTemplateToFormApp 重建画布
│ ├── FakeHostRequestBridge.js
│ ├── formDesignerI18n.js
│ └── ...
├── css/style.scss # 设计器样式(.obpm-form-designer 作用域)
└── i18n/
设计原则与约束¶
- 单页嵌入、无 iframe:与宿主同文档,通过 bridge 与
hostApi协作。 - Vue 壳 + 命令式画布:面板 / 属性 / DnD / 历史为 Vue+TS;字段与容器 DOM 仍由
FormApp/Container/*Field维护,经窄桥通信。 - 拖放单一实现:设计器路径仅 vue3-dnd;禁止再绑原生
ondrag*/dataTransfer。 - 历史为命令栈:结构与属性变更 record-after-success;源码全文替换不进历史;加载模板清栈。
- JsonTemplate 即规范形态:
templatecontext以{ fields, layout }为规范存储(字段定义与 PC/Mobile 布局分离);HtmlTemplate 为 DOM 导出/历史兼容产物;不做旧根级{ formPanel }JsonTemplate 兼容。 - JsonTemplate 写回须完整实例化:
setJsonTemplate经fieldRef解析后creat()/rendTo()重建字段 DOM,禁止仅用空属性 div + preserve DOM 挂载。 - scope 驱动解析:HtmlTemplate 路径下
parseHtml根据 DOMscope实例化对应类。 - 设计态与运行态分离:HtmlTemplate 导出时剥离 placeholder、删除钮、设计预览 DOM。
- 宿主必须实现桥:设计器不内置真实 HTTP;本地预览依赖
FakeHostRequestBridge或 dev 代理。
设计时态后端实现(Java)¶
设计态表单的后端由 obpm-designer 模块暴露 REST API,obpm-core 提供领域模型与服务实现。前端 hostRequestBridge(Vue3 侧 HostRequestBridgeImpl.js)通过统一前缀访问这些接口;表单模板的读写核心在 Form.templatecontext 字段。
分层与入口¶
| 层次 | 类 / 模块 | 职责 |
|---|---|---|
| REST 入口 | obpm-designer/.../form/controller/FormController.java |
表单 CRUD、字段列表、按钮(Activity)、映射/清数等 HTTP 接口 |
| 基类 | AbstractDesignTimeController |
认证用户、ParamsTable、统一 Resource 响应 |
| 服务层 | FormDesignTimeService / FormDesignTimeServiceImpl |
保存/更新/删除、变更校验、复制、同步映射数据 |
| 领域模型 | Form、FormField(obpm-core/.../form/ejb/) |
表单元数据 + templatecontext 模板 |
| 模板解析 | TemplateParser → TemplateNewProcessVisitor |
将 templatecontext(经 FormJsonTemplateCompiler 归一化为 HtmlTemplate)解析为内存 FormField 树 |
| 持久化 | FileSystemFormDAO |
JAXB 序列化 Form 为 XML,写入 workspace |
| 动态表 DDL | FormTableProcessBean |
普通/数据模型表单保存时创建或变更物理表 |
API 前缀(${myapps.context-path.designer} 默认为 /designer):
部分资源(如模块列表)前缀为 /designer/api/designtime/applications/...(见 ModuleDesignTimeController)。
flowchart TB
subgraph designer [obpm-designer]
FC[FormController]
VC[ViewController]
MC[ModuleDesignTimeController]
Others[StateLabel / Summary / Validates / Reports ...]
end
subgraph core [obpm-core]
FDS[FormDesignTimeServiceImpl]
Form[Form + templatecontext]
TP[TemplateParser]
FTP[FormTableProcessBean]
DAO[FileSystemFormDAO]
end
subgraph storage [workspace]
XML["{module}/{formName}.form"]
end
HRB[hostRequestBridge] --> FC
HRB --> VC
HRB --> MC
HRB --> Others
FC --> FDS
FDS --> Form
FDS --> FTP
FDS --> DAO
Form -->|inited| TP
DAO --> XML
与前端设计器的协作流程¶
加载
- 宿主调用
getFormDetail(formId)→GET .../modules/forms/{formId}。 - 响应体为完整
Form对象(JSON);前端读取templatecontext字符串。 FormFormat.vue解析后调用formApp.setJsonTemplate(json)或setHtmlTemplate(html)还原画布。
保存
- 设计器
getJsonTemplate()/getHtmlTemplate()产出模板字符串。 - 宿主将其写入请求体中的
templatecontext,连同name、type、showType等元数据。 PUT .../modules/{moduleId}/forms/{formId}→FormController.doUpdateForm→FormDesignTimeService.update。- 服务端
FileSystemFormDAO.save将 Form 写回 workspace XML;若含动态表字段则FormTableProcessBean.createOrUpdateDynaTable同步 DDL。
当前集成:Vue3 宿主仍多以
getHtmlTemplate()写 HTML 存量;目标形态为 JsonTemplate JSON 字符串。无论哪种,Form.inited()侧解析均走TemplateParser(见下)。
FormController 核心 API¶
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /{applicationId}/modules/{moduleId}/forms |
表单列表(分页;type 可按表单类型过滤) |
| GET | /{applicationId}/modules/forms/{formId} |
表单详情(含 templatecontext) |
| POST | /{applicationId}/modules/{moduleId}/forms |
新建表单 |
| PUT | /{applicationId}/modules/{moduleId}/forms/{formId} |
更新表单(设计器保存主入口) |
| DELETE | /{applicationId}/modules/forms |
批量删除(body 为 id 数组) |
| POST | /{applicationId}/modules/forms/{formId}/copy |
复制表单 |
| GET | /{applicationId}/modules/forms/{formId}/fields |
字段列表;type=eventMapping 时仅 Input/Suggest |
| GET | /{applicationId}/modules/forms/{formId}/clearFields |
可清数字段(getValueStoreFields) |
| GET | /{applicationId}/modules/forms/getSequence |
设计态 id 序列(字段/控件编号) |
| GET/POST/PUT/DELETE | .../forms/{formId}/activitys |
表单按钮(Activity)CRUD |
| GET | /{applicationId}/modules/forms/dataBaseTableMap |
映射表单:库表名下拉 |
| GET | /{applicationId}/modules/forms/dataBaseColumnMap?tableName= |
映射表单:列名下拉 |
| POST | /{applicationId}/modules/forms/{formId}/synchronouslyData |
映射表单同步数据到 t_document |
| POST | /{applicationId}/modules/forms/{formId}/cleardata |
清除指定字段列数据 |
表单类型常量(Form.type,列表接口 type 参数与之对应):
| 值 | 常量 | 说明 |
|---|---|---|
| 1 | FORM_TYPE_NORMAL |
普通表单 |
| 2 | FORM_TYPE_FRAGMENT |
标签页片段 |
| 256 | FORM_TYPE_SEARCHFORM |
查询表单 |
| 65536 | FORM_TYPE_NORMAL_MAPPING |
普通(映射) |
| 1048576 | FORM_TYPE_TEMPLATEFORM |
模板/阅读表单 |
hostRequestBridge 与后端映射¶
设计器属性面板下拉数据由宿主桥接;Vue3 实现请求路径与下表控制器对应(前缀均为 /designer/api/designtime):
| hostRequestBridge 方法 | 后端接口(典型) | 控制器 |
|---|---|---|
getFormDetail |
GET .../applications/{appId}/modules/forms/{formId} |
FormController |
getFormsList |
GET .../applications/{appId}/modules/{moduleId}/forms |
FormController |
getModules |
GET .../applications/{appId}/modules?parentId= |
ModuleDesignTimeController |
getModuleViews |
GET .../applications/{appId}/modules/{moduleId}/views |
ViewController |
getViewsColumnsList / getViewColumnList |
GET .../modules/views/{viewId}/columns |
ViewController |
getEventMappingField |
GET .../forms/{formId}/fields?type=eventMapping |
FormController |
getDataMappingField |
GET .../forms/{formId}/fields(或视图 valuestore 接口) |
FormController / ViewController |
getStateLabels |
GET .../applications/{appId}/statelabels |
StateLabelController |
getSummarys |
GET .../applications/{appId}/summarys |
SummaryController |
getVerification |
GET .../applications/{appId}/validates |
ValidatesController |
getReportList |
打印/报表设计态列表 | PrintDesignTimeController / ReportsController |
Form 实体与 templatecontext¶
Form(cn.myapps.core.runtime.dynaform.form.ejb.Form)继承 FileSystemDesignTimeSerializable,关键字段:
| 字段 | 说明 |
|---|---|
templatecontext |
CDATA 存储的模板字符串(JsonTemplate JSON 或 HtmlTemplate HTML) |
showType |
old | new;决定 TemplateParser 使用旧/新访问器 |
type |
表单类型(见上表) |
tableMapping |
映射表单的数据库表/列映射 |
styleId |
关联样式 |
字段内存模型:FormField 不单独持久化为 XML 子节点,而是 Form.inited() 时由 TemplateParser.parseTemplate(form, FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext)) 解析写入 transient 的 _fields / _elements。因此:
- 设计器保存时
templatecontext是唯一模板来源; - 后端
/fields等接口需先form.inited()再遍历getAllFields(); - 变更
templatecontext后若 Form 已初始化,需重新加载或setInited(false)再inited()。
解析分支(TemplateParser):
showType == "old"→TemplateProcessVisitor(FCK 旧模板);- 否则 →
TemplateNewProcessVisitor(新版拖拽设计器导出的 HtmlTemplate,按 DOMscope实例化InputField、TabField等)。
JsonTemplate 持久化后,
Form.inited()会先经FormJsonTemplateCompiler.resolveHtmlTemplate把templatecontext归一化为 HtmlTemplate 再交给TemplateParser;因此运行时与设计态字段解析链路对 JsonTemplate / HtmlTemplate 两种存量均透明(详见上文「JsonTemplate 后端编译」)。
持久化路径¶
FileSystemFormDAO → AbstractFSDesignTimeDAO:
- 根目录:
Environment.getInstance().getWorkspaceRootPath()(配置项myapps.storage.root下的storage/workspace)。 - 相对路径:
{applicationPath}/{modulePath}/{formName}.form(后缀ModelSuffix.FORM_FILE_SUFFIX=form)。 - 序列化:JAXB XML;
templatecontext经CDataAdapter写入 CDATA 段。
保存校验¶
FormController.doSaveValidate(新建/更新前)与 FormDesignTimeService.doChangeValidate(更新时):
| 校验项 | 说明 |
|---|---|
| 表单名唯一 | 同应用下不可重名 |
| 查询表单 | 不可含 SelectAboutField(左右选择框) |
| 映射表单 | 须选表名、主键;列映射成对;列名不可与系统字段冲突 |
| 字段名唯一 | repeatFieldName 检查 getAllFields() |
| 动态表结构变更 | ChangeLog.compare + FormTableProcessBean.doChangeValidate,冲突时 NeedConfirmException → HTTP 40001 |
更新成功时 version 自增;普通/数据模型表单会触发物理表 CREATE / ALTER。
源码目录(Java 侧)¶
obpm-designer/src/main/java/cn/myapps/designtime/
├── form/controller/FormController.java # 本文 REST 入口
├── view/controller/ViewController.java # 视图/列(属性面板)
├── module/controller/ModuleDesignTimeController.java
├── statelabel/controller/StateLabelController.java
├── summary/controller/SummaryController.java
├── validate/controller/ValidatesController.java
└── common/controller/AbstractDesignTimeController.java
obpm-core/src/main/java/cn/myapps/core/
├── designtime/form/service/
│ ├── FormDesignTimeService.java
│ └── FormDesignTimeServiceImpl.java
└── runtime/dynaform/form/
├── ejb/Form.java, FormField.java, TemplateParser.java
├── ejb/FormJsonTemplateCompiler.java # JsonTemplate → HtmlTemplate 编译
├── ejb/TemplateNewProcessVisitor.java
├── dao/FileSystemFormDAO.java
└── FormTableProcessBean.java # 动态表 DDL
运行时态后端实现(Java)¶
运行时表单由 obpm-runtime 模块暴露 REST API,核心业务逻辑在 obpm-core 的 dynaform 包。前端 PC 运行时(form_normalform.vue)通过 FormDataPacket 获取 HTML 模板 + 字段属性 + 按钮 + 流程信息,完成渲染与交互。设计态写入的 Form.templatecontext 在此被解析并编译为运行时 HTML。
分层与入口¶
| 层次 | 类 / 模块 | 职责 |
|---|---|---|
| REST 入口(表单) | obpm-runtime/.../dynaform/form/controller/FormController.java |
加载表单数据包、新建空文档、字段刷新、打开权限 |
| REST 入口(文档) | .../dynaform/document/controller/DocumentController.java |
文档 CRUD、校验保存、子文档、批量操作 |
| REST 入口(按钮) | obpm-runtime/.../activity/controller/ActivityController.java |
按钮前后脚本、流程/保存/打印等业务动作 |
| 字段专项 | SuggestFieldController、FileUploadController、WordFieldController、CommentController |
智能提示、附件、Word、评论 |
| 服务层 | FormRunTimeService / FormRunTimeServiceImpl |
组装 FormDataPacket、刷新重计算、新建文档 |
| 文档服务 | DocumentProcess / DocumentProcessBean |
文档持久化、校验、子表、操作日志 |
| 领域模型 | 与设计态共用 Form、FormField(obpm-core/.../form/ejb/) |
从 workspace 读取同一份 .form XML |
| 模板解析 | TemplateParser → TemplateNewProcessVisitor |
templatecontext(经 FormJsonTemplateCompiler 归一化为 HtmlTemplate)→ 内存字段树(与设计态相同) |
| 运行时渲染 | Form.getHtmlTemplate()、FormField.toHtmlTemplate() / toAttributes() |
HTML 片段 + 字段 JSON 属性 |
API 前缀(${myapps.context-path.runtime} 默认为 /runtime):
部分辅助接口前缀为 /runtime/api/...(如智能提示、上传)。
flowchart TB
subgraph fe [前端 form_normalform.vue]
Init[initForm / loadForm]
Render[动态模板 formTemplate.template]
Save[buildFormData → PUT documents]
end
subgraph runtime [obpm-runtime/dynaform]
FC[FormController]
DC[DocumentController]
AC[ActivityController]
FieldCtrl[Suggest / Upload / Word / Comment]
end
subgraph core [obpm-core]
FRS[FormRunTimeServiceImpl]
DP[DocumentProcess]
Form[Form.inited + getHtmlTemplate]
TP[TemplateParser]
end
subgraph ws [workspace]
XML["*.form templatecontext"]
end
Init --> FC
FC --> FRS
FRS --> Form
Form -->|inited| TP
XML --> Form
FRS -->|FormDataPacket| Init
Init --> Render
Save --> DC
DC --> DP
Init --> AC
Render --> FieldCtrl
templatecontext → 运行时渲染¶
设计态保存的 templatecontext 在运行时 不直接返回给前端,而是经以下链路转为 FormDataPacket:
FormDesignTimeService.doView(applicationId, formId)— 从 workspace 加载.formXML(与设计态同源)。form.inited()— 先经FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext)归一化(JsonTemplate → HtmlTemplate;HtmlTemplate 原样),再TemplateParser.parseTemplate(form, html):showType == "old"→TemplateProcessVisitor;- 否则 →
TemplateNewProcessVisitor(新版拖拽设计器 HtmlTemplate)。 FormRunTimeServiceImpl.findFormDataPacket— 加载/新建Document,执行form.recalculateDocument(值脚本、流程权限等)。- 组装 HTML —
form.getHtmlTemplate(doc, runner, user): - 新版:遍历布局
_elements,按容器内fieldid定位FormField,调用toHtmlTemplate()输出运行时 HTML; - 旧版:逐
_elements直接toHtmlTemplate()。 - 组装字段 JSON — 遍历
form.getFields(),每项FormField.toAttributes(doc, runner, user, ...)→fields[]。 - 写入
formTemplate—{ template: html, showType, showLog, isEditable, ... }连同activities、style、approvers等一并返回。
与设计态的关系:运行时
TemplateParser输入仍为 HtmlTemplate。Form.inited()会通过FormJsonTemplateCompiler.resolveHtmlTemplate自动把 JsonTemplate 编译为 HtmlTemplate,无需宿主或额外服务层介入(详见上文「JsonTemplate 后端编译」)。前端 Vue3 运行时当前以formTemplate.templateHTML 字符串 注入动态组件(详见 dyna-form.md)。
FormDataPacket 响应结构¶
GET .../forms/{formid}/documents/{docid} 返回的核心数据包(FormDataPacket):
| 字段 | 说明 |
|---|---|
formTemplate |
Map:template(HTML 字符串)、showType、showLog、showLogType、isEditable、confirmLeaveEdit、formName 等 |
fields |
字段属性数组:每项含 name、value、displayType(权限)、控件类型及 otherProps 自定义属性 |
activities |
可见工具栏按钮(经隐藏脚本、只读/流程状态过滤) |
style |
StyleRepositoryVO,关联 Form.styleId |
approvers |
流程审批人 JSON(有流程实例时) |
stateId / stateLabel / sign |
流程实例、状态标签、签章 |
openComment / commentTitle / commentFlag |
评论区配置(脚本计算后) |
showWaterMark / waterMarkText |
水印 |
document |
@JsonIgnore,仅服务端缓存;响应中 docId 为加密 id |
前端 initForm 将 formTemplate.template 作为 Vue 动态模板渲染,并通过 fields 绑定各控件 value / displayType。
FormController 核心 API¶
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /forms/{formid}/documents/{docid} |
加载表单(编辑/查看);返回 FormDataPacket |
| GET | /forms/{formid}/empty |
新建空文档;返回 Document 骨架 |
| POST | /forms/{formid}/documents/{docid}/refresh |
字段刷新重计算(值脚本、联动、Tab 内刷新) |
| GET | /forms/{formid}/documents/{docid}/openable |
文档级打开权限 |
| GET | /forms/{formId}/openable |
表单级打开权限(非 public 时校验角色) |
| GET | /getUploadFieldWaterMark |
上传控件水印配置 |
路径参数 applicationId、docid 经 DesUtil 加解密;加载后将 Document 放入用户私有缓存 MemoryCacheUtil。
DocumentController 核心 API¶
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /documents/{id} |
获取原始文档(含 items) |
| POST | /documents |
创建文档(带校验) |
| PUT | /documents/{id} |
更新文档(带校验) |
| POST | /documents/withoutValid |
创建(跳过校验,草稿) |
| PUT | /documents/{id}/withoutValid |
更新(跳过校验) |
| POST | /documents/validate |
仅校验不保存 |
| DELETE | /documents/{id} |
删除 |
| POST | /views/{viewId}/documents/{parentId}/childs |
子文档(包含元素网格)新建/更新 |
| DELETE | /views/{viewId}/documents/{parentId}/childs/{childId} |
删除子文档 |
请求体为前端 buildFormData() 结构:{ applicationId, formId, id, items, parentId, sign, subDocuments, versions }。保存时 DocumentProcess 写动态表 / 映射表,并触发工作流、日志等。
字段刷新(refresh)¶
POST .../forms/{formid}/documents/{docid}/refresh → FormRunTimeService.refresh:
- 从缓存或库加载
Document,合并请求体document.items; - 定位触发字段
actField,按refreshMode决定局部或全局刷新; form.recalculateDocumentByManual/ 单字段toAttributes(..., isFromRefresh=true);- 返回需更新的字段 Map(含
value、displayType、showValue等)。
选项卡、包含元素、计算字段在全局刷新模式下联动更新。
按钮与流程(ActivityController)¶
表单工具栏与 ButtonField 动作由 ActivityController 处理(前缀 /runtime/api/runtime):
| 典型接口 | 说明 |
|---|---|
POST /{applicationId}/activities/{id}/runbeforeactionscript |
按钮执行前脚本 |
POST /{applicationId}/activities/{id}/runafteractionscript |
按钮执行后脚本 |
各类 ActivityType 业务接口 |
保存(34)、流程启动/提交、打印、导出、跳转等(见 ActivityRunTimeService) |
FormRunTimeServiceImpl.findFormDataPacket 在返回前已按流程状态、只读、isReadonly 参数过滤不可见按钮。
字段专项控制器¶
| 控制器 | 路径示例 | 用途 |
|---|---|---|
SuggestFieldController |
POST /runtime/{appId}/forms/{formId}/documents/{docId}/querySuggest |
智能提示搜索 |
FileUploadController |
/runtime/api/... |
附件上传、下载 |
WordFieldController |
/runtime/api/runtime/forms/wordfield/... |
通用 Word 编辑器 |
CommentController |
/runtime/api/runtime/{appId}/... |
表单评论 |
FormHelperController |
/runtime/api/runtime/{appId}/documents/{docId}/... |
输入日志、签章字段、水印打印 |
upload/servlet/* |
Servlet 路径 | 图片/文件上传、预览(非 REST) |
端到端协作流程¶
打开表单(编辑)
GET /runtime/{appId}/forms/{formId}/documents/{docId}- 服务端:
doViewForm →inited()解析templatecontext→ 加载 Document →recalculateDocument→ 组装FormDataPacket - 前端:
formTemplate.template+fields渲染;activities生成工具栏
新建
GET /runtime/{appId}/forms/{formId}/empty→ 空Document- 再调
findFormDataPacket或等价 load 路径获取完整模板与默认值
保存
- 前端
buildFormData()收集items POST或PUT /runtime/{appId}/documents[/{id}]DocumentController.prepareDocument→ 校验 →DocumentProcess.doCreate/doUpdate
源码目录(Java 侧)¶
obpm-runtime/src/main/java/cn/myapps/runtime/
├── dynaform/
│ ├── form/controller/
│ │ ├── FormController.java # 表单加载 / 刷新 / 空文档
│ │ ├── FormModel.java # 轻量模板 DTO(部分场景)
│ │ ├── SuggestFieldController.java
│ │ ├── FileUploadController.java
│ │ ├── WordFieldController.java
│ │ └── CommentController.java
│ ├── document/controller/
│ │ ├── DocumentController.java # 文档 CRUD
│ │ └── FormHelperController.java
│ ├── view/controller/ViewController.java
│ └── upload/servlet/ # 上传 Servlet
└── activity/controller/ActivityController.java # 按钮 / 流程动作
obpm-core/src/main/java/cn/myapps/core/runtime/dynaform/
├── form/
│ ├── FormDataPacket.java
│ ├── service/FormRunTimeServiceImpl.java
│ └── ejb/Form.java, FormField.java, TemplateParser.java
└── document/ejb/DocumentProcess.java, Document.java, Item.java
更完整的前端消费说明、按钮类型与 openParams 约定见 dyna-form.md。
相关文档¶
- dyna-form.md — 普通表单运行时与
templatecontext解析 - map-field.md — 地图字段专项
- 仓库
component-usage.md— 嵌入 API 与hostRequestBridge字段级说明 - 仓库
docs/superpowers/specs/2026-07-15-vite-vue3-migration-design.md— Vite + Vue3 壳迁移 - 仓库
docs/superpowers/specs/2026-07-16-vue3-dnd-design.md— vue3-dnd 拖放 - 仓库
docs/superpowers/specs/2026-07-16-pinia-undo-redo-design.md— Pinia 撤回/重做 - 仓库
docs/superpowers/specs/2026-07-16-jsontemplate-fields-layout-design.md— JsonTemplatefields+layout分离