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

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

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

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


〇、先打个生活比方

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

没有自定义表单的世界

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

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

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

有了自定义表单的世界

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

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

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

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


一、整套系统的角色分工

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

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


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

关键事实 :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 端界面布局

3.3 PC 端核心就两件事

1) 拖动(@dnd-kit):

tsx 复制代码
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) 保存:

ts 复制代码
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 上所有业务(设备详情、新增工单、工单操作、巡检、保养、问卷)全用它:

tsx 复制代码
// 用法
<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 后端存什么?

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

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

4.2 后端提供两套 API

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


五、App 端:拿 JSON 画 UI

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

5.1 拿到后端返回的数据

json 复制代码
// 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 端做的事(一图流)

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

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

App FormRenderer 的 switch 本质:

js 复制代码
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 个使用方

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 拖拽 ✅
antd UI 框架 ✅
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 + 自研控件。


七、整套体系的心智模型

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

再浓缩一次:

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

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

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

九、加新控件的改动清单

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

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

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

相关推荐
hunterandroid2 小时前
[鸿蒙从零到一] relationalStore 高级实践:事务、迁移与敏感数据保护
前端
lcarusZZZ2 小时前
前端转型,自建新闻转播客工作流
前端
vipxieliang2 小时前
JavaScript 事件循环与任务队列:从 setTimeout 到 Promise
前端·javascript
ZoeLandia2 小时前
TypeScript 05:交叉类型、索引签名、元组与 enum
前端·typescript
anyup_前端梦工厂2 小时前
uni-app 模板 Starter 一键清理 Skill 发布
前端·人工智能·uni-app
行者全栈架构师2 小时前
【鸿蒙心迹】冷启动从 2.8s 优化到 0.9s——耗时拆解与数据对比实战(HarmonyOS 7.x)
前端·算法·架构
临期冰淇淋3 小时前
MTK平台 安卓16 GMS报错修改
android·java·前端
Misnearch3 小时前
React中的回调
前端·javascript·react.js
Profile排查笔记3 小时前
浏览器多开怎么设置独立 IP?Profile、代理与核验流程
前端·人工智能·后端·自动化