# TblForm 纯表单组件

`TblForm` 是面向**非流程场景**的移动端表单容器组件。与 `src/form/table.jsx`（流程表单）相比，它去除了批示意见、退回意见、流程按钮等流程能力，仅保留表单字段渲染、联动计算、子表及保存逻辑。

组件内部自动完成：

- 加载表单：`GET /api/m/approve/getPureMobileForm`
- 保存数据：`POST /api/m/model/formmgr/runtime/runtimeDataSave`
- 业务扩展：`pages/form_common/{formCode}`（`formCode` 为空时不加载）

---

## 目录结构

```
src/tblform/
├── TblForm.jsx          # 主组件（推荐直接使用）
├── FlowCommentPane.jsx  # 流程批示意见区（流程页单独使用，TblForm 不包含）
├── formFieldProps.js    # Form/SubForm 公共 props 组装
├── pureFormAdapter.js   # 接口数据适配
├── formSaveUtils.js     # 保存参数组装、getAllEditIds
├── formCommon.js        # form_common 扩展加载与字段变更扩展
├── index.js             # 统一导出
└── README.md
```

---

## 与流程表单 Table 的区别

| 能力 | `form/table`（流程） | `tblform/TblForm`（纯表单） |
|------|----------------------|------------------------------|
| 入参 | 大量流程参数（批示意见、commentField 等） | **formId、editType、dataId**；可选 **formRelaField**（关联设置 setId，直接入参） |
| 数据加载 | 流程接口 `getMobileForm` | `getPureMobileForm` |
| 数据保存 | `saveForm`（流程草稿） | `runtimeDataSave` |
| 批示意见 UI | 内置 | 无（可用 `FlowCommentPane` 单独挂载） |
| 业务扩展 | `pages/flow_common/{module}` | `pages/form_common/{formCode}` |

---

## 快速开始

### 1. 引入组件

```jsx
import React from 'react';
import Group from 'saltui/lib/Group';
import Button from 'saltui/lib/Button';
import TblForm from '../tblform/TblForm';
```

### 2. 最简用法（三个必填参数）

```jsx
export default class PureFormPage extends React.Component {
  render() {
    return (
      <Group.List>
        <TblForm
          formId="4028b28a9cf4442a019cf45667820020"
          editType="add"
          dataId=""
        />
      </Group.List>
    );
  }
}
```

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `formId` | string | 是 | 表单 ID |
| `editType` | string | 是 | `add` 新增 / `edit` 编辑 / `view` 查看 |
| `dataId` | string | 否 | 业务数据 ID；新增传 `''`，编辑/查看传已有 ID |

### 3. 带保存按钮的完整示例

```jsx
export default class PureFormPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      formId: '4028b28a9cf4442a019cf45667820020',
      editType: 'add',
      dataId: '',
    };
  }

  handleSave = () => {
    this.refs.tblForm.save({
      moduleCode: 'approve',
      formType: '4',
    }).then((result) => {
      // result.existDataId  保存后的数据 ID
      // result.formHtmlVersion / result.formJsonVersion
      this.setState({ dataId: result.existDataId, editType: 'edit' });
    });
  };

  render() {
    const { formId, editType, dataId } = this.state;
    return (
      <div>
        <Group.List>
          <TblForm
            ref="tblForm"
            formId={formId}
            editType={editType}
            dataId={dataId}
            onChange={(formData) => {
              console.log('表单变更', formData);
            }}
            onLoad={(formData, meta) => {
              console.log('加载完成', formData, meta);
            }}
            onSaveSuccess={(result, formData) => {
              console.log('保存成功', result.existDataId);
            }}
          />
        </Group.List>

        {editType !== 'view' ? (
          <Button type="primary" onClick={this.handleSave}>
            保存
          </Button>
        ) : null}
      </div>
    );
  }
}
```

---

## Props 说明

### 核心参数

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `formId` | string | - | 表单 ID |
| `editType` | string | `'add'` | 编辑类型，见下表 |
| `dataId` | string | `''` | 数据 ID |
| `formCode` | string | `''` | 扩展编码；不传则使用接口返回的 `form.formcode` |
| `formRelaField` | string | `''` | 关联字段设置 setId；**直接入参**，无需从流程/表单加载接口获取。有值时内部请求 `getRelaFieldsMap` 启用字段关联（显隐/必填等） |

```jsx
<TblForm
  formId="4028b28a9cf4442a019cf45667820020"
  editType="add"
  dataId=""
  formRelaField="4028b2c6a03bd3a401a03d9421b00bfb"
/>
```

> 流程场景下 `formRelaField` 由 `getMobileForm` 等接口返回；纯表单场景不依赖该接口，由业务页直接传入关联设置 ID 即可。

### 回调参数（可选）

| 参数 | 类型 | 说明 |
|------|------|------|
| `onChange` | `(formData, itemParam?, subTblNo?) => void` | 字段变更后触发，`formData` 与 `processInfo.editFormData` 结构一致 |
| `onLoad` | `(formData, meta) => void` | 表单加载并渲染前适配完成 |
| `onSaveSuccess` | `(result, formData) => void` | 保存成功（`result.code === '1'`） |

---

## 实例方法（ref）

通过 `ref` 调用：

```jsx
<TblForm ref="tblForm" ... />
```

| 方法 | 返回值 | 说明 |
|------|--------|------|
| `getEditFormData()` | Object | 当前表单数据（同 `editFormData`） |
| `save(options?)` | Promise | 调用保存接口 |

### save(options) 可选参数

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `formType` | string | `'4'` | 表单类型（与 PC 端一致） |
| `moduleCode` | string | `'approve'` | 模块编码，写入 `moduleCode` |
| `logField` | string | `''` | 操作日志字段 |
| `formVersion` | string | `''` | 表单版本号 |
| `showToast` | boolean | `true` | 是否自动 Toast 提示 |

---

## 接口说明

### 加载：getPureMobileForm

- **地址**：`/api/m/approve/getPureMobileForm`
- **方法**：GET
- **参数**：`formId`、`editType`、`dataId`
- **鉴权**：Header `Authorization: Bearer {token}`（项目统一处理）

**响应结构（节选）：**

```json
{
  "success": true,
  "code": "0",
  "content": {
    "form": {
      "formId": "4028b28a9cf4442a019cf45667820020",
      "formcode": "test_lizhao0316",
      "formTblName": "lztest20260312",
      "item": [ /* 字段定义 */ ]
    },
    "formData": {
      "formId": "4028b28a9cf4442a019cf45667820020",
      "dataId": "",
      "mainTblName": "lztest20260312",
      "mainTblData": [],
      "subTbl": []
    }
  }
}
```

### 保存：runtimeDataSave

- **地址**：`/api/m/model/formmgr/runtime/runtimeDataSave`
- **方法**：POST（`application/x-www-form-urlencoded`）

| 参数 | 说明 |
|------|------|
| `runtimeData` | `JSON.stringify(editFormData)`，结构与 `processInfo.state.editFormData` 一致 |
| `editType` | `add` / `edit` |
| `allEditIds` | 主表/子表/关联表已有记录 id，逗号分隔 |
| `existDataId` | 新增时首次为空；再次保存传上次返回的 `existDataId` |
| `formType` | 默认 `4` |
| `moduleCode` | 模块编码 |
| `formVersion` | 表单版本（可选） |
| `logField` | 日志字段（可选） |

**成功响应（节选）：**

```json
{
  "success": true,
  "content": {
    "code": "1",
    "info": "保存成功",
    "existDataId": "xxxxxxxx",
    "formHtmlVersion": "1",
    "formJsonVersion": "1"
  }
}
```

---

## editFormData 数据结构

与流程页 `processInfo.jsx` 中 `editFormData` **完全一致**，便于复用原有校验、提交逻辑。

```javascript
{
  formId: '4028b28a9cf4442a019cf45667820020',
  dataId: '',                    // 保存后由 existDataId 回写
  mainTblName: 'lztest20260312',
  mainTblData: [
    { key: 'f11$', value: 'xxx', required: false, /* ... */ }
  ],
  subTbl: [
    {
      subTblName: 'lztest20260316',
      subTblData: [
        [ { key: 'f2', value: 'yyy' } ]
      ]
    }
  ],
  relatedTbl: [],
  formItem: [ /* 字段元数据 */ ],
  linkFields: [],
  trans: false
}
```

---

## 业务扩展（form_common）

### 扩展文件路径

| 类型 | 路径/约定 |
|------|-----------|
| 表单级扩展模块 | 业务工程 `pages/form_common/{formCode}.js`（推荐） |
| 表单级 mobileExt | 全局变量 `{formCode}_mobileExt` |
| 全局模块（备选） | `window.{formCode}_formCommon` |

> **说明**：`fmui-base` 通过包内 `lib/form/form_common` 的 `require.context` 加载扩展，**不会**再 `require('pages/form_common/...')`，避免宿主未建目录时 webpack 报 `Module not found`。业务文件仍建议放在 `pages/form_common/`，并在宿主 webpack 中配置 alias（见下）。

**宿主 webpack alias（approve 等工程必配一次）：**

```javascript
const path = require('path');

// webpack resolve.alias 中增加：
[path.resolve(__dirname, 'node_modules/fmui-base/lib/form/form_common')]: path.resolve(
  __dirname,
  'pages/form_common'
),
```

未配置 alias 时仅加载包内占位模块，扩展不生效但不报错；也可在页面入口挂载 `window.xxx_formCommon`。

**formCode 来源优先级：**

1. 组件 props：`formCode`
2. 接口返回：`content.form.formcode`
3. 为空 → 不加载扩展（不报错）

### 扩展示例

新建 `pages/form_common/test_lizhao0316.js`：

```javascript
export default {
  /** 字段变更（TblForm.change 时调用） */
  dealwithFormItemChange(itemParam, table) {
    // itemParam: 当前字段信息
    // table: TblForm 实例（可访问 refs）
  },

  /** 字段参数处理（子表 SubForm 加载时也会调用，可设置 subColumnCount: 1-4） */
  dealwithCommonFormParam(itemParam, subForm) {
    if (itemParam.key === 'sub_tbl_1') {
      itemParam.subColumnCount = 2;
    }
    return itemParam;
  },

  /** 保存前校验（可选） */
  saveBefore(type, state, success, fail) {
    // type: 2 表示保存
    success();
  },
};
```

可选全局扩展 `test_lizhao0316_mobileExt`：

```javascript
window.test_lizhao0316_mobileExt = {
  dealwithFormItemChangeExt(itemParam, table, callback) {
    // callback([{ type: 'main', key: 'f11$', data: '联动值' }]);
  },
  dealwithFormParamExt(itemParam, subForm) {
    // 子表多列：在现有扩展里赋值 subColumnCount（1-4），SubForm/Table 会自动读取
    if (itemParam.key === 'sub_tbl_1') {
      itemParam.subColumnCount = 2;
    }
    return itemParam;
  },
};
```

流程表单在 `pages/flow_common/{module}.js` 的 **`dealwithCommonFormParam`** 中同样可设置 `itemParam.subColumnCount`；流程页还可通过 `{module}_{formKey}_mobileExt.dealwithFormParamExt` 按子表编码区分列数。无需新增扩展方法名。

### 手动加载扩展（高级）

```javascript
import { loadFormCommon, getFormCommon } from '../tblform';

loadFormCommon('test_lizhao0316');
const ext = getFormCommon();
if (ext && ext.dealwithFormItemChange) {
  // ...
}
```

---

## 流程页组合用法（表单 + 批示意见）

纯表单用 `TblForm`，流程批示用 `FlowCommentPane`（与 `TblForm` 分离）：

```jsx
import React from 'react';
import TblForm from '../tblform/TblForm';
import FlowCommentPane from '../tblform/FlowCommentPane';

export default class ProcessFormTab extends React.Component {
  render() {
    const t = this;
    return (
      <React.Fragment>
        <TblForm
          ref="tblForm"
          formId={t.state.formId}
          editType={t.state.editType}
          dataId={t.state.dataId}
          onChange={t.change}
        />
        <FlowCommentPane
          commentField={t.state.commentField}
          commentDefaultList={t.state.commentDefaultList}
          commentBackList={t.state.commentBackList}
          hasCommentField={t.state.hasCommentField}
          newspyj={t.state.newspyj}
          defaultValue={t.state.defaultValue}
          commentUpload={t.state.commentUpload}
          commentAttitude={t.state.commentAttitude}
          inscriptionShow={t.state.inscriptionShow}
          module={t.state.module}
        />
      </React.Fragment>
    );
  }
}
```

---

## 工具函数导出

```javascript
import {
  TblForm,
  FlowCommentPane,
  normalizePureMobileForm,
  editTypeToStatus,
  getAllEditIds,
  buildRuntimeDataSaveParams,
  isRuntimeSaveSuccess,
  loadFormCommon,
  getFormCommon,
} from '../tblform';
```

| 函数 | 说明 |
|------|------|
| `normalizePureMobileForm(content, editType)` | 将 `getPureMobileForm` 响应转为组件内部状态 |
| `editTypeToStatus(editType)` | `add/edit/view` → `0/1/2` |
| `getAllEditIds(formData)` | 收集保存用 `allEditIds` |
| `buildRuntimeDataSaveParams(formData, options)` | 组装 `runtimeDataSave` 请求体 |
| `isRuntimeSaveSuccess(content)` | 判断 `content.code === '1'` |

---

## 常见问题

### 1. 新增保存后如何继续编辑？

第一次 `save` 成功后，组件会把 `existDataId` 写入内部 `savedDataId` 和 `editFormData.dataId`。再次点击保存时，会以 `existDataId` 传给后端（符合「新增后再次保存」逻辑）。

建议业务页同步更新 state：

```javascript
onSaveSuccess={(result) => {
  this.setState({
    dataId: result.existDataId,
    editType: 'edit',
  });
}}
```

### 2. formCode 为空会怎样？

不会加载任何扩展模块，不影响表单正常渲染，仅无自定义扩展逻辑。

### 2.1 启动报 Can't resolve 'pages/form_common'？

请升级 `fmui-base` 至已改用包内 `form_common` 的版本，并在宿主 webpack 为 `lib/form/form_common` 配置 alias 指向业务的 `pages/form_common`（见上文）。不要直接在 `fmui-base` 里写 `require('pages/form_common/...')`。

### 3. 为何不用 flow_common？

`TblForm` 构造时传入 `module: ''`，`table.jsx` 在 `module` 为空时**不加载** `flow_common`，避免与流程扩展混用。字段级扩展通过 props `formCode` 走 `pages/form_common/{formCode}`。

### 4. 查看模式如何禁用保存？

`editType="view"` 时字段只读；页面层不渲染保存按钮即可。

---

## 依赖说明

- React + SaltUI（`Loading`、`Toast` 等）
- `../form/form`、`../form/subForm`、`../form/table`（继承联动计算能力）
- `../db/db`（`DB.form.getPureMobileForm`、`DB.form.runtimeDataSave`）
- 全局方法：`getLoginUserInfo()`（鉴权）

---

## 版本记录

| 版本 | 说明 |
|------|------|
| 1.0.1 | 支持 props 直接传入 `formRelaField`（关联设置 setId），启用字段关联 |
| 1.0.0 | 初始版本：纯表单加载/保存、form_common 扩展、FlowCommentPane 分离 |
