# 高级表单

高级表单,如甘特图、流程图、泳道图等,通常用于领域模型对象的 CRUD 之外的复杂业务 场景。

# 目标读者

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

# 概述

本系统的高级表单文档,与 表单定制 文档中提及的 CRUD 基本表单不同, 本文档主要介绍如何使用系统提供的高级表单功能。

# 甘特图

甘特图是一种图表工具,以条形图形式展示任务的时间安排、进度和依赖关系,用于可视化 管理和规划项目、排产、排课等任务。

甘特图表单需要用户提供两种类型的领域模型定义,分别为 Row 和 Task,表示甘特图的行 和任务,一行可以有 0 或者多个任务。行与行之间可以是平级或者是基于树形的上下级关 系。

典型显示效果图如下:

Gantt Form 效果图

# 甘特图视图配置

甘特图支持通过在 gantt form 中的 extInfo 字段配置视图信息。视图信息的配置格式如下:

{
  /** 甘特图相关配置 */
  /** Gantt chart related configuration */
  "gantt"?: {
    /** 默认显示的甘特图中的开始时间, ISO8601 和 GB/T 7408-2005格式,例如 2023-05-22T00:00:00+08:00 */
    /** Default start time displayed in the Gantt chart, ISO8601 and GB/T 7408-2005 format, e.g., 2023-05-22T00:00:00+08:00 */
    "viewDateStart"?: string;
    /** 默认显示的甘特图的时间长度, ISO8601 和 GB/T 7408-2005 格式,例如 P5D */
    /** Default duration displayed in the Gantt chart, ISO8601 and GB/T 7408-2005 format, e.g., P5D */
    "viewDuration"?: string;
    /** 左侧行列表展示字段,默认显示 gantt 表单关联领域模型的所有字段 */
    /** Fields displayed in the left row list, by default shows all fields of the domain model associated with the Gantt form */
    "rowListDisplayColumns"?: [{
      // 字段名
      // Field name
      "key": string,
      // 显示名
      // Display name
      "title": string,
    }];
    /** 任务展示字段,默认显示被 hover 的 task 领域模型的所有字段 */
    /** Task display fields, by default shows all fields of the hovered task domain model */
    "tooltipDisplayColumns"?: [{
      // 字段名
      // Field name
      "key": string,
      // 显示名
      // Display name
      "title": string,
    }];
    /** 任务组字段 */
    /** Task group field */
    "taskGroupColumnKey"?: string;
    /** 任务是否可以更新, Since version 0.29 */
    /** Whether tasks can be updated, Since version 0.29 */
    "taskUpdatable"?: boolean;
  };
}
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
34
35
36
37

# 甘特图示例

这里以平台自带的例子为例简单说明

提示

如果您的商务许可包含对平台代码库的直接访问,可以通过 grails-plugin:samples (opens new window) 获取该示例的完整代码。

如果您的商务需求不包含对平台代码库的访问,可以联系客服或技术人员获取该示例的完整 代码。

我们以一个树形结构的项目管理为例,项目(SampleProject)可以有多个里程碑(SampleMilestone),里程碑可以有多个子里程碑,子里程碑可以有多个任务(SampleTask),任务可以有多个任务依赖。 由于甘特图的各行之间可能是上下级关系,并且每行的类型可能都不一样,因此需要开发人员在 domain 的声明中告知甘特图表单如何前端渲染所需的各个字段。

# SampleProject

通过声明 ganttEnableCombinedTasks, ganttRowColumn 字段来告知甘特图表单如何找到 “是否需要计算子任务包络” 字段和 “子行” 字段。

需要注意的是这里声明的子行字段 subMilestones 并不是该对象的一个字段,它是通过 API render 动态生成的。

class SampleProject {

  String name
  String label
  String description

  static constraints = {
    name nullable: false, blank: false
    label nullable: true
    description nullable: true, blank: true
  }

  static mapping = {
    tablePerHierarchy true
  }

  static hasMany = [
    milestones: SampleMilestone
  ]

  static mappedBy = [
    milestones: 'project'
  ]

  static inlineSearchColumns = ['label']

  static ganttCombinedTaskLabel = 'label'

  // this field will be generated by render api
  static ganttRowColumn = 'subMilestones'

  // this field will be generated by render api
  static ganttEnableCombinedTasks = 'enableCombineSubRowTasks'

  static loadAfter = []

}
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
34
35
36
37
  • API Render
import grails.core.GrailsApplication
import org.grails.datastore.gorm.GormEntity
import tech.muyan.domain.DomainDataService
import tech.muyan.enums.FetchType
import tech.muyan.sample.gantt.SampleMilestone
import tech.muyan.sample.gantt.SampleProject

GrailsApplication grailsApplication = application as GrailsApplication
DomainDataService domainDataService = grailsApplication.mainContext.getBean(DomainDataService)

// object 可能是一个包含某一 domain 实例的所有动态和静态字段的 map 或者 domain 实例本身,取决于该 domain 是否包含动态字段。
// 如果 object 是 domain entity,这里提供一个方法将 domain 实例转换为 map。
def res = object instanceof GormEntity ? domainDataService.convertDomainObject2Map(object, FetchType.EXCLUDE_ARRAY_COLUMNS) : object
def sampleProject = object as SampleProject

// 通过给 'enableCombineSubRowTasks' 字段设置为 false 让甘特图感知 project 关闭子任务包络计算功能
res['enableCombineSubRowTasks'] = false

// 由于所有的 SampleMilestone 都属于同一个 project,同时 milestone 之间互相又有上下级关系。
// 如果我们希望将所有的 milestone 以树形结构展示,那么我们需要将 project 下所有非根节点的 milestone 过滤掉,
// 否则渲染时会同时出现 project 的子节点和 milestone 子节点重复的情况。
Collection<SampleMilestone> subMilestones = sampleProject.milestones.findAll {
  it.parentMilestone == null
}

// 我们在 SampleProject 的 ganttRowColumn 中定义了一个 subMilestones 字段,这个字段的值会被甘特图用于渲染 project 的子节点,
// 因此我们只需要将过滤后的 milestone 赋值给 subMilestones 字段即可。
res['subMilestones'] = subMilestones

return [
  result: res
]
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

# SampleMilestone

通过声明 ganttRowColumn, ganttTasksColumn 字段来告知甘特图表单如何找到 “子行” 字段和 “任务” 的字段。

class SampleMilestone {

  String name
  String label
  String description
  SampleProject project
  SampleMilestone parentMilestone

  static constraints = {
    name nullable: false, blank: false
    label nullable: true
    description nullable: true, blank: true
    subMilestones nullable: true
    tasks nullable: true
    parentMilestone nullable: true
    project nullable: false
  }

  static mapping = {
    tablePerHierarchy true
  }

  static hasMany = [
    subMilestones: SampleMilestone,
    tasks: SampleTask,
  ]

  static mappedBy = [
    subMilestones: 'parentMilestone',
    tasks: 'milestone',
  ]

  static inlineSearchColumns = ['label']

  static ganttRowColumn = 'subMilestones'

  static ganttTasksColumn = 'tasks'

  static loadAfter = [SampleProject]

}
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
34
35
36
37
38
39
40
41

# SampleTask

通过声明 dependsOnTasks 字段来告知甘特图表单如何找到 “依赖的任务” 的字段。

class SampleTask {

  String name

  String label

  ZonedDateTime taskStartDate

  ZonedDateTime taskDueDate

  Integer progress

  GanttTaskType taskType

  String groupName

  Integer displaySequence

  List<SampleTask> dependsOnTasks

  SampleMilestone milestone

  static constraints = {
    name nullable: false, blank: false
    label nullable: false
    taskStartDate nullable: false
    taskDueDate nullable: false
    progress nullable: true
    taskType nullable: false
    dependsOnTasks nullable: true
    groupName nullable: true
    displaySequence nullable: true
    milestone nullable: true
  }

  static mapping = {
    progress defaultValue: '0'
    taskType enumType: 'string'
    displaySequence defaultValue: '0'
    tablePerHierarchy true
  }

  static inlineSearchColumns = ['label']

  static ganttTaskStartColumn = 'taskStartDate'

  static ganttTaskGroupColumn = 'groupName'

  static ganttTaskEndColumn = 'taskDueDate'

}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 甘特图 domain 字段映射关系

下面给出了配置映射关系的静态字段名,类型以及默认值

字段 描述 默认字段名 字段类型
ganttRowColumn 甘特图行中子行列表所在的字段名 rows Object
ganttTasksColumn 甘特图行所包含的任务列表字段名 entities List<Object>
ganttEnableCombinedTasks 甘特图行是否根据子行的所有任务自动计算包络任务的字段名,默认会开启计算 enableCombinedTasks Boolean
ganttCombinedTaskLabel 甘特图行包络任务的名称字段名 label String
ganttTaskLabelColumn 甘特图任务显示名的字段名 label String
ganttTaskTypeColumn 甘特图任务类型的字段名 taskType tech.muyan.enums.GanttTaskType
ganttTaskProgressColumn 甘特图任务进度的字段名 progress Integer
ganttTaskGroupNameColumn 甘特图任务所属组名的字段名,相同的组名在前端会渲染为统一颜色 groupName String
ganttTaskDependentColumn 甘特图任务依赖的任务的字段名,被依赖的任务会渲染一条箭头指向依赖的任务 dependsOnTasks List<Object>
ganttTaskStartColumn 甘特图任务时间开始的字段名 scheduleStart ZonedDateTime
ganttTaskEndColumn 甘特图任务时间结束的字段名 scheduleEnd ZonedDateTime

提示

如果通过指定字段的方式让表单感知各个字段,那么这些字段的渲染可以是松散的。也就是说这些字段可以在对象客制化的 render logic 中动态生成。

需要注意的是如果涉及某些可能会更新的字段例如任务开始结束时间时不要使用这样的方式,否则会导致在甘特图中拖拽任务更新时间功能失效。

注意

如果要通过对象客制化的 render logic 中动态生成 ganttRowColumn 或者 ganttTasksColumn 所指定的字段,请确保在 render logic 中返回这些字段的值必须是一个 GormEntity 或者是包含 @CLASS@ 字段的 Map<String, Object> 集合。

因为甘特图渲染需要获取子节点的对象类型,如果对象是一个 GormEntity 则会直接获取对象的 class,否则会尝试读取 @CLASS@ 字段作为其类型。 domainDataService.convertDomainObject2Map() => Map<String, Object> 方法会在返回的 Map 中放入 @CLASS@ 字段。 所以如果有需要推荐使用 domainDataService.convertDomainObject2Map() 方法将 domain 实例转换为 map 后再放入自定义的字段。

下面是一个例子:

res['subMilestones'] = subMilestones.collect {
  def map = domainDataService.convertDomainObject2Map(it, FetchType.INCLUDE_ARRAY_COLUMNS_WITH_LABEL)
  map['customField'] = 'customValue'
}
1
2
3
4

# 表单配置

  1. 首先需要创建一个表单,表单的类型为 Gantt,然后绑定一个 domain,注意该 domain 必须是一个行领域模型。
  2. 创建菜单,将表单绑定到菜单上。
  3. 进入新创建的菜单后通过标题栏的创建按钮创建甘特图的行,创建行时可以选择是否创建任务,如果选择创建任务则会在创建行的同时创建一个任务。

上述示例中的相关表单及菜单配置如下:

# 表单配置

organization.name(*),name(*),label,description,objectType.shortName(*),type(*),menu.label,enableRoles,extInfo
$ROOT_ORG$,List of SampleProjects,,,SampleProject,LIST,SampleProject,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Create SampleProject,,,SampleProject,CREATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Update SampleProject,,,SampleProject,UPDATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,List of SampleTasks,,,SampleTask,LIST,SampleTask,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Create SampleTask,,,SampleTask,CREATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Update SampleTask,,,SampleTask,UPDATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,List of SampleMilestones,,,SampleMilestone,LIST,SampleMilestone,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Create SampleMilestone,,,SampleMilestone,CREATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Update SampleMilestone,,,SampleMilestone,UPDATE,NULL,"ROLE_ADMIN,ROLE_DEVELOPER",
$ROOT_ORG$,Project Gantt,,,SampleProject,GANTT,SampleProjectGantt,"ROLE_ADMIN,ROLE_DEVELOPER","{
    ""gantt"": {
        ""viewDateStart"": ""2023-05-22T00:00:00+08:00"",
        ""viewDuration"": ""P1D"",
        ""rowListDisplayColumns"": [{
            ""key"": ""label"",
            ""title"": ""label""
        }, {
            ""key"": ""description"",
            ""title"": ""description""
        }],
        ""tooltipDisplayColumns"": [{
            ""key"": ""label"",
            ""title"": ""label""
        }, {
            ""key"": ""taskStartDate"",
            ""title"": ""taskStartDate""
        }, {
            ""key"": ""taskDueDate"",
            ""title"": ""taskDueDate""
        }, {
            ""key"": ""groupName"",
            ""title"": ""groupName""
        }],
        ""taskGroupColumnKey"": ""groupName""
    }
}"
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
34
35
36
37

# 菜单配置

organization.name(*),parent.label,label(*),icon,link,type,displaySequence,enableRoles
$ROOT_ORG$,NULL,Sample,FileTextOutlined,,MENU_GROUP,1001,"ROLE_ADMIN,ROLE_DEVELOPER"
$ROOT_ORG$,Sample,SampleProject,ProjectOutlined,,FORM,10,"ROLE_ADMIN,ROLE_DEVELOPER"
$ROOT_ORG$,Sample,SampleTask,TaskOutlined,,FORM,20,"ROLE_ADMIN,ROLE_DEVELOPER"
$ROOT_ORG$,Sample,SampleMilestone,MilestoneOutlined,,FORM,30,"ROLE_ADMIN,ROLE_DEVELOPER"
$ROOT_ORG$,Sample,SampleProjectGantt,CalendarOutlined,,FORM,40,"ROLE_ADMIN,ROLE_DEVELOPER"
1
2
3
4
5
6

# 表单字段配置

form.name(*),fieldName(*),displaySequence,label,helpText,fieldType,nullable,group.name,extInfo
;; Create form
Create SampleProject,label,10,Label,Project Label,STATIC_FIELD,true,,
Create SampleProject,description,20,Description,Project Description,STATIC_FIELD,true,,
Create SampleProject,milestones,30,Milestones,Project Milestones,STATIC_FIELD,true,,

;; Update form
Update SampleProject,id,0,ID,Project ID,STATIC_FIELD,false,,
Update SampleProject,label,10,Label,Project Label,STATIC_FIELD,true,,
Update SampleProject,description,20,Description,Project Description,STATIC_FIELD,true,,
Update SampleProject,milestones,30,Milestones,Project Milestones,STATIC_FIELD,true,,

;; List form
List of SampleProjects,id,0,ID,Project ID,STATIC_FIELD,false,,
List of SampleProjects,label,10,Label,Project Label,STATIC_FIELD,true,,
List of SampleProjects,description,20,Description,Project Description,STATIC_FIELD,true,,
List of SampleProjects,milestones,30,Milestones,Project Milestones,STATIC_FIELD,true,,

;; Create form
Create SampleMilestone,label,10,Label,Milestone Label,STATIC_FIELD,true,,
Create SampleMilestone,project,15,Project,Project,STATIC_FIELD,true,,
Create SampleMilestone,description,20,Description,Milestone Description,STATIC_FIELD,true,,
Create SampleMilestone,subMilestones,30,SubMilestones,Milestone SubMilestones,STATIC_FIELD,true,,
Create SampleMilestone,tasks,40,Tasks,Milestone Tasks,STATIC_FIELD,true,,

;; Update form
Update SampleMilestone,id,0,ID,Milestone ID,STATIC_FIELD,false,,
Update SampleMilestone,label,10,Label,Milestone Label,STATIC_FIELD,true,,
Update SampleMilestone,project,15,Project,Project,STATIC_FIELD,true,,
Update SampleMilestone,description,20,Description,Milestone Description,STATIC_FIELD,true,,
Update SampleMilestone,subMilestones,30,SubMilestones,Milestone SubMilestones,STATIC_FIELD,true,,
Update SampleMilestone,tasks,40,Tasks,Milestone Tasks,STATIC_FIELD,true,,

;; List form
List of SampleMilestones,id,0,ID,Milestone ID,STATIC_FIELD,false,,
List of SampleMilestones,label,10,Label,Milestone Label,STATIC_FIELD,true,,
List of SampleMilestones,project,15,Project,Project,STATIC_FIELD,true,,
List of SampleMilestones,description,20,Description,Milestone Description,STATIC_FIELD,true,,
List of SampleMilestones,subMilestones,30,SubMilestones,Milestone SubMilestones,STATIC_FIELD,true,,
List of SampleMilestones,tasks,40,Tasks,Milestone Tasks,STATIC_FIELD,true,,

;; Create form
Create SampleTask,label,10,Label,Task Label,STATIC_FIELD,false,,
Create SampleTask,milestone,15,Milestone,Milestone,STATIC_FIELD,true,,
Create SampleTask,taskStartDate,20,Task Start Date,Task Start Date,STATIC_FIELD,false,,
Create SampleTask,taskDueDate,30,Task Due Date,Task Due Date,STATIC_FIELD,false,,
Create SampleTask,taskType,40,Task Type,Task Type,STATIC_FIELD,false,,
Create SampleTask,dependsOnTasks,50,Depends On Tasks,Task Dependencies,STATIC_FIELD,true,,

;; Update form
Update SampleTask,id,0,ID,Task ID,STATIC_FIELD,false,,
Update SampleTask,label,10,Label,Task Label,STATIC_FIELD,false,,
Update SampleTask,milestone,15,Milestone,Milestone,STATIC_FIELD,true,,
Update SampleTask,taskStartDate,20,Task Start Date,Task Start Date,STATIC_FIELD,false,,
Update SampleTask,taskDueDate,30,Task Due Date,Task Due Date,STATIC_FIELD,false,,
Update SampleTask,taskType,40,Task Type,Task Type,STATIC_FIELD,false,,
Update SampleTask,dependsOnTasks,50,Depends On Tasks,Task Dependencies,STATIC_FIELD,true,,

;; List form
List of SampleTasks,id,0,ID,Task ID,STATIC_FIELD,false,,
List of SampleTasks,label,10,Label,Task Label,STATIC_FIELD,false,,
List of SampleTasks,milestone,15,Milestone,Milestone,STATIC_FIELD,true,,
List of SampleTasks,taskStartDate,20,Task Start Date,Task Start Date,STATIC_FIELD,false,,
List of SampleTasks,taskDueDate,30,Task Due Date,Task Due Date,STATIC_FIELD,false,,
List of SampleTasks,taskType,40,Task Type,Task Type,STATIC_FIELD,false,,
List of SampleTasks,dependsOnTasks,50,Depends On Tasks,Task Dependencies,DYNAMIC_FIELD,true,,
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# DynamicLogic 定义

name(*),logicType,code(F),description,isSystem,DELETE_FLAG

Project render core logic,OBJECT_DYNAMIC_HOOK,"groovy/gantt/projectApiRender.groovy",示例项目的 API 返回值客制化,Y,N
1
2
3

# DynamicObjectHook 定义

organization.name,objectType.shortName,name(*),hookType,coreLogic.name,active,isSystem,description
$ROOT_ORG$,SampleProject,Project API render,RENDER,Project render core logic,Y,N,SampleProject 的客制化API返回逻辑
1
2
Last Updated: 2024/9/29 02:33:14