# 表单客制化(Form Hook)
系统通过 Form Hook(表单级 Hook) 与 Data Hook(数据级 Hook) 提供表单层面的客制化能力,用于实现字段默认值、联动、显隐、只读、必填、备选项、数据变换等行为。该能力取代了早期版本中的字段级客制化(Field Hook / Dynamic Field Hook),后者已从平台中移除。
# 目标读者
本文档的目标读者为:本系统的开发和实施人员
# 概述
Form Hook 与 Data Hook 均关联一个 Dynamic Logic 对象,通过 DynamicForm 上的三个字段进行配置:
| 字段 | 类型 | 说明 |
|---|---|---|
formHook.name | DynamicLogic | 表单级 Hook:字段默认值、联动、显隐、只读、必填、备选项等 |
formHookTriggerFields | String | 逗号分隔的字段名列表;这些字段在界面上发生变更时,会触发 Form Hook 重新执行 |
dataHook.name | DynamicLogic | 数据级 Hook:对列表/详情返回数据做变换 |
提示
旧版文档中的「字段客制化」(Dynamic Field Hook、Field dependencies hook、字段快捷搜索逻辑等)已随平台演进移除,相关能力统一由 Form Hook 提供,请勿再按旧方式配置。
# Form Hook(表单级 Hook)
# 触发时机
Form Hook 在以下时机执行,不同时机注入的 object 不同:
| 时机 | object |
|---|---|
| 新建表单打开时 | 空 Map [:] |
| 打开详情、编辑表单时 | 数据库中的记录(GORM 实体);表单配置了 Data Hook 时,为 Data Hook 返回的该条记录(Map) |
| 加载列表数据时,每行执行一次 | 同上,为该行的记录 |
formHookTriggerFields 中的字段在界面上被修改时 | 界面上当前的表单值(尚未保存),类型为 org.grails.web.json.JSONObject |
只有最后一种时机(以及下面说明的打开编辑表单、详情后的补充调用)initiated 为 true,changedFields 为本次变化的字段名;其他时机 initiated 为 false,changedFields 为空列表。各时机对应的接口见 相关接口。
打开编辑表单、详情时会多执行一次
打开编辑表单或详情时,表单数据加载完成后,前端还会把触发字段当作变化的字段,再调用一次 POST /form/$formId/refresh:这次 initiated 为 true,changedFields 为这些字段名(实测是界面上有值、可编辑的触发字段),object 为界面上的表单值。也就是说,即使用户没有改动任何字段,字段联动的逻辑在打开编辑表单时也会执行一次。所以字段联动分支里不要无条件返回字段的 value,否则记录中已保存的值会在打开编辑表单时被覆盖。新建表单打开时,只要触发字段没有值(例如没有被 Form Hook 设置默认值),就不会有这次调用。
新建和编辑时,Form Hook 用于设置字段默认值与字段属性;字段被修改时用于字段联动;详情和列表中用于按记录内容动态调整字段属性。
# 注入变量
Form Hook 执行时,系统注入如下变量:
| 变量名称 | 变量类型 | 描述 |
|---|---|---|
form | tech.muyan.dynamic.form.DynamicForm | 当前表单对象 |
changedFields | java.util.List<String> | 触发本次执行的字段名列表,只在 initiated 为 true 时有值,其他时机为空列表 |
initiated | boolean | 字段变更触发(包括打开编辑表单、详情后的补充调用)时为 true,其他时机为 false |
object | 随时机不同 | 当前记录,取值见上表 触发时机 |
owner | 对象 | 内嵌子表、关联列表场景下的主记录(例如在订单详情中加载订单明细列表时为该订单),其他场景为 null |
userContext | tech.muyan.api.security.MuyanAuthentication | 当前操作用户信息 |
此外可以使用所有动态逻辑通用的 application、invoke、logger 变量。Form Hook 没有 log 变量,打印日志请使用 logger。
# 返回结果
Form Hook 返回一个以字段 key 为键的 Map,每个字段项可包含:
| 键 | 类型 | 说明 |
|---|---|---|
value | 任意 | 字段的默认值 / 新值 |
display | String | hide 隐藏字段;show 显示字段;readonly 只读显示。字段变更时返回 hide 当前会导致表单崩溃,见本节示例后的 warning |
required | boolean | 是否必填(false 时字段自动转为可空) |
options / enumOptions | 数组 | 选择类字段的备选项 |
min / max | 数值 | 数值字段的校验范围 |
editable | boolean | 是否可编辑 |
helpText | String | 字段帮助信息 |
| 其他 FieldProps | 任意 | 平台支持的任何字段属性(含字段组 extInfo 等) |
示例(订单的新建、编辑表单共用这个 Form Hook,触发字段见下文 配置方式):
// 字段联动:「订单类型」为加急(URGENT)时,「交货日期」必填
if (initiated && changedFields.contains('orderType')) {
return [
deliveryDate: [
required: object?.orderType == 'URGENT'
]
]
}
// 新建表单打开:object 为空 Map,为「状态」设置默认值和备选项
if (!initiated && !object) {
return [
status: [
value : 'DRAFT',
options: ['DRAFT', 'SUBMITTED', 'APPROVED']
]
]
}
// 其他时机(打开编辑表单、查看详情、加载列表、其他触发字段变更)不做修改
return [:]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这段代码的要点:
- 设置默认值的分支要用
!initiated && !object限定为新建。如果不加判断,打开编辑表单、查看详情时也会返回value,前端会用它覆盖记录中已保存的状态;其他触发字段(例如下文 CSV 中的customer)变更时也一样。 - 字段联动分支只调整字段属性(
required),不改字段的值。原因见上文 打开编辑表单、详情时会多执行一次。 - 其他情况返回空 Map
[:],表示不修改任何字段。
字段变更时不能返回 display: 'hide'
当前版本在字段变更触发(initiated 为 true)时返回 display: 'hide',表单会崩溃,显示 "Something went wrong"(前端报 React error #300),新建和编辑表单都会出现,这是当前版本的界面问题。表单打开时(initiated 为 false)返回 display: 'hide' 可以正常隐藏字段。在修复之前,请避免在字段联动中切换字段显隐,可以改为切换 required 等其他属性。
# 配置方式
通过 CSV 种子数据在 DynamicForm.csv 中配置:
name(*),label,description,objectType.shortName(*),type.name(*),formHook.name,formHookTriggerFields,dataHook.name,extInfo,accessRequirement.name
Create order,,,Order,CREATE,order_form_hook,"orderType,customer",,,USER
List order,,,Order,LIST,,,order_list_data_hook,,USER
2
3
formHook.name:关联的 Form Hook 动态逻辑名称formHookTriggerFields:逗号分隔的触发字段名列表。有多个字段时,整列要用英文双引号括起来,否则逗号会被当成列分隔符,后面的字段名会错位到下一列dataHook.name:关联的 Data Hook 动态逻辑名称
平台执行 Form Hook / Data Hook 时不校验逻辑类型,建议统一使用 FUNCTION_LOGIC 类型(平台内置的 DynamicFormField FormHook 即为该类型)。
# 内置示例
平台内置的动态逻辑 DynamicFormField FormHook 是一个 Form Hook,挂在表单字段的新建和编辑表单(Create dynamic form field、Update dynamic form field)上,可以在 开发定制 > 动态逻辑 > 动态逻辑 中搜索这个名称查看代码。它的逻辑如下:
initiated为false,并且object中有所属表单(form)时:把"字段组"(group)的备选项限定为该表单的字段组(通过extInfo.defaultOptionsCondition),并按表单类型设置"字段类型"(fieldType)的备选项(enumOptions)。新建表单打开时object为空 Map,所以这一段只在编辑、查看详情时生效。- 无论
initiated取值,遍历changedFields,当变化的字段是name时,把字段名按驼峰拆分后回填到label。这两个表单配置的触发字段是fieldName,不是name,所以当前这一段实际不会被触发。
# Data Hook(数据级 Hook)
Data Hook 用于对列表与详情接口返回的数据进行变换,注入变量:
| 变量名称 | 变量类型 | 描述 |
|---|---|---|
ids | List<Long> | 要获取的记录 id。查看详情时为只含该记录 id 的列表;按多个 id 获取数据时为这些 id;GET /form/data/{formId} 时为空列表;列表、搜索、关联列表、树形数据查询时为 null |
offset | Integer | 分页偏移量,只在列表、搜索、关联列表查询时有值 |
cursor | Integer | 游标,只在关联列表查询时可能有值 |
max | Integer | 每页条数,只在列表、搜索、关联列表查询时有值 |
owner | 对象 | 关联列表场景下的主对象,其他场景为 null |
conditions / parsedConditions | 对象 | 查询条件 |
userContext | tech.muyan.api.security.MuyanAuthentication | 当前操作用户信息 |
与 Form Hook 一样,Data Hook 可以使用 application、invoke、logger,没有 log 变量。
配置了 Data Hook 的表单,不再查询数据库,列表、详情等接口直接使用 Data Hook 的返回值。返回结构如下:
return [
data : [ // 本页的记录列表
[id: 1, name: "..."],
],
total: 1 // 记录总数,用于分页
]
2
3
4
5
6
查看详情时,平台取 data 中的第一条记录。因此 Data Hook 需要根据 ids 是否为 null 判断当前是在查列表,还是在按 id 取记录。
提示
Data Hook 的结果同时服务于 AMIS 表单:AMIS 表单的数据通过 GET /form/data/$formId 获取,该接口会执行 Data Hook 做数据变换。
# 相关接口
下表路径省略了 /api 前缀,从外部调用时需加上,见 地址前缀。
| 接口 | 说明 |
|---|---|
POST /form/formHook/$formId | 新建表单打开时获取 Form Hook 数据 |
POST /form/$formId/refresh | 字段变更后批量刷新表单字段属性;打开编辑表单、详情后也会调用一次,见 触发时机 |
GET /data/$domainName/$id/withFormHook?formId= | 详情、编辑时获取记录数据 + Form Hook 结果 |
GET /form/data/$formId | AMIS 表单数据(含 Data Hook 变换) |