# 表单客制化(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 [:]
1
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
1
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               // 记录总数,用于分页
]
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 变换)
Last Updated: 2026/9/24 14:27:35