自定义表单:通俗理解指南
用生活化的比喻 + 流程图,把"PC 拖拽搭表单 → App 端渲染使用"讲明白。
本文档基于
newhome-web(PC)+xinhewan-app(APP)双端代码事实梳理。
〇、先打个生活比方
想象你开了一家物业公司,每天有业主来报修。
没有自定义表单的世界
每来一种新报修(空调、水管、电梯、门禁......),程序员就要改一次代码:
- 报修空调 → 写一个
RepairAirConditionPage.js - 报修水管 → 写一个
RepairWaterPage.js - 报修电梯 → 写一个
RepairElevatorPage.js
每加一种新业务,就找程序员 + 走 PR + 走发版。一周才能上线一个新表单。
有了自定义表单的世界
物业管理员自己 在 PC 后台搭积木:
- 拖一个"问题类型"过来
- 拖一个"上门时间"过来
- 拖一个"上传图片"过来
- 拖一个"联系人"过来
保存 ------业主打开 App 就看到这张新表了。5 分钟搞定,不用发版。
一句话总结:自定义表单 = 把"开发表单"变成"配置表单",让业务人员自己搞定业务变更。
一、整套系统的角色分工
#mermaid-svg-0lr0tA8GcXSBcDAV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0lr0tA8GcXSBcDAV .error-icon{fill:#552222;}#mermaid-svg-0lr0tA8GcXSBcDAV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0lr0tA8GcXSBcDAV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0lr0tA8GcXSBcDAV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0lr0tA8GcXSBcDAV .marker.cross{stroke:#333333;}#mermaid-svg-0lr0tA8GcXSBcDAV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0lr0tA8GcXSBcDAV p{margin:0;}#mermaid-svg-0lr0tA8GcXSBcDAV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster-label text{fill:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster-label span{color:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster-label span p{background-color:transparent;}#mermaid-svg-0lr0tA8GcXSBcDAV .label text,#mermaid-svg-0lr0tA8GcXSBcDAV span{fill:#333;color:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV .node rect,#mermaid-svg-0lr0tA8GcXSBcDAV .node circle,#mermaid-svg-0lr0tA8GcXSBcDAV .node ellipse,#mermaid-svg-0lr0tA8GcXSBcDAV .node polygon,#mermaid-svg-0lr0tA8GcXSBcDAV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0lr0tA8GcXSBcDAV .rough-node .label text,#mermaid-svg-0lr0tA8GcXSBcDAV .node .label text,#mermaid-svg-0lr0tA8GcXSBcDAV .image-shape .label,#mermaid-svg-0lr0tA8GcXSBcDAV .icon-shape .label{text-anchor:middle;}#mermaid-svg-0lr0tA8GcXSBcDAV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0lr0tA8GcXSBcDAV .rough-node .label,#mermaid-svg-0lr0tA8GcXSBcDAV .node .label,#mermaid-svg-0lr0tA8GcXSBcDAV .image-shape .label,#mermaid-svg-0lr0tA8GcXSBcDAV .icon-shape .label{text-align:center;}#mermaid-svg-0lr0tA8GcXSBcDAV .node.clickable{cursor:pointer;}#mermaid-svg-0lr0tA8GcXSBcDAV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0lr0tA8GcXSBcDAV .arrowheadPath{fill:#333333;}#mermaid-svg-0lr0tA8GcXSBcDAV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0lr0tA8GcXSBcDAV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0lr0tA8GcXSBcDAV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0lr0tA8GcXSBcDAV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0lr0tA8GcXSBcDAV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0lr0tA8GcXSBcDAV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster text{fill:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV .cluster span{color:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0lr0tA8GcXSBcDAV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0lr0tA8GcXSBcDAV rect.text{fill:none;stroke-width:0;}#mermaid-svg-0lr0tA8GcXSBcDAV .icon-shape,#mermaid-svg-0lr0tA8GcXSBcDAV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0lr0tA8GcXSBcDAV .icon-shape p,#mermaid-svg-0lr0tA8GcXSBcDAV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0lr0tA8GcXSBcDAV .icon-shape .label rect,#mermaid-svg-0lr0tA8GcXSBcDAV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0lr0tA8GcXSBcDAV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0lr0tA8GcXSBcDAV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0lr0tA8GcXSBcDAV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 三个角色
保存/查询
拉取/提交
理解
理解
🖥️ PC 后台
newhome-web
业务人员配置 + 业务人员消费
🗄️ 后端服务
存模板 存数据
📱 RN App
xinhewan-app
业主填写 物业查看
| 端 | 角色 | 一句话任务 |
|---|---|---|
| 🖥️ PC 后台 | 甲方 + 业务消费方 | 拖控件配属性,也消费自己配的表单(设备详情/工单/巡检等) |
| 🗄️ 后端 | 仓库 | 存模板、存数据、转发请求 |
| 📱 RN App | 乙方 | 拿模板画 UI、收业主输入 |
重要:PC 端既是"建表单的"也是"用表单的"------设备详情页、新增工单页、工单操作抽屉都用自己配的表单。
二、端到端全流程(一张图走完)
👮 物业员工 📱 App 👤 业主 🗄️ 后端+数据库 🖥️ PC 后台 🧑💼 物业管理员 👮 物业员工 📱 App 👤 业主 🗄️ 后端+数据库 🖥️ PC 后台 🧑💼 物业管理员 #mermaid-svg-RLJan5rsORFcHjqc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RLJan5rsORFcHjqc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RLJan5rsORFcHjqc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RLJan5rsORFcHjqc .error-icon{fill:#552222;}#mermaid-svg-RLJan5rsORFcHjqc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RLJan5rsORFcHjqc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RLJan5rsORFcHjqc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RLJan5rsORFcHjqc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RLJan5rsORFcHjqc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RLJan5rsORFcHjqc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RLJan5rsORFcHjqc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RLJan5rsORFcHjqc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RLJan5rsORFcHjqc .marker.cross{stroke:#333333;}#mermaid-svg-RLJan5rsORFcHjqc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RLJan5rsORFcHjqc p{margin:0;}#mermaid-svg-RLJan5rsORFcHjqc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RLJan5rsORFcHjqc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RLJan5rsORFcHjqc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RLJan5rsORFcHjqc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RLJan5rsORFcHjqc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RLJan5rsORFcHjqc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RLJan5rsORFcHjqc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RLJan5rsORFcHjqc .sequenceNumber{fill:white;}#mermaid-svg-RLJan5rsORFcHjqc #sequencenumber{fill:#333;}#mermaid-svg-RLJan5rsORFcHjqc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RLJan5rsORFcHjqc .messageText{fill:#333;stroke:none;}#mermaid-svg-RLJan5rsORFcHjqc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RLJan5rsORFcHjqc .labelText,#mermaid-svg-RLJan5rsORFcHjqc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RLJan5rsORFcHjqc .loopText,#mermaid-svg-RLJan5rsORFcHjqc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RLJan5rsORFcHjqc .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RLJan5rsORFcHjqc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RLJan5rsORFcHjqc .noteText,#mermaid-svg-RLJan5rsORFcHjqc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RLJan5rsORFcHjqc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RLJan5rsORFcHjqc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RLJan5rsORFcHjqc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RLJan5rsORFcHjqc .actorPopupMenu{position:absolute;}#mermaid-svg-RLJan5rsORFcHjqc .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RLJan5rsORFcHjqc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RLJan5rsORFcHjqc .actor-man circle,#mermaid-svg-RLJan5rsORFcHjqc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RLJan5rsORFcHjqc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 配置阶段(PC 端建表单) ② PC 端"自消费"(同一份代码渲染自己配的表单) ③ 业主消费(App 端) ④ 物业员工回显(App 详情页只读) 拖控件 配属性1POST /nbParmFormList/save{ fieldList: ..., ext_parm: JSON }2保存成功 返回 formId3新建工单4GET /nbParmFormList/{formId}5ext_parm JSON6SpecDynamicForm 渲染 antd 组件7填写8POST /nbFlow/do9进入"我要报修"10GET /nbFlow/form/{formId}11ext_parm JSON(同一份)12FormRenderer 渲染 RN 组件13填写14POST /nbFlow/do15打开工单详情16GET /nbFlow/flowOrderDetail17业主填的数据18CustomFormStaticDisplay 只读展示19
关键事实 :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-svg-1k0ZIFIgg4Tc6fJJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .error-icon{fill:#552222;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .marker.cross{stroke:#333333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ p{margin:0;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster-label text{fill:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster-label span{color:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster-label span p{background-color:transparent;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .label text,#mermaid-svg-1k0ZIFIgg4Tc6fJJ span{fill:#333;color:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node rect,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node circle,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node ellipse,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node polygon,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .rough-node .label text,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node .label text,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .image-shape .label,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .rough-node .label,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node .label,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .image-shape .label,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .icon-shape .label{text-align:center;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node.clickable{cursor:pointer;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .arrowheadPath{fill:#333333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster text{fill:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .cluster span{color:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .icon-shape,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .icon-shape p,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .icon-shape .label rect,#mermaid-svg-1k0ZIFIgg4Tc6fJJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1k0ZIFIgg4Tc6fJJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PC 后台界面
右侧: 属性面板
当前选中: 字段3
标题: 问题描述
占位符: ...
是否必填: ✓
最大长度: 300
中间: 手机壳画布 268×560
字段1: 报修类型
字段2: 联系电话
字段3: 问题描述
字段4: 现场照片
⋮⋮ 拖动手柄在左上
左侧: 控件库
📻 单选
☑️ 多选
📝 输入框
🔢 步进器
📅 日期
🖼️ 图片
⭐ 评分
🏠 认证房产
📋 项目
🏢 房源
👤 客户
📊 级联
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 后端存什么?
#mermaid-svg-TC9IOQQm6vXfu17S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TC9IOQQm6vXfu17S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TC9IOQQm6vXfu17S .error-icon{fill:#552222;}#mermaid-svg-TC9IOQQm6vXfu17S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TC9IOQQm6vXfu17S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TC9IOQQm6vXfu17S .marker.cross{stroke:#333333;}#mermaid-svg-TC9IOQQm6vXfu17S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TC9IOQQm6vXfu17S p{margin:0;}#mermaid-svg-TC9IOQQm6vXfu17S .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-TC9IOQQm6vXfu17S .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-TC9IOQQm6vXfu17S .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-TC9IOQQm6vXfu17S .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-TC9IOQQm6vXfu17S .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-TC9IOQQm6vXfu17S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TC9IOQQm6vXfu17S .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-TC9IOQQm6vXfu17S .node rect,#mermaid-svg-TC9IOQQm6vXfu17S .node circle,#mermaid-svg-TC9IOQQm6vXfu17S .node ellipse,#mermaid-svg-TC9IOQQm6vXfu17S .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TC9IOQQm6vXfu17S .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-TC9IOQQm6vXfu17S .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-TC9IOQQm6vXfu17S .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TC9IOQQm6vXfu17S .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-TC9IOQQm6vXfu17S .edgeLabel .label text{fill:#333;}#mermaid-svg-TC9IOQQm6vXfu17S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1对多
1对多
form_template
int
id
PK
string
name
string
biz_type
int
status
1在用 0待发 -1历史
int
version
form_field
int
id
PK
int
form_id
FK
string
field_type
TEXT/RADIO/IMAGE/CASCADER...
string
field_title
int
is_need
1必填 0非必填
int
important
text
ext_parm
★ 整段 FormControl JSON 字符串
json
option_list
后端冗余索引
string
normal_value
string
cascader_type_id
form_response
int
id
PK
int
form_id
FK
int
field_id
FK
string
field_value
json
image_urls
json
cascader_full_name
int
user_id
最核心的一列就是 ext_parm------存的正是 PC 端拖出来的所有配置:
json
{
"type": "radio",
"label": "问题类型",
"options": ["空调", "水管", "电梯"],
"required": true,
"important": true,
"defaultValues": "空调"
}
4.2 后端提供两套 API
#mermaid-svg-bh4WidIqG51uYg2m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bh4WidIqG51uYg2m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bh4WidIqG51uYg2m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bh4WidIqG51uYg2m .error-icon{fill:#552222;}#mermaid-svg-bh4WidIqG51uYg2m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bh4WidIqG51uYg2m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bh4WidIqG51uYg2m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bh4WidIqG51uYg2m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bh4WidIqG51uYg2m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bh4WidIqG51uYg2m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bh4WidIqG51uYg2m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bh4WidIqG51uYg2m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bh4WidIqG51uYg2m .marker.cross{stroke:#333333;}#mermaid-svg-bh4WidIqG51uYg2m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bh4WidIqG51uYg2m p{margin:0;}#mermaid-svg-bh4WidIqG51uYg2m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bh4WidIqG51uYg2m .cluster-label text{fill:#333;}#mermaid-svg-bh4WidIqG51uYg2m .cluster-label span{color:#333;}#mermaid-svg-bh4WidIqG51uYg2m .cluster-label span p{background-color:transparent;}#mermaid-svg-bh4WidIqG51uYg2m .label text,#mermaid-svg-bh4WidIqG51uYg2m span{fill:#333;color:#333;}#mermaid-svg-bh4WidIqG51uYg2m .node rect,#mermaid-svg-bh4WidIqG51uYg2m .node circle,#mermaid-svg-bh4WidIqG51uYg2m .node ellipse,#mermaid-svg-bh4WidIqG51uYg2m .node polygon,#mermaid-svg-bh4WidIqG51uYg2m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bh4WidIqG51uYg2m .rough-node .label text,#mermaid-svg-bh4WidIqG51uYg2m .node .label text,#mermaid-svg-bh4WidIqG51uYg2m .image-shape .label,#mermaid-svg-bh4WidIqG51uYg2m .icon-shape .label{text-anchor:middle;}#mermaid-svg-bh4WidIqG51uYg2m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bh4WidIqG51uYg2m .rough-node .label,#mermaid-svg-bh4WidIqG51uYg2m .node .label,#mermaid-svg-bh4WidIqG51uYg2m .image-shape .label,#mermaid-svg-bh4WidIqG51uYg2m .icon-shape .label{text-align:center;}#mermaid-svg-bh4WidIqG51uYg2m .node.clickable{cursor:pointer;}#mermaid-svg-bh4WidIqG51uYg2m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bh4WidIqG51uYg2m .arrowheadPath{fill:#333333;}#mermaid-svg-bh4WidIqG51uYg2m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bh4WidIqG51uYg2m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bh4WidIqG51uYg2m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bh4WidIqG51uYg2m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bh4WidIqG51uYg2m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bh4WidIqG51uYg2m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bh4WidIqG51uYg2m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bh4WidIqG51uYg2m .cluster text{fill:#333;}#mermaid-svg-bh4WidIqG51uYg2m .cluster span{color:#333;}#mermaid-svg-bh4WidIqG51uYg2m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bh4WidIqG51uYg2m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bh4WidIqG51uYg2m rect.text{fill:none;stroke-width:0;}#mermaid-svg-bh4WidIqG51uYg2m .icon-shape,#mermaid-svg-bh4WidIqG51uYg2m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bh4WidIqG51uYg2m .icon-shape p,#mermaid-svg-bh4WidIqG51uYg2m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bh4WidIqG51uYg2m .icon-shape .label rect,#mermaid-svg-bh4WidIqG51uYg2m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bh4WidIqG51uYg2m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bh4WidIqG51uYg2m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bh4WidIqG51uYg2m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} APP 端
PC 端
nbParmFormList
nbParmFormList/save
nbFlow/form/:id
nbFlow/do
home/survey/selectByFormIdAndUserId
home/survey/saveResponse
共享 form_field.ext_parm
之前猜测"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 端做的事(一图流)
#mermaid-svg-nzX5mv4XlW0xAB3S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nzX5mv4XlW0xAB3S .error-icon{fill:#552222;}#mermaid-svg-nzX5mv4XlW0xAB3S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nzX5mv4XlW0xAB3S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nzX5mv4XlW0xAB3S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nzX5mv4XlW0xAB3S .marker.cross{stroke:#333333;}#mermaid-svg-nzX5mv4XlW0xAB3S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nzX5mv4XlW0xAB3S p{margin:0;}#mermaid-svg-nzX5mv4XlW0xAB3S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster-label text{fill:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster-label span{color:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster-label span p{background-color:transparent;}#mermaid-svg-nzX5mv4XlW0xAB3S .label text,#mermaid-svg-nzX5mv4XlW0xAB3S span{fill:#333;color:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S .node rect,#mermaid-svg-nzX5mv4XlW0xAB3S .node circle,#mermaid-svg-nzX5mv4XlW0xAB3S .node ellipse,#mermaid-svg-nzX5mv4XlW0xAB3S .node polygon,#mermaid-svg-nzX5mv4XlW0xAB3S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nzX5mv4XlW0xAB3S .rough-node .label text,#mermaid-svg-nzX5mv4XlW0xAB3S .node .label text,#mermaid-svg-nzX5mv4XlW0xAB3S .image-shape .label,#mermaid-svg-nzX5mv4XlW0xAB3S .icon-shape .label{text-anchor:middle;}#mermaid-svg-nzX5mv4XlW0xAB3S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nzX5mv4XlW0xAB3S .rough-node .label,#mermaid-svg-nzX5mv4XlW0xAB3S .node .label,#mermaid-svg-nzX5mv4XlW0xAB3S .image-shape .label,#mermaid-svg-nzX5mv4XlW0xAB3S .icon-shape .label{text-align:center;}#mermaid-svg-nzX5mv4XlW0xAB3S .node.clickable{cursor:pointer;}#mermaid-svg-nzX5mv4XlW0xAB3S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nzX5mv4XlW0xAB3S .arrowheadPath{fill:#333333;}#mermaid-svg-nzX5mv4XlW0xAB3S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nzX5mv4XlW0xAB3S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nzX5mv4XlW0xAB3S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nzX5mv4XlW0xAB3S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nzX5mv4XlW0xAB3S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nzX5mv4XlW0xAB3S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster text{fill:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S .cluster span{color:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nzX5mv4XlW0xAB3S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nzX5mv4XlW0xAB3S rect.text{fill:none;stroke-width:0;}#mermaid-svg-nzX5mv4XlW0xAB3S .icon-shape,#mermaid-svg-nzX5mv4XlW0xAB3S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nzX5mv4XlW0xAB3S .icon-shape p,#mermaid-svg-nzX5mv4XlW0xAB3S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nzX5mv4XlW0xAB3S .icon-shape .label rect,#mermaid-svg-nzX5mv4XlW0xAB3S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nzX5mv4XlW0xAB3S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nzX5mv4XlW0xAB3S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nzX5mv4XlW0xAB3S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} radio
checkbox
input
textarea
stepper
datetime
image
room
cascader
property_room
project
customers
rating
是
否
App 拿到后端 JSON
遍历 fieldList
每条字段 JSON.parse extParm
是什么 type?
画单选
画多选
画文本框
画多行文本
画步进器
画日期
画图片上传
画三级级联
画二级级联
画房号下拉
画项目下拉
画客户下拉
画五星评分
仅 QuestionnaireRenderer 支持
监听用户改动
onChange 累积 formValues
点提交?
把 formValues 拼成后端格式 POST 出去
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 个使用方
#mermaid-svg-9UGyiDKVQsAUuS8Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9UGyiDKVQsAUuS8Z .error-icon{fill:#552222;}#mermaid-svg-9UGyiDKVQsAUuS8Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9UGyiDKVQsAUuS8Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .marker.cross{stroke:#333333;}#mermaid-svg-9UGyiDKVQsAUuS8Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9UGyiDKVQsAUuS8Z p{margin:0;}#mermaid-svg-9UGyiDKVQsAUuS8Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster-label text{fill:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster-label span{color:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster-label span p{background-color:transparent;}#mermaid-svg-9UGyiDKVQsAUuS8Z .label text,#mermaid-svg-9UGyiDKVQsAUuS8Z span{fill:#333;color:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .node rect,#mermaid-svg-9UGyiDKVQsAUuS8Z .node circle,#mermaid-svg-9UGyiDKVQsAUuS8Z .node ellipse,#mermaid-svg-9UGyiDKVQsAUuS8Z .node polygon,#mermaid-svg-9UGyiDKVQsAUuS8Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .rough-node .label text,#mermaid-svg-9UGyiDKVQsAUuS8Z .node .label text,#mermaid-svg-9UGyiDKVQsAUuS8Z .image-shape .label,#mermaid-svg-9UGyiDKVQsAUuS8Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-9UGyiDKVQsAUuS8Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .rough-node .label,#mermaid-svg-9UGyiDKVQsAUuS8Z .node .label,#mermaid-svg-9UGyiDKVQsAUuS8Z .image-shape .label,#mermaid-svg-9UGyiDKVQsAUuS8Z .icon-shape .label{text-align:center;}#mermaid-svg-9UGyiDKVQsAUuS8Z .node.clickable{cursor:pointer;}#mermaid-svg-9UGyiDKVQsAUuS8Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .arrowheadPath{fill:#333333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9UGyiDKVQsAUuS8Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9UGyiDKVQsAUuS8Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9UGyiDKVQsAUuS8Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster text{fill:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z .cluster span{color:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9UGyiDKVQsAUuS8Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9UGyiDKVQsAUuS8Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-9UGyiDKVQsAUuS8Z .icon-shape,#mermaid-svg-9UGyiDKVQsAUuS8Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9UGyiDKVQsAUuS8Z .icon-shape p,#mermaid-svg-9UGyiDKVQsAUuS8Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9UGyiDKVQsAUuS8Z .icon-shape .label rect,#mermaid-svg-9UGyiDKVQsAUuS8Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9UGyiDKVQsAUuS8Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9UGyiDKVQsAUuS8Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9UGyiDKVQsAUuS8Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本项目 RN App
📋 旧问卷
pages/Home/Survey.js
📋 新问卷
pages/Home/QuestionnaireForm.js
🔧 报修流程
pages/RepairProcess/RepairForms.js
📖 工单详情
只读回显
components/PropertyOrderDetail/CustomFormStaticDisplay.js
FormRenderer 通用
QuestionnaireRenderer 问卷专用
5.5 App 端的"坑"(之前没意识到的)
- 字段 key 不统一 :PC 端用
f_{id},APP 端直接用id。这是已存在的不一致。 typeObj缺customers映射 :renderControl 实现了 customers,但反向映射表里没登记,后端返回CUSTOMERS字段时会找不到前端 type。rating在 FormRenderer 里完全缺失 :会落到TODO兜底报错。- 没有统一的提交格式化 :每个业务页(Survey/RepairForms/QuestionnaireForm)自己写 checkbox 数组→字符串逻辑,不像 PC 有
formatSubmitValues统一出口。 - 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 + 自研控件。
七、整套体系的心智模型
#mermaid-svg-DW6WRy7zyqDX4b7K{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DW6WRy7zyqDX4b7K .error-icon{fill:#552222;}#mermaid-svg-DW6WRy7zyqDX4b7K .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DW6WRy7zyqDX4b7K .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DW6WRy7zyqDX4b7K .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DW6WRy7zyqDX4b7K .marker.cross{stroke:#333333;}#mermaid-svg-DW6WRy7zyqDX4b7K svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DW6WRy7zyqDX4b7K p{margin:0;}#mermaid-svg-DW6WRy7zyqDX4b7K .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster-label text{fill:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster-label span{color:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster-label span p{background-color:transparent;}#mermaid-svg-DW6WRy7zyqDX4b7K .label text,#mermaid-svg-DW6WRy7zyqDX4b7K span{fill:#333;color:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K .node rect,#mermaid-svg-DW6WRy7zyqDX4b7K .node circle,#mermaid-svg-DW6WRy7zyqDX4b7K .node ellipse,#mermaid-svg-DW6WRy7zyqDX4b7K .node polygon,#mermaid-svg-DW6WRy7zyqDX4b7K .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DW6WRy7zyqDX4b7K .rough-node .label text,#mermaid-svg-DW6WRy7zyqDX4b7K .node .label text,#mermaid-svg-DW6WRy7zyqDX4b7K .image-shape .label,#mermaid-svg-DW6WRy7zyqDX4b7K .icon-shape .label{text-anchor:middle;}#mermaid-svg-DW6WRy7zyqDX4b7K .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DW6WRy7zyqDX4b7K .rough-node .label,#mermaid-svg-DW6WRy7zyqDX4b7K .node .label,#mermaid-svg-DW6WRy7zyqDX4b7K .image-shape .label,#mermaid-svg-DW6WRy7zyqDX4b7K .icon-shape .label{text-align:center;}#mermaid-svg-DW6WRy7zyqDX4b7K .node.clickable{cursor:pointer;}#mermaid-svg-DW6WRy7zyqDX4b7K .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DW6WRy7zyqDX4b7K .arrowheadPath{fill:#333333;}#mermaid-svg-DW6WRy7zyqDX4b7K .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DW6WRy7zyqDX4b7K .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DW6WRy7zyqDX4b7K .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DW6WRy7zyqDX4b7K .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DW6WRy7zyqDX4b7K .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DW6WRy7zyqDX4b7K .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster text{fill:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K .cluster span{color:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DW6WRy7zyqDX4b7K .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DW6WRy7zyqDX4b7K rect.text{fill:none;stroke-width:0;}#mermaid-svg-DW6WRy7zyqDX4b7K .icon-shape,#mermaid-svg-DW6WRy7zyqDX4b7K .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DW6WRy7zyqDX4b7K .icon-shape p,#mermaid-svg-DW6WRy7zyqDX4b7K .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DW6WRy7zyqDX4b7K .icon-shape .label rect,#mermaid-svg-DW6WRy7zyqDX4b7K .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DW6WRy7zyqDX4b7K .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DW6WRy7zyqDX4b7K .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DW6WRy7zyqDX4b7K :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} JSON
JSON
JSON
JSON 数据
JSON 数据
🖥️ PC 端
拖控件 配属性
newhome-web
🗄️ 后端
ext_parm 字段
🖥️ PC 端
SpecDynamicForm 渲染
antd 组件
📱 App 端
FormRenderer 渲染
RN 组件
📖 App 详情页
CustomFormStaticDisplay
只读展示
所有角色都只跟 JSON 打交道------这就是"自定义"的全部秘密。
再浓缩一次:
- PC = 搭积木的工人(@dnd-kit + antd)
- 后端 = 货架 (只存
ext_parm原文)- App = 按图纸(switch)把积木拼成实物的消费者(自研 + antd-mobile-rn + picker)
- 整个过程不写一行新代码就支持新表单------这就是"自定义表单"的全部秘密。
- 唯一耦合点 :
ext_parmJSON 的字段约定 +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 驱动的力量。