form-create 动态表单设计器实战:从拖拽设计到主子表与流程表单绑定

🌐 演示地址http://ruoyioffice.com | 📦 源码1ruoyi-office-vben | 📦 源码2ruoyi-office | 📦 源码3ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)

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

form-create-architecture.png

▲ 表单低代码全景: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 的选型是「设计器 + 运行时」两个包分工:

image.png

角色 版本 职责
设计器 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.vuesetConfAndFields2(detailForm, formConf, formFields, formVariables) 渲染,formVariables 可回填流程变量

  • 审批详情processInstance/detail/index.vue 同样用 <form-create> + fApi(form-create 的命令式 API)控制字段

    image.png

发布时表单定义会被「快照」进流程定义,因此改了表单不影响已发起的流程实例,保证历史数据稳定。


五、进阶一:主子表(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);     // 隐藏
  }
}

image.png

这种「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
  • 操作路径:工作流程 → 表单管理 → 新建表单(拖拽设计)→ 流程模型 → 表单设计步骤绑定该表单
  • 推荐体验流程
    1. 新建表单,拖入用户选择器、字典下拉、上传组件
    2. 保存后到流程模型,在「表单设计」步骤选这张表单
    3. 配置某节点的字段权限(部分只读、部分隐藏)
    4. 发起流程,观察发起页表单渲染
    5. 走到该节点审批,观察字段权限生效
仓库 地址
前端 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)

📦 源码仓库GitCode | GitHub

💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐