form-create 动态表单设计器实战:从拖拽设计到主子表与流程表单绑定
form-create 动态表单设计器实战:从拖拽设计到主子表与流程表单绑定
🌐 演示地址:http://ruoyioffice.com | 📦 源码1:ruoyi-office-vben | 📦 源码2:ruoyi-office | 📦 源码3:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
企业系统里 80% 的审批单都是「填一张表」:请假、报销、用印、采购……如果每张表都让前端写一遍
<a-form>,业务一变就要发版。动态表单(low-code form) 的价值就在于:业务人员拖拽设计表单,系统存成 JSON,运行时动态渲染——改字段不改代码。RuoYi Office 基于 form-create 生态搭了这套能力:fc-designer-pro负责拖拽设计,@form-create/ant-design-vue负责运行时渲染,再深度接入 Flowable 流程表单、主子表、字段权限。本文基于真实源码把这条链路讲透。

▲ 表单低代码全景:fc-designer-pro 拖拽设计 → conf+fields 编码存库 → BPM 模型绑定 formId → 运行时 form-create 渲染 + 字段权限控制;HTML 源文件见 images/form-create-architecture.html
引言:动态表单到底难在哪?
「拖个表单」听起来简单,做成企业级却有一堆坑:
痛点一:设计态和运行态要解耦。设计器产出的是一套 JSON 规则(rule),运行时要能脱离设计器独立渲染——两者不能耦合在一个组件里。
痛点二:标准组件不够用。企业表单要「选用户」「选部门」「选字典」「上传到 MinIO」「富文本」——这些不是 Input/Select 能覆盖的,必须能往设计器里注入自定义组件。
痛点三:要和流程引擎绑定。审批表单不是孤立的,要绑到流程模型上,发起页、审批页都用同一份表单定义渲染,还要支持流程变量回填。
痛点四:主子表。一张采购单(主表)挂 N 行采购明细(子表),动态表单原生的 subform 体验差,企业更想要 Excel 式表格编辑。
痛点五:字段权限。同一张表,发起人能填全部,审批人只能看一部分、改一部分——字段级的读/写/隐藏/必填要能按流程节点配置。
| 现状 | 后果 |
|---|---|
| 每张表手写 Vue 组件 | 改字段就发版,迭代慢 |
| 设计器与渲染耦合 | 运行时被迫加载整个设计器 |
| 只有基础组件 | 选人/选部门/字典全要自研对接 |
| 表单游离于流程之外 | 审批表单两套维护 |
| 无字段权限 | 敏感字段全员可见可改 |
一、技术选型:fc-designer-pro + form-create 运行时
form-create 是一个由 JSON 规则驱动表单渲染的开源框架:你给它一段 rule(描述有哪些字段、什么类型、什么校验),它就渲染出完整表单。RuoYi Office 的选型是「设计器 + 运行时」两个包分工:

| 角色 | 包 | 版本 | 职责 |
|---|---|---|---|
| 设计器 | fc-designer-pro |
6.2.0(商业 Pro 编译包) | 拖拽设计 UI |
| 运行时 | @form-create/ant-design-vue |
^3.3.1 | JSON → 表单渲染 |
注意:RuoYi Office 用的是商业 Pro 版设计器(含 VxeTable 主子表、更多企业组件),不是开源的
@form-create/designer;但运行时渲染用的是开源的@form-create/ant-design-vue,二者通过统一的 rule JSON 协议衔接。
设计器入口在 views/bpm/form/designer/index.vue:
import FcDesigner from 'fc-designer-pro';
import {
buildBpmProcessGlobalData,
buildBpmProcessGlobalVariable,
setConfAndFields,
useFormCreateDesigner,
} from '#/components/form-create';
二、设计态:自定义组件注入
开箱的 form-create 只有基础组件,企业要的「选人/选部门/字典/上传/富文本」靠 useFormCreateDesigner() 注入到设计器的拖拽面板。项目把这些封装在 src/components/form-create/ 下:
components/form-create/
├── helpers.ts # encodeConf/encodeFields/decodeFields、useFormCreateDesigner
├── bpm-context.ts # 流程上下文 globalData/globalVariable
├── components/
│ ├── dict-select.vue # 字典下拉
│ ├── dept-select.vue # 部门选择
│ ├── area-select.vue # 地区级联
│ └── use-api-select.tsx # 远程 API 下拉
└── rules/ # 各组件的拖拽规则定义
├── use-upload-file-rule.ts
├── use-dict-select.ts
└── use-editor-rule.ts
注入后,业务人员在设计器左侧就能直接拖出「用户选择器」「部门选择器」「字典下拉」等企业组件,和拖一个普通 Input 体验一致。
三、存储态:conf + fields 编解码
设计完成后要存到后端。form-create 的表单定义拆成两部分:
- conf:表单全局配置(option),一个 JSON 字符串
- fields:每个字段一条 rule,存成「字符串数组」(每条 rule 独立 JSON)
后端表 bpm_form 对应字段:
// api/bpm/form/index.ts
export interface Form {
id?: number;
name: string;
conf: string; // form-create option JSON 字符串
fields: string[]; // 每条 rule 独立 JSON 字符串
status: number;
category?: string; // 'bound' 表示绑定物理表(主子表)
}
保存前用 encodeConf / encodeFields 编码:
// components/form-create/helpers.ts
export function encodeConf(designerRef: any) {
return formCreate.toJson(designerRef.value.getOption());
}
export function encodeFields(designerRef: any) {
const rule = designerRef.value.getRule();
const fields: string[] = [];
rule.forEach((item: any) => {
const encodedRule = cloneDeep(item);
stripDynamicSelectorDefaultValue(encodedRule); // 清掉动态选择器的默认值
fields.push(formCreate.toJson(encodedRule)); // 逐条序列化
});
return fields;
}
保存到后端(designer/modules/form.vue):
data.conf = encodeConf(designerComponent);
data.fields = encodeFields(designerComponent);
if (formData.value?.id && editorAction.value !== 'copy') {
await updateForm(data); // PUT /bpm/form/update
} else {
savedFormId = await createForm(data); // POST /bpm/form/create
}
编辑时反向回显,用 setConfAndFields 把字符串解码回设计器:
const formDetail = await getForm(id);
setConfAndFields(designerRef, formDetail.conf, formDetail.fields);
designerRef.value.setGlobalData(buildBpmProcessGlobalData()); // 注入流程上下文
designerRef.value.setGlobalVariable(buildBpmProcessGlobalVariable());
四、绑定态:与 BPM 流程模型联动
表单设计完,要绑到流程模型上,才能在审批中使用。完整链路:
表单设计 → bpm_form 表(conf + fields)
↓ 流程模型「表单设计」步骤选 formId
model.formId
↓ 流程定义发布(快照)
processDefinition.formConf / formFields
↓
发起页 / 审批页 → <form-create> 渲染
-
模型绑定:流程模型编辑的「表单设计」步骤(
model/form/modules/form-design.vue)选一个formId -
发起流程:
processInstance/create/modules/form.vue用setConfAndFields2(detailForm, formConf, formFields, formVariables)渲染,formVariables可回填流程变量 -
审批详情:
processInstance/detail/index.vue同样用<form-create>+fApi(form-create 的命令式 API)控制字段
发布时表单定义会被「快照」进流程定义,因此改了表单不影响已发起的流程实例,保证历史数据稳定。
五、进阶一:主子表(fcVxeDataTable)
企业表单的硬需求是主子表。RuoYi Office 用 Pro 版内置的 fcVxeDataTable(基于 VxeGrid)实现 Excel 式子表。当表单 category === 'bound' 时,把物理表的列注入设计器全局数据:
// designer/index.vue 把物理表结构注入 $globalData
designerRef.value.setGlobalData({
physicalTable: mainTableColumns, // 主表列
physicalDetail: detailTableColumns, // 子表列(明细)
});
运行时识别 Vxe 规则,单独加载明细数据:
// processInstance/detail/index.vue
function isVxeDataTableRule(rule: any) {
return (
rule?.type === 'fcVxeDataTable' ||
rule?.type === 'dataTable' ||
rule?._fc_drag_tag === 'fcVxeDataTable'
);
}
子表绑定关系通过 saveFormTableBinding → /bpm/form-data/binding/save 持久化,实现「表单字段 ↔ 物理表列」的映射。
六、进阶二:字段权限运行时控制
字段权限是审批表单的灵魂。先看权限枚举(simple-process-design/consts.ts):
export enum FieldPermissionType {
READ = '1', // 只读
WRITE = '2', // 可编辑
NONE = '3', // 隐藏
REQUIRED = '4', // 必填
}
设计期:从 fields[] 解析出所有字段(parseFormFields 递归取 field/title),在流程节点配置每个字段的权限。运行期:后端返回 formFieldsPermission,前端通过 form-create 的命令式 API 动态应用,不改 rule JSON 本身:
// processInstance/detail/index.vue
function setFieldPermission(field: string, permission: string) {
if (permission === FieldPermissionType.READ) {
fApi.value?.disabled(true, field); // 只读
}
if (permission === FieldPermissionType.WRITE) {
fApi.value?.disabled(false, field); // 可编辑
}
if (permission === FieldPermissionType.REQUIRED) {
fApi.value?.disabled(false, field); // 可编辑 + 动态追加 required 校验
}
if (permission === FieldPermissionType.NONE) {
fApi.value?.hidden(true, field); // 隐藏
}
}

这种「rule 定义 + 运行时 API 控制」的解耦设计,意味着同一份表单定义可在不同节点呈现不同的可编辑形态,无需为每个节点复制一份表单。
七、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 设计/运行解耦 | fc-designer-pro 设计 + form-create 渲染 | 运行时轻量 |
| 自定义组件 | useFormCreateDesigner 注入 | 选人/部门/字典开箱即用 |
| 表单存储 | conf + fields[] 字符串 | 结构清晰,逐字段可控 |
| 编解码 | encodeConf/encodeFields/setConfAndFields | 保存回显闭环 |
| 流程绑定 | model.formId + 发布快照 | 改表单不影响历史实例 |
| 主子表 | fcVxeDataTable + 物理表 globalData | Excel 式子表编辑 |
| 字段权限 | fApi.disabled/hidden 运行时 | 一份表单多节点形态 |
| 流程变量回填 | setConfAndFields2 formVariables | 发起页带入上下文 |
八、快速体验
- 在线演示:http://ruoyioffice.com/web/(账号
admin/admin123) - 操作路径:工作流程 → 表单管理 → 新建表单(拖拽设计)→ 流程模型 → 表单设计步骤绑定该表单
- 推荐体验流程:
- 新建表单,拖入用户选择器、字典下拉、上传组件
- 保存后到流程模型,在「表单设计」步骤选这张表单
- 配置某节点的字段权限(部分只读、部分隐藏)
- 发起流程,观察发起页表单渲染
- 走到该节点审批,观察字段权限生效
| 仓库 | 地址 |
|---|---|
| 前端 | GitCode |
| 后端 | GitCode · GitHub |
常见问题(FAQ)
RuoYi Office 的表单设计器用的是开源 form-create 吗?
设计器用的是商业版 fc-designer-pro 6.2.0(含 VxeTable 主子表等企业组件);运行时渲染用开源的 @form-create/ant-design-vue 3.3.1。两者通过统一的 rule JSON 协议协作,运行时不依赖设计器,部署更轻量。
表单设计结果存在哪、什么格式?
存在后端 bpm_form 表,分 conf(表单全局配置 option,单个 JSON 字符串)和 fields(每个字段一条 rule,字符串数组)两部分,分别用 encodeConf / encodeFields 编码、setConfAndFields 解码回显。
改了表单会影响已经发起的流程吗?
不会。流程定义发布时会把表单定义快照进 processDefinition.formConf / formFields,已发起的流程实例使用快照版本,后续修改表单只影响新发起的流程。
动态表单支持主子表吗?
支持。通过商业 Pro 版内置的 fcVxeDataTable(基于 VxeGrid)实现 Excel 式明细表,表单 category='bound' 时绑定物理表列,子表映射通过 /bpm/form-data/binding/save 持久化。
字段权限是怎么实现的?
权限分只读(1)/可编辑(2)/隐藏(3)/必填(4) 四种,在流程节点上按字段配置。运行时后端返回 formFieldsPermission,前端通过 form-create 的 fApi.disabled() / hidden() 动态控制,不修改表单 rule 本身,因此同一份表单可在不同节点呈现不同形态。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)