# 字段客制化

系统支持通过客制化的方式,实现对字段的默认值、校验、联动等行为进行定制化。

# 目标读者

本文档的目标读者为:本系统的开发和实施人员

# 客制化字段默认值

除了支持使用 grails 模型定义的方式 (opens new window)指定字段保存到数据库中时的默认值, 系统还支持通过客制化指定创建表单中的字段的默认值。

# 注入变量

该客制化运行时,系统的注入变量如下表所列:

变量名称 变量类型 描述
objectType tech.muyan.DomainClass 当前操作的对象类型
userContext grails.plugin.springsecurity.userdetails.GrailsUser 当前操作的用户信息
destColumn java.lang.String 设定默认值的目标列
destColumnType java.lang.String 目标列的完整类型名称
application grails.core.GrailsApplication 当前的 grails 应用上下文
log Closure<?> 用于打印执行日志的 log 闭包

# 返回结果

该客制化运行的结果需要返回一个包含 key 为 "result" 的 Map 结果,如下所示

// result key 对应的 value 即为系统会传递到前台的默认值
// The value corresponding to the result key is the default value passed to the frontend by the system
return [result: xxx]
1
2

# 客制化字段校验

表单字段可以定义动态的校验逻辑,在前台表单中该字段发生修改时,会通过接口调用后台的校验逻辑进行校验, 如果校验不通过,则会在前台显示错误信息。

# 注入变量

动态字段校验逻辑执行时,系统注入的相关变量如下标所示

变量名称 变量类型 描述
userContext grails.plugin.springsecurity.userdetails.GrailsUser 当前操作的用户信息
application grails.core.GrailsApplication 当前的 grails 应用上下文
domainName java.lang.String 当前操作的对象类型名称
destColumn java.lang.String 校验的目标列名称
destColumnValue java.lang.Object 目标列的值
create boolean 是否是创建操作,如为 false 表示更新操作
requestData java.lang.Object 前台表单所有字段值的 JSON 序列化
objectId java.lang.Long 校验的对象 id, 对于创建操作,值为 -1
log Closure<?> 用于打印执行日志的 log 闭包

提示

requestData 的示例结构为:

{
  "formValues":{},  // 界面上显示的表单字段的值,包括当前在界面上还未保存到数据库中的值
  "record":{}, // 如果是更新操作,该值为当前记录保存在数据库中的值,如果是创建操作,该值为所有有默认值的字段的值
  "unique":[] // 与当前触发字段组合的唯一性校验的字段列表
}
1
2
3
4
5

可以使用 requestData.getAt("formValues").getAt("fieldName") 来获取界面上名为 fieldName 的字段的值

# 返回结果

客制化校验返回结果的结果如下所示, valid 表示校验是否通过, message 是校验失败时的提示信息,如果校验成功,该值会被忽略。

// result key 对应的 value 即为系统会传递到前台的默认值
// The value corresponding to the result key is the default value passed to the frontend by the system
return [
  valid: true | false,
  message: "校验失败的界面提示信息 Field validation failure information"
]
1
2
3
4
5

# 客制化字段联动

系统支持通过客制化的方式,实现字段之间的联动,可实现的逻辑包括:

  • 决定目标字段是否隐藏
  • 决定目标字段是否只读
  • 决定目标字段是否必填
  • 可以直接设定目标字段的值
  • 如果目标字段是选择类型,则可以设定其他选择字段的备选项

以下是向系统注入字段联动处理逻辑需要创建的 Dynamic Field Hook对象的概述:

Object Type: 选择该客制化逻辑适用的对象类型
Logic source: 逻辑来源,可选为 static field 或 dynamic field
Trigger field: 联动的触发字段,该字段的变化会触发联动
Target field: 联动的目标字段,即该客制化逻辑的目标字段
Trigger dynamic field: 联动的触发动态字段,该动态字段的变化会触发联动
Target dynamic field: 联动的目标动态字段,即该客制化逻辑的目标动态字段
Hook Type: 选择 Field Dependencies hook 
core Logic: 客制化代码,具体代码中可使用的注入变量和返回值约定参见下述文档
1
2
3
4
5
6
7
8

提示

Trigger field 和 Trigger dynamic field 同时只能有一个字段设置值 Trigger field 和 Trigger dynamic field 同时只能有一个字段设置值

具体设置哪个字段的值由字段 Logic source 决定

# 注入变量

该客制化代码中,可使用的注入变量如下表所示:

变量名称 变量类型 描述
application grails.core.GrailsApplication 当前的 grails 应用上下文
userContext grails.plugin.springsecurity.userdetails.GrailsUser 当前操作的用户信息
domainName java.lang.String 不包含package部分的,当前操作的对象类型
sourceColumn java.lang.String 驱动列,该字段的变化触发了该客制化
destColumn java.lang.String 目标列,客制化的返回结果会作用于该列
sourceColumnValue java.lang.String 驱动列的值
object org.grails.web.json.JSONObject 用户正在编辑的对象当前界面上各个字段的值
log Closure<?> 用于打印执行日志的 log 闭包

提示

在客制化代码被调用的时点,注入的 object 对象只是界面上的输入框中,每一列的暂存数据, 还没有保存到后台的数据库中。

# 返回结果

该客制化代码需要返回的结果为一个多层的 Map 数据,具体返回数据及其说明如下:

return [
  //指定该字段的显示状态:hide 为隐藏、show 为显示且可编辑、readonly 为只读
  //Specify the display status of the field: hide for hidden, show for editable, and readonly for read-only
  display: hide | show | readonly

  // 指定该字段是否必填, true 表示必填,false 表示非必填
  // Specify whether the field is required: true for required, false for non-required
  required: true | false

  // 指定该字段的值,如果是个多选字段,可以使用 [] 的形式来指定多个值
  // Specify the value of the field, if it is a multi-select field, you can use the [] form to specify multiple values
  value: [] 或者 xxx,

  // 如果该字段是个选择类型的字段,如下的返回值指定其备选项,
  // 每个备选项均包括显示给用户看的 Label 属性和实际保存的 value 属性
  // If the field is a select type field, the following return value specifies its options,
  // each option includes the Label attribute displayed to the user and the value attribute actually saved
  options: [
      {
          "value": "ABSTRACT_DATE",
          "label": "Abstract date"
      },
      {
          "value": "ABSTRACT_DATE_TIME",
          "label": "Abstract date with time"
      }
  ]
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

提示

如果希望将某个选择字段的备选项清空,需要将 options 设置为空数组传递到前台,而不能传递 undefined 或者 null 到前台

{ 
  options: [] 
}
1
2
3

# 字段联动代码示例

如下的相关代码示例片段演示了一些可能在业务中用到的返回形式

  • 将客制化的目标字段设置为隐藏或只读
// 声明返回结果的 Map 对象
// Declare a Map object for the return result
Map result = [:] 

// 以下的两句代码是互斥的,否则后一句会覆盖前一句
// The following two lines of code are mutually exclusive, otherwise the latter will override the former
// 设置目标字段在界面上隐藏
// Set the target field to be hidden on the interface
result.put("display", "hide") 

// 设置目标字段在界面上为只读字段
// Set the target field to be read-only on the interface
result.put("display", "readonly") 

// 返回结果
// Return the result
result 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 将客制化字段设置为显示,且必填
// 声明返回结果的 Map 对象
// Declare a Map object for the return result
Map result = [:] 

// 设置目标字段在界面上的正常显示
// Set the target field to be displayed normally on the interface
result.put("display", "show") 

// 设置目标字段为必填字段
// Set the target field as required
result.put("required", true) 

// 返回结果
// Return the result
result 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 根据来源字段的值,修改目标字段的选项(这是一个很通用的,省市字段联动的示例)
// 声名返回结果的 Map 对象
// Declare a Map object for the return result
Map result = [:] 

// 设置目标字段在界面上的正常显示
// Set the target field to be displayed normally on the interface
result.put("display", "show") 

// 设置目标字段为必填字段
// Set the target field as required
result.put("required", true) 

// 如果来源字段的值是 "JS"
// If the value of the source field is "JS"
if (sourceColumnValue == "JS") { 
    // 将目标字段的选项设置为 [NJ, HA],显示分别为 "南京", "淮安"
    // Set the options of the target field to [NJ, HA], displayed as "南京", "淮安" respectively
    result.put("options", [
        [value: "NJ", label: "南京"],
        [value: "HA", label: "淮安"],
    ]) 
    // 将目标字段的值设置为 "NJ"
    // Set the value of the target field to "NJ"
    result.put("value", "NJ") 
}
// 返回结果
// Return the result
result 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 真实系统中的使用示例

本章节列出了两个在正式系统中使用的,使用客制化代码控制表单行为的例子。

# 控制字段显示

创建 DynamicFieldDefinition 时根据 fieldType 控制 optionsJson 字段显示

# Dynamic Field Hook 对象

Target Field: opitonsJson
Logic source: Static field 
Object Type: Dynamic Field Definition
Field Name: fieldType
Hook Type: Field dependencies hook
Core logic: 关联的 Dynamic Logic 定义,可参考下述代码片段
1
2
3
4
5
6

# 客制化代码

// 声明返回的 Map 对象
// Declare a Map object for the return result
def result = [:] 

// 如果 fieldType 字段的值等于 OBJECT、FILE、BOOLEAN 或者 IMAGE
// If the value of the fieldType field equals OBJECT, FILE, BOOLEAN or IMAGE
if (sourceColumnValue == tech.muyan.enums.FieldType.OBJECT.name()
    || sourceColumnValue == tech.muyan.enums.FieldType.FILE.name()
    || sourceColumnValue == tech.muyan.enums.FieldType.BOOLEAN.name()
    || sourceColumnValue == tech.muyan.enums.FieldType.IMAGE.name()) {
  // 将目标字段 optionsJson 设置为只读
  // Set the target field optionsJson to read-only
  result.put("display", "readonly") 
} else {
  // 否则将目标字段 opitonsJson 设置为显示且可编辑
  // Otherwise, set the target field optionsJson to be displayed and editable
  result.put("display", "show") 
}
// 返回 result 对象作为客制化逻辑执行的结果
// Return the result object as the result of custom logic execution
result 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 控制选择字段的备选项

以下的客制化配置和代码,实现了在创建 Object Dynamic Field 对象时,根据用户选择的 dynamicField 字段, 控制 displayComponentType 字段中的备选项。

# Dynamic Logic 对象

Target Field: opitonsJson
Object Type: Dynamic Field Definition
Field Name: fieldType
Logic Type: Field dependencies
Code: 参考下述代码片段
1
2
3
4
5

# 客制化代码

// 声明返回的 result Map,默认显示该字段
// Declare the result Map to be returned, default to show this field
def result = ["display": "show"]

// 声明返回的 options 数组
// Declare the options array to be returned
def options = []

// 从传递到后台的 sourceColumnValue 字段,在这里是 dynamicFieldDefinition 的 id 字段获取对象
// Get the object from the sourceColumnValue field passed to the backend, which is the id field of dynamicFieldDefinition here
def dynamicField = tech.muyan.DynamicFieldDefinition.get(Long.valueOf(sourceColumnValue))

if (null != dynamicField) {
  // 获取对象中保存的 fieldType 信息
  // Get the fieldType information saved in the object
  def fieldType = dynamicField.fieldType

  // 从配置的映射表中获取每一种 fieldType 可以使用的 displayComponentType 列表
  // Get the list of displayComponentTypes that can be used for each fieldType from the configured mapping table
  def optionConfig = tech.muyan.config.DynamicFieldTypeToComponentMapping.FieldTypeToComponentMapping.get(fieldType)
  if (optionConfig != null && optionConfig.size() > 0) {
    // 遍历该列表,将枚举值的 name 和 label 分别放入 options 列表
    // Iterate through the list, putting the name and label of the enum values into the options list respectively
    for (option in optionConfig) {
      options.add(Map.of("value", option.name(), "label", option.label))
    }
    // 将 options 数组放入 result map
    // Put the options array into the result map
    result.put("options", options)
  }
}
// 返回结果
// Return the result
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 字段快捷搜索逻辑

系统支持通过客制化的方式,定制某个对象的页面上,引用其他对象的时候,快捷搜索的过滤逻辑,具体

对于客制化搜索逻辑,需要创建如下的Dynamic Field Hook对象

Target Field: 设置为待搜索的对象字段
Object Type: 选择该客制化逻辑适用的对象类型,这里要设置字段所属于的对象的类型, 而不是待搜索对象的类型
Hook Type: 选择 Field search addon hook 
Core Logic: 客制化代码,具体代码中可使用的注入变量和返回值约定参见下述文档
1
2
3
4

# 注入变量

该客制化代码中,可使用的注入变量如下表所示:

变量名称 变量类型 描述
ownerClass java.lang.Class<?> 该字段所属的对象类型
fieldName java.lang.String 在对象中,待搜索字段的字段名称
fieldClass java.lang.Class<?> 待搜索字段的字段类型
keyword java.lang.String 搜索关键字
userContext grails.plugin.springsecurity.userdetails.GrailsUser 当前操作的用户信息
application grails.core.GrailsApplication 当前的 grails 应用上下文
log Closure<?> 用于打印执行日志的 log 闭包

# 返回结果

该客制化代码需要返回的结果为一个 Map 数据类型,该 Map 包含一个 key 为 result, value 为 domain 对象列表的元素,具体返回数据示例如下:

return [
  // 包含且仅包含一个 key 为 result 的数组
  // Contains and only contains an array with a key of result
  'result': [// result 中是一个数组
    // 数组中每个元素都是一个 domain 对象
    // Each element in the array is a domain object
  ]
]
1
2
3
4
5
6
7

# Action 及 Wizard 字段客制化

Action 和 Wizard 中所使用的动态字段也可支持 DynamicFieldHook 客制化,在创建供 Action 和 Wizard 使用的字段客制化 DynamicFieldHook 对象时,需要注意如下几点:

  1. triggerField 应该设置为 df_<DynamicFieldDefinition 字段 name 属性>
  2. logicSource 应该设置为 DYNAMIC_FIELD
  3. objectType 应设置为空,因为该客制化逻辑不是针对某个对象类型的,而是针对某个 动态字段的。如果通过导入 CSV 方式创建 DynamicFieldHook 对象,可以将 objectType.shortName 列设置为空。

如下是一个实际项目中的例子,在 用户 这个字段上,定义了名为

// DynamicFieldDefinition.csv
// 定义一个动态字段,其 name 为 "用户"
name(*),fieldType,optionsJson,label,referenceClazz.fullName
用户,OBJECT,,用户,tech.muyan.security.User

// DynamicFieldInstance.csv
// 定义一个动态字段实例,关联到上述 name 为 "用户" 的动态字段定义,
// 其 type 为 "Action parameter field", 关联到 name 为 "BatchAddUserToGroupInUserGroupList" 的 Action
// type,label,displayComponentType,dynamicField.name(*),objectType.shortName(*),action.name,wizardStep.name,editable,display,required,displaySequence
Action parameter field,用户,OBJECT_MULTIPLE_SELECTION,用户,,BatchAddUserToGroupInUserGroupList,,true,true,true,10

// DynamicFieldHook.csv 
// 定义一个动态字段客制化,objectType.shortName 为空,其 triggerField 为 "df_用户", logicSource 为 "DYNAMIC_FIELD"
organization.name,objectType.shortName,coreLogic.name,hookType,triggerField,targetField,triggerDynamicField.label,targetDynamicField.label,logicSource,name(*),active,isSystem,description
$ROOT_ORG$,,User search,SEARCH,,df_用户,,,DYNAMIC_FIELD,User search in bulk add user to group action,Y,Y,"批量加用户到用户组 Action 中的用户搜索"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意

注意如果多个 Action 或 Wizard 使用了同一个动态字段定义(同一个 DynamicFieldDefinition),且同时定义了 DynamicFieldHook,因系统使用 df_<DynamicFieldDefinition Name> 来识别字段客制化,因此系统无法区分不同的 Action 或 Wizard 对应的字段的客制化。

这种情况下,需要创建多个 DynamicFieldDefinition 对象,每个 Action 或 Wizard 的 字段实例请分别基于不同的 DynamicFieldDefinition 定义进行创建。

Last Updated: 2024/10/26 09:20:23