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

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

用生活化的比喻 + 流程图,把"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 端的"坑"(之前没意识到的)

  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 + 自研控件。


七、整套体系的心智模型

#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_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 驱动的力量。

相关推荐
其实防守也摸鱼2 小时前
Codex 下载与本地部署实战指南
linux·服务器·前端·数据库·算法·ai
南极企鹅2 小时前
生产环境gc文件分析
java·jvm·算法
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 前后端分离的工程实践
java·spring cloud·架构
溪语流沙2 小时前
面试加分项!Manacher:O(n)求最长回文子串的对称之美
java·python·leetcode·面试·职场和发展
知守观2 小时前
Maven SNAPSHOT 更新排查:-U 无效、clean 无用,问题在 _remote.repositories(附完整链路)
java·后端·maven
牛战士coding2 小时前
从一句\“取消订单\“到退款通知:AI 写操作完整业务闭环
java·人工智能
苏渡苇2 小时前
从零搭建 Vue 3 工程化项目:Vite + TypeScript + Pinia + Element Plus 实战
前端·vue.js·typescript
啵啵啵鱼2 小时前
第三章 栈、队列和数组相关结构
java·数据结构·栈·队列