跳转至

表单拖拽设计器(Form Designer)设计方案

表单拖拽设计器是 OBPM 设计态 的可视化表单模板编辑器:通过左侧控件面板拖拽、中间画布排版、右侧属性面板配置,生成与运行时兼容的表单模板(持久化为 Form.templatecontext)。模板的规范形态为 JsonTemplate(根级 fields 存放字段定义 + layout.pc/mobile 存放布局树,布局内用 fieldRef 关联字段);HtmlTemplategetHtmlTemplate() 从画布 DOM 导出,供源码模式与运行时使用。产物以 npm 包 obpm-form-designer 发布,嵌入 Vue3 设计器(FormFormat.vue)或独立调试页使用。

层次 实现位置
后端 / Java cn.myapps.core.runtime.dynaform.form.ejb.Formtemplatecontext 字段持久化)
FormJsonTemplateCompiler(JsonTemplate → HtmlTemplate 编译,供 Form.inited() / 运行时解析)
设计器宿主(Vue3) FormFormat.vueobpm-designer-vue3/src/components/modulesDetail/
HostRequestBridgeImpl.js(设计态 HTTP 门面)
表单设计器本体 obpm-designer-web-formbuilder2(本仓库,npmobpm-form-designer
入口 src/lib.tsmountObpmFormDesigner(root, hostActionBridge, hostRequestBridge)(ESM;Vite 构建)
Vue 壳:DesignerAppDndProvider)+ PalettePanel / CanvasHost / PropsPanel;Pinia historyStore
画布内核仍为命令式:FormApp.jsFormPanel.jsContainer.js、各 *Field.js
运行时消费 普通表单运行时解析 templatecontext 渲染字段(见 dyna-form.md

本文档描述 新版拖拽设计器showType !== 'old');旧版 FCK 设计器仍通过 FormFormat.vue 内 iframe 加载,不在本文展开。


产品定位与核心场景

  • 可视化排版:容器 / 多栏布局 + 30+ 字段控件,支持嵌套、排序、删除;拖放基于 vue3-dnd(面板 → 画布、画布内移动、Relstr 页签排序)。
  • 撤回 / 重做:Pinia 命令栈覆盖拖入、移动、删除与常规属性写回;form-content-bar SVG 图标按钮 + 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 调用同一入口。
  • mountObpmFormDesignercreateApp(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-boardPalettePanel / PaletteListItem 布局 / 输入 / 展示按钮 / 包含控件四类列表;vue3-dnd useDragPALETTE_CONTROL
中间画布 #diagramFlow#formContainerCanvasHost FormPanel 根容器;useDrop + canvasDndBridge;顶栏 .form-content-bar
画布顶栏 .form-content-bar 撤回 / 重做(SVG 图标,随 canUndo / canRedo 禁用);「显示容器边框」
右侧属性面板 #propsContainer(Vue PropsPanel getPropsDesc() 分组渲染;部分写回经 applyPropChange 记入历史
工具条 .component-toolbar 「查看源代码」等

控件面板配置源:FormBuilderHtmlCreator.jsBASIC_CONTROL_ITEMSDISPLAY_BUTTON_CONTROL_ITEMSINCLUDE_CONTROL_ITEMSLAYOUT_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>
  • showTitlefalse 时导出会移除 .baseLabelFormApp._cleanupExportedTemplateDom)。
  • myselfrows:占行宽百分比(如 100width:100%)。
  • 设计态专用节点(导出前移除):.placeholder.delete / .btn-deletedata-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-dndDndProvider + HTML5Backend)。画布业务仍在 Container / 字段类中;Vue 层通过 canvasDndBridge 把 hover/drop 坐标与 item 接到 placeholder / 创建 / 移动。

DnD 分层

位置 职责
Provider DesignerApp.vue DndProvider + HTML5Backend,面板 / 画布 / Relstr 共用上下文
拖源 / 落点 PaletteListItemCanvasHostRelstrRow useDrag / useDrop
Bridge src/vue/dnd/canvasDndBridge.ts hover / drop → 容器 placeholder、工厂创建、addElement / remove
命令式内核 Container.jsAbstractField.js、多栏容器 插入算法、占位线、选中;**不再**绑定原生 ondrag*

Item 类型

type 含义 payload
PALETTE_CONTROL 面板拖出新控件 { controlId }
CANVAS_ELEMENT 画布已有元素移动 { elementId }
RELSTR_ROW 选项卡页签配置行排序 { index }

画布 drop 只接受前两类;Relstr 只接受 RELSTR_ROW;两类区域互不接受对方 type。

从面板拖入

  1. PaletteListItem useDrag → type PALETTE_CONTROL,item { controlId }(如 Input)。
  2. CanvasHost useDrop hover:按 clientOffset 解析目标容器,展示 placeholder
  3. drop:canvasDndBridge.dropPalette → 按 controlId 工厂创建字段/容器 → addElement;成功后 record(AddElementCommand)

画布内移动

  1. 画布元素经 dndManager 绑定拖源(CANVAS_ELEMENT);**多栏容器的内列**不可独立拖动。
  2. drop:dropCanvasElement → 原容器 remove → 目标 addElement + 插入位置(支持首位 / 中间间隙;边缘命中带 slack)。
  3. 成功后 record(MoveElementCommand)

Relstr 页签排序

  • Name 行 为拖动手柄(RelstrRow);排序后同步画布 tab DOM(syncTabDomFromRelstr / tabFieldUi.js)。
  • 属性变更走 SetPropsCommand(含 Relstr undo 后 DOM 对齐)。

选中与删除

  • 点击字段:容器 resetAllElemandContainersetChecked(true),右侧渲染属性。
  • 删除按钮:设计态 .delete 区域(designerDeleteButton.js),导出时剥离;成功后 record(RemoveElementCommand)(含子树 purgeAllElementsInSubtree)。
  • 选项卡 / 包含元素 / 视图选择框等有专用 UI 模块(tabFieldUi.jsincludeFieldUi.jsviewdialogFieldUi.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:撤回 / 重做按钮为内联 SVGcurrentColor,悬停 #0db3a6,禁用 #bbb),title / aria-label 为「撤回」「重做」。
  • 快捷键(设计器根 keydown capture,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(Vue PropsPanel 写入 #propsContainer;旧 PropsPanel.js 已由 Vue/TS 替代)。
  • renderPropsPanel(propsDesc, propValues, scope):按分组渲染;布局类属性经 reorganizeLayoutGroup 归入 layout 组。
  • 写回:常规属性经 applyPropChangesetPropValues / changeHandler,并 record(SetPropsCommand);Relstr 等复杂编辑器可走 recordPropPatch
  • 异步下拉getModulesgetModuleViewsgetFormsListgetEventMappingField 等,均 await hostRequestBridge.*,响应为 JSON 字符串(与历史 AJAX 一致)。
  • 脚本属性:展示编辑按钮,调用 hostActionBridge.openIscript(code, scriptFieldName, fieldId);保存由宿主回调 hostApi.handleScriptEditor / formApp.setScript
  • 图标openAddIcon(icontype, fieldId)onBtnSelectIconOk / onBtnSelectFontOk 写回 imgpath / iconpath
  • 约定:属性面板逻辑尽量不直接改画布 DOM;画布结构变更集中在 AbstractElement / 各 *FieldrenderHtml 中(见仓库 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.vuemountEmbedDesigner 中提供完整实现。

hostRequestBridge(设计态 HTTP)

宿主传入 普通对象;设计器挂到 formApp.hostRequestBridge,**不向该对象写入**运行期字段(除宿主自行维护 formId / moduleId 等)。

通用约定

  • 方法均为 async,返回 Promise<string>(JSON 字符串 body);多处 JSON.parse(result)
  • 成功响应体通常含 datadata.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.initload() 已注释;由宿主在 render 之后主动拉取并注入:

  1. hostRequestBridge.getFormDetail(formId)
  2. 解析 data.templatecontext(JsonTemplate JSON 字符串;兼容 data.htmlTemplate 与历史 HTML,见 FormFormat.parseFormDetailTemplate
  3. formApp.setJsonTemplate(json)(或 setHtmlTemplate(html) 兼容路径)→ 重建控件树

新建表单:getFormDetail 返回空串或空 templatecontext 时保持空白 FormPanel

保存 / 导出

API 返回 说明
getHtmlTemplate() { ok, html } checkField() + formPanel.checkConProp();成功则克隆画布、_cleanupExportedTemplateDom、清空 include 预览、&amp;&
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 格式

命名说明:表单设计器的 JsonTemplatetemplatecontext 字段所承载的 JSON 本体(字段定义 + PC/Mobile 布局),与 Excel 导入设计器中的 JsonTemplatesheets + config不是同一模型

仓库设计稿:obpm-designer-web-formbuilder2/docs/superpowers/specs/2026-07-16-jsontemplate-fields-layout-design.md

定位

概念 含义
JsonTemplate 根级 fields(全部 *Field 定义)+ layoutpc / mobile 布局树);不包含 errcodedata、表单 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 applyJsonTemplateToFormAppfields Map + fieldRef 解析)
入口 FormApp.js getJsonTemplate / setJsonTemplate

getJsonTemplate() 流程

  1. formPanel.checkField() && formPanel.checkConProp(),失败则 { ok: false, json: null }
  2. DFS 遍历 formPanel 树(serializeLayoutElement):
  3. 布局类(formPanel / container / 多栏容器):propValues 平铺到节点(剔除 formsOptionsviewsoptionsoptionstextoptionseditPropprocessprevalue 等 UI 缓存),附带 stylechildren 每项为 { "<scope>": { ... } } 单键包裹;
  4. 字段类:完整节点 push 到根 fields{ [scope]: { scope, id, ... } });布局树该位置只写 fieldRef 占位。
  5. 输出 { fields, layout: { pc: { formPanel }, mobile: null } }JSON.stringify

setJsonTemplate(json) 流程

  1. 源码模式开启 → { ok: false }
  2. parseJsonTemplateInput(json) 归一化入参:
  3. JsonTemplatefields 为数组,且 layout.pc.formPanel 为对象;
  4. 拒绝:根级 { formPanel }(旧格式);
  5. 兼容{ data: { templatecontext } } / 扁平 templatecontext;内容为 JSON 则继续解析(同样只认新格式),内容为 HTML(以 < 开头)则转 HtmlTemplate 路径
  6. JsonTemplate 路径applyJsonTemplateToFormApp):
  7. fieldsMap<fieldId, fieldNode>unwrapChildEntry,键为节点 id);
  8. resetDesignerCanvas:清空 allElementsformPanel.childs#formContainer
  9. layout.pc.formPanelformPanel.rendTo() + applyLayoutNodeProps
  10. 遍历布局 children
    • 布局 scope → Container / 多栏容器 → rendTo() → 递归;多栏无子节点时 ensureColumns()
    • scope === "fieldRef" → 按 fieldId 查 Map → mountFieldFromNodecreat() 生成完整设计态 DOM);缺失则 console.warn 并跳过;
  11. resetAllElemandContainer()syncAllViewdialogEventMappings()
  12. HtmlTemplate 路径setHtmlTemplate(html)reloadDesignerFromHtmlparseHtml;根为 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 根对象 有且仅有 fieldslayout 两个键:

{
  "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 仅布局节点;每项为单键对象,键名为子 scopecontainer / 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 容器 / 表单根 子节点列表(单键包裹)

容器类额外常见字段:directioncontainerHeightborderwidthpaddingtop / paddingright / paddingbottom / paddingleftcontainerwidth(仅 formPanel)。

字段类额外属性见各控件 getPropsDesc() 分组(如 fieldtypehiddenscriptrelstr 等),均平铺在 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.formPaneltwoColumnContainerchildren 为多个 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;页签配置在 relstrnameformIdtypehiddenScript 等);不用 children 承载页签内容;布局中为 fieldRef
includeField 定义在 fieldsmoduleviewidincludetype 等平铺;不含设计态 .include-view-previewviewsoptions
viewdialogField 定义在 fieldsdialogviewmapping 等平铺
buttonField 定义在 fieldsactiontyperelatedformidacttype 等动作属性平铺
fieldRef 仅出现在 layout.*.formPanel{ scope: "fieldRef", fieldId }

以下字段样例为 fields 数组中的单键包裹节点(完整 JsonTemplate 还须含 layout)。序列化时会剔除 formsOptionsviewsoptionseditProp 等 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.formPanelformPanel.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():克隆 #formContainer innerHTML → _cleanupExportedTemplateDom(移除 placeholder、删除钮、设计预览等)→ HTML 字符串。
  • setHtmlTemplate(html)reloadDesignerFromHtmlparseHtmlscope 解析;与 setJsonTemplateapplyJsonTemplateToFormApp 并行存在,互不替代。
  • 源码模式编辑的是 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.cssimport 'obpm-form-designer/form-export-runtime.css'。设计器 UI 样式(左侧板、属性板、删除钮)不进入模板。

校验规则(导出前)

  • 当前选中字段checkSelf() / checkSpeProp()(如宽度非负)。
  • FormPanelcheckConProp()(边框、容器高宽、myselfrows、padding 非负等)。
  • 失败时 LightToast 提示,ok: false

源码模式

  • enterSourceMode():将 getHtmlTemplate() 结果写入 #form_source_editor,隐藏 #formContainer
  • exitSourceMode()setHtmlTemplate(textarea.value) 重建模型。
  • 若根节点带 scope="formPanel",走 preserve DOM 路径,避免误改表单外壳 name / nameIndexreloadDesignerFromHtml 第二参 restoreFormPanelMeta)。

JsonTemplate 后端编译(FormJsonTemplateCompiler)

设计态保存的 templatecontext 既可能是 JsonTemplate JSON 字符串(规范形态),也可能是历史 HtmlTemplate HTML 字符串。后端通过 cn.myapps.core.runtime.dynaform.form.ejb.FormJsonTemplateCompilerTemplateParser 入口前自动归一化,使运行时 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

  1. templatecontext 为空 → 原样返回。
  2. trim() 后首字符为 < → 视为 HtmlTemplate,原样返回(兼容历史存量与源码模式产物)。
  3. 否则调用 compileJsonTemplateToHtml;编译失败(非 JsonTemplate)返回原 trim() 串,交由 TemplateParser 自行处理。

JsonTemplate 识别parseFormJsonTemplateRoot)支持以下入参形态:

形态 说明
{ "fields": [...], "layout": { "pc": { "formPanel": { ... } }, "mobile": null } } 标准设计器导出(规范形态)
{ "data": { "templatecontext": "<JsonTemplate 字符串>" } } API 包装(内层须为新格式)
{ "data": { "htmlTemplate": "<HTML 字符串>" } } HTML 包装 → 视为 HtmlTemplate,返回 nullresolveHtmlTemplate 兜底
扁平 templatecontext 直接递归
{ "formPanel": { ... } } 旧根级形态:设计器前端已拒绝;后端编译器若尚未跟进新格式,须同步升级后才能编译新存量

要求:fields 为数组,且 layout.pc.formPanel.scope === "formPanel"(或缺省为 formPanel),否则视为非 JsonTemplate。

编译规则compileNodeToHtml,与设计器导出结构对齐;后端须跟进 fields + fieldRef

  1. 用根 fieldsid → fieldNode 索引。
  2. layout.pc.formPanel 起递归(忽略 mobile):
  3. 布局类 scope(formPanel / container / twoColumnContainer / threeColumnContainer / fourColumnContainer):
    • formPanel 输出 class="formPanel",其余多栏/列容器无 class
    • children 数组递归编译,每项为单键对象,键名 = 子节点 scope
  4. fieldRef:按 fieldIdfields 索引,命中则按字段类编译;缺失则跳过。
  5. 字段类 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-btntabField 渲染 normalTabDiv + normalTabPanelDiv(页签配置走 relstr);labelField 仅渲染 baseLabel
  6. 属性编码:剔除 scope / style / children / fieldId(占位键)后平铺为 DOM 属性;特殊属性:
  7. refreshfields / validatelibs:JSONArray 用 ; 连接;
  8. mapping:JSONArray 去括号/引号后用 ; 分隔;
  9. relstr:JSONArray 内逐项移除 formsOptions,脚本字段做 HTML 编码,整体引号转为单引号;
  10. targetlist{formselect, moduleselect}"form|module"
  11. processdescription:前置 processprevalue JSON(HTML 编码)+ ;[描述]
  12. dispatcherparams:JSON 串做 HTML 编码;
  13. processprevalue:不单独输出(由 processdescription 内联)。
  14. style 对象:camelCase 键转连字符,拼成 inline style。
  15. 转义:属性值统一 & / " / < 转义。

跟进提醒:设计器前端已切换为 { fields, layout };后端 FormJsonTemplateCompiler 须同步支持 fields Map + 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,仍保留 baseConviewdialog-btn
resolveHtmlTemplate_keepsLegacyHtml HtmlTemplate 输入原样透传
formWithJsonTemplate_getHtmlTemplate_outputsRuntimeHtml JsonTemplate → Form.inited()getHtmlTemplate(doc, null, null) 输出 <o-input ...> 运行时标签
jsonTemplate_parsesThroughTemplateParser 编译产物经 TemplateParser.parseTemplateform.getFields() 非空

其它工具 API

方法 说明
resetAllId() 重编号 formPanelallElements、布局容器子树 id
parseHtml(htmlNode) 将已有模板 DOM 吃进设计器(进阶集成)
formDesignerHostApi / hostApi get/set Html/JsonTemplatehandleScriptEditoronBtnSelectIconOk 等写回门面

与 Vue3 设计器集成

FormFormat.vueshowType === 'new')职责摘要:

  1. import { mountObpmFormDesigner } from "obpm-form-designer"(及 CSS),挂载到 #formDesignRoot
  2. 构造 hostActionBridge,转发到脚本编辑 / 选项卡表单 / 图标选择等宿主能力。
  3. HostRequestBridgeImplInit(appId, moduleId, formId) 作为 hostRequestBridge
  4. watch(formId/moduleId/appId) → 拉取详情并 setJsonTemplate / setHtmlTemplate
  5. defineExpose({ getHtmlTemplate, resetId, notifyVisible }) 供父级 Form.vue 保存与 Tab 切换时刷新。

showType === 'old' 仍用 iframe 加载 formHtml/fcktest2.html,与新版并行。


与后端 Form 的关系

  • Java Form.templatecontext(CDATA)存储 JsonTemplate JSON 字符串(规范形态)或历史 HtmlTemplate HTML 字符串(兼容)。
  • 运行时 TemplateParser.parseTemplate 输入仍是 HTMLForm.inited() 在调用前通过 FormJsonTemplateCompiler.resolveHtmlTemplate 自动把 JsonTemplate 编译为 HtmlTemplate(详见上文「JsonTemplate 后端编译」),因此 templatecontext 可直接以 JsonTemplate 形态持久化。
  • 设计器 只产出模板片段,不直接写库;持久化由宿主 getJsonTemplate() / getHtmlTemplate() 写入 templatecontext 后提交保存 API。
  • 字段 namefieldtyperelstr(选项卡)、mapping 等属性在 JsonTemplate 的 fields 节点上平铺,与 FormField XML 字段对应;布局位置由 layout.pc 中的 fieldRef 表达。

国际化

  • 语言包:i18n/strings_zh.propertiesstrings_cn.propertiesstrings_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 导出模板可选运行时样式

主要依赖vuepiniavue3-dndreact-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/

设计原则与约束

  1. 单页嵌入、无 iframe:与宿主同文档,通过 bridge 与 hostApi 协作。
  2. Vue 壳 + 命令式画布:面板 / 属性 / DnD / 历史为 Vue+TS;字段与容器 DOM 仍由 FormApp / Container / *Field 维护,经窄桥通信。
  3. 拖放单一实现:设计器路径仅 vue3-dnd;禁止再绑原生 ondrag* / dataTransfer
  4. 历史为命令栈:结构与属性变更 record-after-success;源码全文替换不进历史;加载模板清栈。
  5. JsonTemplate 即规范形态templatecontext{ fields, layout } 为规范存储(字段定义与 PC/Mobile 布局分离);HtmlTemplate 为 DOM 导出/历史兼容产物;不做旧根级 { formPanel } JsonTemplate 兼容。
  6. JsonTemplate 写回须完整实例化setJsonTemplatefieldRef 解析后 creat() / rendTo() 重建字段 DOM,禁止仅用空属性 div + preserve DOM 挂载。
  7. scope 驱动解析:HtmlTemplate 路径下 parseHtml 根据 DOM scope 实例化对应类。
  8. 设计态与运行态分离:HtmlTemplate 导出时剥离 placeholder、删除钮、设计预览 DOM。
  9. 宿主必须实现桥:设计器不内置真实 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 保存/更新/删除、变更校验、复制、同步映射数据
领域模型 FormFormFieldobpm-core/.../form/ejb/ 表单元数据 + templatecontext 模板
模板解析 TemplateParserTemplateNewProcessVisitor templatecontext(经 FormJsonTemplateCompiler 归一化为 HtmlTemplate)解析为内存 FormField
持久化 FileSystemFormDAO JAXB 序列化 Form 为 XML,写入 workspace
动态表 DDL FormTableProcessBean 普通/数据模型表单保存时创建或变更物理表

API 前缀${myapps.context-path.designer} 默认为 /designer):

/designer/api/designtime/applications/{applicationId}/...

部分资源(如模块列表)前缀为 /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

与前端设计器的协作流程

加载

  1. 宿主调用 getFormDetail(formId)GET .../modules/forms/{formId}
  2. 响应体为完整 Form 对象(JSON);前端读取 templatecontext 字符串。
  3. FormFormat.vue 解析后调用 formApp.setJsonTemplate(json)setHtmlTemplate(html) 还原画布。

保存

  1. 设计器 getJsonTemplate() / getHtmlTemplate() 产出模板字符串。
  2. 宿主将其写入请求体中的 templatecontext,连同 nametypeshowType 等元数据。
  3. PUT .../modules/{moduleId}/forms/{formId}FormController.doUpdateFormFormDesignTimeService.update
  4. 服务端 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

Formcn.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,按 DOM scope 实例化 InputFieldTabField 等)。

JsonTemplate 持久化后,Form.inited() 会先经 FormJsonTemplateCompiler.resolveHtmlTemplatetemplatecontext 归一化为 HtmlTemplate 再交给 TemplateParser;因此运行时与设计态字段解析链路对 JsonTemplate / HtmlTemplate 两种存量均透明(详见上文「JsonTemplate 后端编译」)。

持久化路径

FileSystemFormDAOAbstractFSDesignTimeDAO

  • 根目录:Environment.getInstance().getWorkspaceRootPath()(配置项 myapps.storage.root 下的 storage/workspace)。
  • 相对路径:{applicationPath}/{modulePath}/{formName}.form(后缀 ModelSuffix.FORM_FILE_SUFFIX = form)。
  • 序列化:JAXB XML;templatecontextCDataAdapter 写入 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-coredynaform 包。前端 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 按钮前后脚本、流程/保存/打印等业务动作
字段专项 SuggestFieldControllerFileUploadControllerWordFieldControllerCommentController 智能提示、附件、Word、评论
服务层 FormRunTimeService / FormRunTimeServiceImpl 组装 FormDataPacket、刷新重计算、新建文档
文档服务 DocumentProcess / DocumentProcessBean 文档持久化、校验、子表、操作日志
领域模型 与设计态共用 FormFormFieldobpm-core/.../form/ejb/ 从 workspace 读取同一份 .form XML
模板解析 TemplateParserTemplateNewProcessVisitor templatecontext(经 FormJsonTemplateCompiler 归一化为 HtmlTemplate)→ 内存字段树(与设计态相同)
运行时渲染 Form.getHtmlTemplate()FormField.toHtmlTemplate() / toAttributes() HTML 片段 + 字段 JSON 属性

API 前缀${myapps.context-path.runtime} 默认为 /runtime):

/runtime/api/runtime/{applicationId}/...

部分辅助接口前缀为 /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

  1. FormDesignTimeService.doView(applicationId, formId) — 从 workspace 加载 .form XML(与设计态同源)。
  2. form.inited() — 先经 FormJsonTemplateCompiler.resolveHtmlTemplate(templatecontext) 归一化(JsonTemplate → HtmlTemplate;HtmlTemplate 原样),再 TemplateParser.parseTemplate(form, html)
  3. showType == "old"TemplateProcessVisitor
  4. 否则 → TemplateNewProcessVisitor(新版拖拽设计器 HtmlTemplate)。
  5. FormRunTimeServiceImpl.findFormDataPacket — 加载/新建 Document,执行 form.recalculateDocument(值脚本、流程权限等)。
  6. 组装 HTMLform.getHtmlTemplate(doc, runner, user)
  7. 新版:遍历布局 _elements,按容器内 fieldid 定位 FormField,调用 toHtmlTemplate() 输出运行时 HTML;
  8. 旧版:逐 _elements 直接 toHtmlTemplate()
  9. 组装字段 JSON — 遍历 form.getFields(),每项 FormField.toAttributes(doc, runner, user, ...)fields[]
  10. 写入 formTemplate{ template: html, showType, showLog, isEditable, ... } 连同 activitiesstyleapprovers 等一并返回。

与设计态的关系:运行时 TemplateParser 输入仍为 HtmlTemplateForm.inited() 会通过 FormJsonTemplateCompiler.resolveHtmlTemplate 自动把 JsonTemplate 编译为 HtmlTemplate,无需宿主或额外服务层介入(详见上文「JsonTemplate 后端编译」)。前端 Vue3 运行时当前以 formTemplate.template HTML 字符串 注入动态组件(详见 dyna-form.md)。

FormDataPacket 响应结构

GET .../forms/{formid}/documents/{docid} 返回的核心数据包(FormDataPacket):

字段 说明
formTemplate Map:template(HTML 字符串)、showTypeshowLogshowLogTypeisEditableconfirmLeaveEditformName
fields 字段属性数组:每项含 namevaluedisplayType(权限)、控件类型及 otherProps 自定义属性
activities 可见工具栏按钮(经隐藏脚本、只读/流程状态过滤)
style StyleRepositoryVO,关联 Form.styleId
approvers 流程审批人 JSON(有流程实例时)
stateId / stateLabel / sign 流程实例、状态标签、签章
openComment / commentTitle / commentFlag 评论区配置(脚本计算后)
showWaterMark / waterMarkText 水印
document @JsonIgnore,仅服务端缓存;响应中 docId 为加密 id

前端 initFormformTemplate.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 上传控件水印配置

路径参数 applicationIddocidDesUtil 加解密;加载后将 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}/refreshFormRunTimeService.refresh

  1. 从缓存或库加载 Document,合并请求体 document.items
  2. 定位触发字段 actField,按 refreshMode 决定局部或全局刷新;
  3. form.recalculateDocumentByManual / 单字段 toAttributes(..., isFromRefresh=true)
  4. 返回需更新的字段 Map(含 valuedisplayTypeshowValue 等)。

选项卡、包含元素、计算字段在全局刷新模式下联动更新。

按钮与流程(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)

端到端协作流程

打开表单(编辑)

  1. GET /runtime/{appId}/forms/{formId}/documents/{docId}
  2. 服务端:doView Form → inited() 解析 templatecontext → 加载 Document → recalculateDocument → 组装 FormDataPacket
  3. 前端:formTemplate.template + fields 渲染;activities 生成工具栏

新建

  1. GET /runtime/{appId}/forms/{formId}/empty → 空 Document
  2. 再调 findFormDataPacket 或等价 load 路径获取完整模板与默认值

保存

  1. 前端 buildFormData() 收集 items
  2. POSTPUT /runtime/{appId}/documents[/{id}]
  3. 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 — JsonTemplate fields + layout 分离