1,自定义表单通俗理解-PC设计APP渲染

76 阅读7分钟

自定义表单:通俗理解指南

用生活化的比喻 + 流程图,把"PC 拖拽搭表单 → App 端渲染使用"讲明白。

本文档基于 newhome-web(PC)+ xinhewan-app(APP)双端代码事实梳理。


〇、先打个生活比方

想象你开了一家物业公司,每天有业主来报修。

没有自定义表单的世界

每来一种新报修(空调、水管、电梯、门禁……),程序员就要改一次代码:

  • 报修空调 → 写一个 RepairAirConditionPage.js
  • 报修水管 → 写一个 RepairWaterPage.js
  • 报修电梯 → 写一个 RepairElevatorPage.js

每加一种新业务,就找程序员 + 走 PR + 走发版。一周才能上线一个新表单。

有了自定义表单的世界

物业管理员自己在 PC 后台搭积木:

  • 拖一个"问题类型"过来
  • 拖一个"上门时间"过来
  • 拖一个"上传图片"过来
  • 拖一个"联系人"过来

保存——业主打开 App 就看到这张新表了。5 分钟搞定,不用发版。

一句话总结:自定义表单 = 把"开发表单"变成"配置表单",让业务人员自己搞定业务变更。


一、整套系统的角色分工

mermaid diagram

端角色一句话任务
🖥️ PC 后台甲方 + 业务消费方拖控件配属性,也消费自己配的表单(设备详情/工单/巡检等)
🗄️ 后端仓库存模板、存数据、转发请求
📱 RN App乙方拿模板画 UI、收业主输入

重要:PC 端既是"建表单的"也是"用表单的"——设备详情页、新增工单页、工单操作抽屉都用自己配的表单。


二、端到端全流程(一张图走完)

mermaid diagram

关键事实:PC/APP 不是同一套 API——PC 用 /nbParmFormList/*,APP 用 /nbFlow/form/* 和 /home/survey/*。它们共享同一个 ext_parm 数据库字段,但 HTTP 端点分开。


三、PC 端:拖拽搭表单(真实技术栈)

⚠️ 之前的猜测说"PC 端可能用 react-dnd/formily"——已确认是错的。

3.1 PC 端真实技术栈

技术实际用法
拖拽库@dnd-kit/core + @dnd-kit/sortable
UI 框架antd(不是 antd-mobile)
表单antd Form(属性面板用)
类型TypeScript FormControl interface
核心渲染器自研 SpecDynamicForm(一个组件统治所有 PC 业务)
预览自研 PhoneFormPreview(手机壳)
低代码引擎❌ 没用 formily/qform

3.2 PC 端界面布局

mermaid diagram

3.3 PC 端核心就两件事

1) 拖动(@dnd-kit):

const handleDragEnd = (event) => {
  const {active, over} = event;
  if (over && active.id !== over.id) {
    setFormControls(arrayMove(
      formControls,
      formControls.findIndex(c => c.id === active.id),
      formControls.findIndex(c => c.id === over.id),
    ));
  }
};

2) 保存:

POST /nbParmFormList/save
{
  oprType: 1,            // 1新增 2编辑 3升版
  parmFormListVo: {
    name: '空调报修',
    bizType: 'OWNER',
    formType: 'FLOW',
    fieldList: [
      {
        fieldType: 'RADIO',
        fieldTitle: '报修类型',
        isNeed: 1,
        extParm: JSON.stringify({   // ★ 整段控件配置
          type: 'radio', label: '报修类型',
          options: ['空调', '水管', '电梯'],
          required: true,
        }),
      },
      // ...
    ],
  },
}

就这么简单。"拖拽"是 UI 表现,底层就是一份 JSON。

3.4 PC 端的三态机

状态status可操作
在用版本1预览 / 升版 / 复制
待发版0预览 / 编辑 / 发版 / 删除
历史版本-1预览 / 复制

为什么需要"发版"机制?因为表单改了不能立刻生效(会影响线上工单),要先存成"待发版"→ 测试通过 → 手动发版才生效。

3.5 PC 端统一渲染器 SpecDynamicForm

这是整个体系的精华——同一个组件,PC 上所有业务(设备详情、新增工单、工单操作、巡检、保养、问卷)全用它:

// 用法
<SpecDynamicForm
  fields={fields}              // FormControl[]
  initialValues={initial}      // 回显(key 形如 f_{id})
  onSubmit={handleSubmit}
  hideToolbar={true}           // 嵌入抽屉
  columns={2}                  // 1单列 2双列
/>

// 父组件通过 ref 拿
const values = formRef.current.getValues()
// values 形如 { f_1: 'xxx', f_2: ['a','b'], ... }

PC 端特性:

  • 字段名一律 f_{id}(强约定)
  • 编辑/只读态切换(同一份 schema 两套渲染)
  • 统一提交格式化 formatSubmitValues(checkbox 数组→a,b,c、image 数组→url1,url2、datetime→dayjs 格式化、customers "其他"→"name(phone)"……)
  • 内置编辑/取消/提交工具栏

四、后端:仓库 + 翻译官

后端是个很单纯的角色——只做三件事:存模板、存数据、转发请求。不解析 ext_parm 里的 JSON,原样存原样给。

4.1 后端存什么?

mermaid diagram

最核心的一列就是 ext_parm——存的正是 PC 端拖出来的所有配置:

{
  "type": "radio",
  "label": "问题类型",
  "options": ["空调", "水管", "电梯"],
  "required": true,
  "important": true,
  "defaultValues": "空调"
}

4.2 后端提供两套 API

mermaid diagram

之前猜测"PC/APP 用同一套 API"——已确认错误。这是两套独立 API,只是共享同一个数据库字段 ext_parm。


五、App 端:拿 JSON 画 UI

这是本项目(xinhewan-app)要做的事。核心思路只有一句:JSON 驱动渲染。

5.1 拿到后端返回的数据

// GET /nbFlow/form/123 响应
{
  "fieldList": [
    {
      "id": 101,
      "fieldType": "RADIO",
      "fieldTitle": "问题类型",
      "extParm": "{\"type\":\"radio\",\"label\":\"问题类型\",\"options\":[\"空调\",\"水管\"],\"required\":true}"
    },
    {
      "id": 102,
      "fieldType": "TEXT",
      "fieldTitle": "联系电话",
      "extParm": "{\"type\":\"input\",\"label\":\"联系电话\",\"validation\":{\"max\":11}}"
    }
  ]
}

5.2 App 端做的事(一图流)

mermaid diagram

5.3 App 端三套渲染器(事实)

渲染器文件控件数用途
FormRenderersrc/components/FormRender/FormRenderer.js12(缺 rating)报修流程 + 旧问卷
QuestionnaireRenderersrc/pages/Home/QuestionnaireForm.js(内置)8(无业务控件 + rating)新问卷(多五星评分)
CustomFormStaticDisplaysrc/components/PropertyOrderDetail/CustomFormStaticDisplay.js0(只读)工单详情只读回显

App FormRenderer 的 switch 本质:

const renderControl = (field) => {
  switch (field.type) {
    case 'radio':     return <Radio ... />;
    case 'input':     return <TextInput ... />;
    case 'image':     return <ImagePicker ... />;
    case 'stepper':   return <Stepper ... />;
    // ... 12 个 case
    default:
      return <Text>TODO: 未实现控件类型 {field.type}</Text>;
  }
};

5.4 App 端 4 个使用方

mermaid diagram

5.5 App 端的"坑"(之前没意识到的)

  1. 字段 key 不统一:PC 端用 f_{id},APP 端直接用 id。这是已存在的不一致。
  2. typeObj 缺 customers 映射:renderControl 实现了 customers,但反向映射表里没登记,后端返回 CUSTOMERS 字段时会找不到前端 type。
  3. rating 在 FormRenderer 里完全缺失:会落到 TODO 兜底报错。
  4. 没有统一的提交格式化:每个业务页(Survey/RepairForms/QuestionnaireForm)自己写 checkbox 数组→字符串逻辑,不像 PC 有 formatSubmitValues 统一出口。
  5. PC 端的 disabled / important / normalValues 字段 APP 完全不消费。

六、用了什么第三方依赖?

依赖用途用了没
PC 端
@dnd-kit/core + @dnd-kit/sortable拖拽✅
antdUI 框架✅
antd Form属性面板表单✅
dayjs日期格式化✅
formily / qform整套低代码❌ 没用
APP 端
react-native-picker弹下拉框✅
@ant-design/react-native多选 / 弹窗 / Toast✅
Stepper.js(自研)数字加减自研
DateTimePicker.js(自研)日期自研
DefaultImagePicker(自研)图片自研
formily / qform—❌ 没用
react-dnd / dnd-kit拖拽❌ App 端不需要

结论:PC 端和 APP 端都没用任何低代码/表单引擎框架。都是自研 + 通用 UI 库。两端的实现思路非常相似,只是 PC 用 antd-web + dnd-kit,APP 用 antd-mobile + 自研控件。


七、整套体系的心智模型

mermaid diagram

所有角色都只跟 JSON 打交道——这就是"自定义"的全部秘密。

再浓缩一次:

  • PC = 搭积木的工人(@dnd-kit + antd)
  • 后端 = 货架(只存 ext_parm 原文)
  • App = 按图纸(switch)把积木拼成实物的消费者(自研 + antd-mobile-rn + picker)
  • 整个过程不写一行新代码就支持新表单——这就是"自定义表单"的全部秘密。
  • 唯一耦合点:ext_parm JSON 的字段约定 + fieldType 大写枚举的对照表。

八、PC 和 APP 的关键差异总结

维度PCAPP
角色建表 + 用表只用表
API/nbParmFormList/*/nbFlow/form/* + /home/survey/*
拖拽@dnd-kit不需要
渲染SpecDynamicForm(统一)3 套(FormRenderer / QuestionnaireRenderer / StaticDisplay)
字段 keyf_{id}id 直接用
提交格式化formatSubmitValues 统一各业务页自己写
状态机在用/待发/历史不感知
业务控件支持全FormRenderer 缺 rating

九、加新控件的改动清单

唯一耦合点就是 ext_parm JSON + fieldType 枚举。所以加新控件,两端各加 case + 后端加 fieldType 字典项 即可:

想加什么PC 端要改APP 端要改后端要改
通用控件 XFormControlUtils.tsx + basicControlTypes + addForm/typeObj + SpecDynamicForm/renderItem + renderStaticItem + formatSubmitValuesFormRenderer.js 的 typeObj + renderControl casefieldType 字典加一项
问卷专用 Y—QuestionnaireForm.js 的 QuestionnaireRenderer.renderControl—
详情回显 Z—CustomFormStaticDisplay.js 的 data.map 分支—

不需要改 schema 静态文件,不需要发版,不需要碰数据库——这就是 JSON 驱动的力量。