# 动态表单取值与提交数据详解

本文说明 **`flmDynamicForm` 运行时**如何汇总各分组数据、数据在 **`flmGridForm`** 中如何维护，以及 **`componentList` 自定义控件**为何必须遵守 `onChange` / `onInput` 契约才能出现在提交结果中。

与外部组件注册、设计器配置形态相关的说明见：[framework-external-components.md](./framework-external-components.md)。

---

## 1. 总览：数据从哪来

```
┌─────────────────────────────────────────────────────────────────┐
│ flmDynamicForm (config.groups + config.buttons)                  │
│   ├─ type === 'form'  → flmGridForm (每个分组一个实例)              │
│   │       └─ 内部 reactive: formModel { [prop]: value }          │
│   └─ type === 'subForm' → flm-sub-form + 本组件内 subFormData     │
│                                                                  │
│ 底部按钮 submit/cancel/reset → getDynamicFormData() → emit        │
└─────────────────────────────────────────────────────────────────┘
```

**要点**：

- 动态表单**不会**遍历 DOM 或调用各控件的「取值 API」；**只读 `flmGridForm` 暴露的 `formModel` 与子表单的 `subFormData`**。
- 任一表单项的值要进提交 payload，必须在用户交互时通过 `flmGridForm` 已注入的回调写进 `formModel[prop]`（见第 3 节）。

---

## 2. 提交时：`getDynamicFormData` 行为

源码：`src/packages/components/complex/flmDynamicForm/flmDynamicForm.vue`。

### 2.1 `type === 'form'` 分组

1. 用分组 **`id`** 在 `formRefs` 里取对应的 **`flmGridForm` 实例**（通过 `ref` 回调挂载）。
2. 从实例上解析 **`formModel`**（兼容多种 Vue 3 实例形态）：

   - `inst.formModel`
   - `inst.exposed.formModel`
   - `inst.$.exposed.formModel`
   - `inst.$.setupState.formModel`

3. 若当前 `id` 取不到 `formModel`，会**兜底**：在 `formRefs` 里找**第一个**能 `pickFormModel` 成功的实例（多表单分组时若 `id` 不一致可能出现合并错乱，应保证配置里 **`group.id` 与 ref 使用的 id 一致**）。
4. 将 `formModel` **深拷贝为普通对象**后 **`Object.assign` 到结果根对象**，即：**同一动态表单里多个 `form` 分组的字段会摊平到同一层**；若不同分组出现相同 `prop` 键，**后遍历的分组会覆盖先遍历的**。

### 2.2 `type === 'subForm'` 分组

- 数据来自 **`subFormData[id]`**（与 `flm-sub-form` 的 `v-model` 同步），在结果中为 **`payload[groupId] = 行数组`**，**不摊平到根对象**。

### 2.3 底部按钮与事件参数

- `submit` / `cancel` / `reset`：`emit(event, getDynamicFormData())`，即上述合并后的 **payload**。
- 其他按钮：`emit('customEvent', { event, data: btn })`，**不带**自动汇总的表单数据；若业务需要当前表单值，需自行 `ref` 动态表单或在外层监听并缓存。

---

## 3. `flmGridForm` 内：`formModel` 如何写入

源码：`src/packages/components/complex/flmGirdForm/flmGridForm.vue`。

### 3.1 核心结构

- `formModel`：`reactive<Record<string, any>>({})`，通过 **`expose({ formModel, reset })`** 暴露给父级。
- 任意字段更新统一走 **`formChange(value, prop)`** → **`formModel[prop] = value`**。

### 3.2 与配置的双向同步

- **`props.config.model`** 变化时：`updateFormModel` 把外部 model 合并进 `formModel`；`updateControl` 把 `formModel` 同步回各 `controlConfig` 的展示值（`modelValue` 或 `model-value`，由 `isInputControl` 决定）。
- **`formModel` 变化时：`watch(formModel, …)` → `updateControl`，保证控件 config 上的当前值与模型一致。

### 3.3 表单项 `prop` 即 `formModel` 的键

每个 `items[]` 项有 **`prop`（字符串）**。`controlDom` 里注入回调时使用的就是这个 **`prop`**：

```ts
;(config as any)[event] = (value: any) => formChange(value, prop)
```

因此 **提交对象里字段名 = 各表单项的 `prop`**。设计器保存时，`FlameFramework` 通常用控件内部 id 作为 `prop`（如 `ctrl_xxx_时间戳`），业务若需要固定业务字段名，需在**加载配置后映射**或改设计器生成规则（见外部组件指南第 5 节）。

---

## 4. 控件渲染分支与「写回」方式

`controlDom` 对不同类型处理不同，**只有写回路径接上了 `formChange`，值才会进 `formModel`**。

| 类型 / 分支 | 写回方式 |
|-------------|----------|
| **`flmInput`** | `config.onInput` → `formChange(value, prop)`（注意不是 `onChange`） |
| **`flmSearchSelect`** | 单独渲染，使用 `onUpdate` 等，内部再 `formChange` |
| **`flmDatePicker`** | `onUpdate:modelValue` → `formChange` |
| **`flmImage` / 上传** | `onUpload` 等 → `formChange` |
| **`FlmEditor`** | `onUpdate` → `formChange` |
| **其余（含 `resolveComponent` 自定义）** | 拷贝后的 `config` 上挂 **`onChange`**（非 flmInput）→ `formChange`；渲染 **`<DynamicComp config={config} />`** |

**自定义组件（`componentList` 拖入、保存为某 `controlType`）**落在最后一行：**必须**在用户改值时调用传入的 **`config.onChange(newValue)`**。若错误地只使用 `emit('update:modelValue')` 而父级未绑定，**不会**更新 `formModel`。

若某自定义控件**错误地把 `controlType` 设成 `flmInput`**，则框架会注入 **`onInput`**；此时组件内应调用 **`config.onInput`**，而不是 `onChange`。

---

## 5. 单独使用 `flmGridForm` 时的取值

未包在 `flmDynamicForm` 里时：

- **提交类按钮**：`flmGridForm` 内在校验通过后 **`emit('submit', formModel)`**（引用同一 reactive 对象）。
- **程序化读取**：父组件通过 **`ref` + `expose`** 拿到子组件实例，读取 **`formModel`**（与 `flmDynamicForm` 的 `pickFormModel` 目标一致）。

`flmDynamicForm` 正是依赖子级 **`expose` 的 `formModel`** 做聚合，而不是依赖 `submit` 事件参数。

---

## 6. 常见问题与排障

### 6.1 基础控件有值，自定义控件提交没有

1. 确认运行时 **`resolveComponent(controlType)`** 能解析到正确组件（全局注册名与 JSON 中 `controlType` 一致）。  
2. 在自定义组件内确认用户操作后是否调用了 **`config.onChange`**（或 **`config.onInput`** 当类型为 `flmInput`）。  
3. 确认没有把 `formModel` 当作只读：应通过回调写回，而不是仅本地 `ref`。

### 6.2 多个 `form` 分组字段「串了」或只有一部分有数据

- 检查每个 **`form` 分组的 `id`** 是否与 `flmDynamicForm` 里 `setFormRef(id, el)` 使用的 id 一致。  
- 若 `id` 缺失或重复，`pickFormModel` 可能走到兜底逻辑，导致读到错误或单个实例。

### 6.3 `prop` 为设计器 id，与后端字段不一致

- 属**配置形态**问题：在加载 JSON 后批量改写 `items[].prop`，或在保存/加载管线做映射；不在 `getDynamicFormData` 内自动转换。

### 6.4 `subForm` 有数据，`form` 没有

- `subForm` 走 **`subFormData`**；`form` 走 **`flmGridForm.formModel`**。两套机制独立，勿混用。

---

## 7. 相关源码索引

| 主题 | 路径 |
|------|------|
| 汇总提交、`formRefs`、`pickFormModel`、`getDynamicFormData` | `src/packages/components/complex/flmDynamicForm/flmDynamicForm.vue` |
| `formModel`、`formChange`、`controlDom`、各特殊分支、`expose` | `src/packages/components/complex/flmGirdForm/flmGridForm.vue` |
| 保存时 `items[].prop` / `controlType` / `controlConfig` | `src/packages/components/framework/framework.vue` → `buildFrameworkConfig` |
| 自定义控件契约（注册与设计器） | [framework-external-components.md](./framework-external-components.md) |

---

## 8. 自检清单（取值相关）

- [ ] 自定义控件在变更时调用 **`config.onChange`**（或 **`config.onInput`** 当 `controlType === 'flmInput'`）  
- [ ] 从 **`config.modelValue` / `config['model-value']`** 读取当前值并与展示同步  
- [ ] 多 **`form` 分组**时 **`group.id` 唯一且稳定**，与 ref 键一致  
- [ ] 需要固定业务字段名时，已对 **`items[].prop`** 或 payload 做映射  
- [ ] **`subForm`** 数据在 payload 中为 **`payload[groupId]`** 数组，而非根级扁平字段  
