【若依】31、流程表单
表单分类:
- 动态表单:我们在每一个流程任务中配置的表单信息,可以设置每一个字段的可读性、可写性以及是否必填等信息,动态表单一般来说是不需要完整的页面的。
- 外置表单:我们可以自定义一个 HMTL 片段或者一个 JSON 字符串,然后在我们的项目中去引用这个 HTML 片段或者 JSON 字符串。一般来说,我们在流程中用到表单,基本上都是外置表单。
- 内置表单:之前在 flowable-ui 中使用的表单,其实就是内置表单。
无论是哪种表单,只有在开始节点和任务节点是支持表单定义的,其他节点不支持。
❓表单与变量传递数据的区别❓ 变量零存零取,表单整存整取; 表单会校验而变量不会;
1 动态表单
1. 绘制流程图
在绘制流程图的过程中,可以为 start 节点或者 UserTask 节点添加动态表单属性:
下载流程图,在 UserTask 节点中,就有流程图对应的元素:
1
2
3
4
5
6
7
8
9
<userTask id="sid-B3D149FE-2230-4934-B5E4-89E53CAA940F" name="请假申请" flowable:assignee="${INITATOR}" flowable:formFieldValidation="true">
<extensionElements>
<flowable:formProperty id="days" name="请假天数" type="long" required="true"></flowable:formProperty>
<flowable:formProperty id="reason" name="请假理由" type="string" required="true"></flowable:formProperty>
<flowable:formProperty id="startTime" name="开始时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
<flowable:formProperty id="endTime" name="结束时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
此时,如果想给其他节点也添加动态表单属性,可以参考上面这个写法去完成:
1
2
3
4
5
6
7
8
9
<startEvent id="startEvent1" flowable:initiator="INITATOR" flowable:formFieldValidation="true">
<extensionElements>
<flowable:formProperty id="days" name="请假天数" type="long" required="true"></flowable:formProperty>
<flowable:formProperty id="reason" name="请假理由" type="string" required="true"></flowable:formProperty>
<flowable:formProperty id="startTime" name="开始时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
<flowable:formProperty id="endTime" name="结束时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</startEvent>
2. 查询启动节点的动态表单定义信息
当启动一个流程实例的时候,可以先去查询流程的启动节点上有哪些表单需要填写,可以查询到参数的名称、类型、是否必填等重要信息:
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
/**
* 查询启动节点的动态表单定义信息
*/
@Test
void getStartFormData() {
// 查询流程定义(DynamicFormDemo01)
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("DynamicFormDemo01").latestVersion().singleResult();
// 获取启动节点上的表单定义信息
StartFormData startFormData = formService.getStartFormData(processDefinition.getId());
// 表单的 key,这个属性实际上是给外置表单使用的
log.info(")_(" + "流程定义的 id:{},表单的 key:{}", processDefinition.getId(), startFormData.getFormKey());
// 获取 startEvent 全部的动态表单属性
List<FormProperty> formPropertyList = startFormData.getFormProperties();
for (FormProperty formProperty : formPropertyList) {
String id = formProperty.getId();
String name = formProperty.getName();
String value = formProperty.getValue();
FormType type = formProperty.getType();
// 类型的名字
String typeName = type.getName();
Object info = "";
// 对于枚举和日期类型,可以获取该字段的额外信息
if (type instanceof EnumFormType) {
// 如果当前字段是一个枚举类型,获取枚举类型的值
info = type.getInformation("values");
} else if (type instanceof DateFormType) {
// 如果当前字段是一个时间类型,获取时间格式
info = type.getInformation("datePattern");
}
log.info(")_(" + "id:{},name:{},value:{},type:{},info:{}", id, name, value, typeName, info);
}
}
当查询到这些信息之后,就可以去填写一个表单,进而去启动流程实例。
3. 启动带表单的流程实例
在启动的时候,完全可以把表单实例当成是普通变量来对待,直接启动即可:
1
2
3
4
5
6
7
8
9
10
11
@Test
void startProcessInstanceByKey() {
Authentication.setAuthenticatedUserId("yueyazhui");
Map<String, Object> vars = new HashMap<>();
vars.put("days", 3);
vars.put("reason", "想去西安玩");
vars.put("startTime", "2023-01-24");
vars.put("endTime", "2023-01-26");
vars.put("type", "timeOff");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("DynamicFormDemo01", vars);
}
也可以利用 FormService 来启动流程实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void submitStartFormData() {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("DynamicFormDemo01").latestVersion().singleResult();
Authentication.setAuthenticatedUserId("yueya");
Map<String, String> vars = new HashMap<>();
vars.put("days", "7");
vars.put("reason", "休息一周");
vars.put("startTime", "2023-02-21");
vars.put("endTime", "2023-02-27");
vars.put("type", "askForLeave");
// 用这种方式提交,会检查各个表单属性是否正确
ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), vars);
}
注:通过 FormService 启动的时候,会去检查动态表单中的各种约束条件是否都满足,如果不满足,表单启动会失败。
4. 查询任务上的表单
前面的案例是 startEvent 上的表单,接下来看任务上的表单,也就是 UserTask 上的表单。
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
/**
* 查询某一个任务节点上的表单信息,每一个任务节点表单上的数据,都是通过前面的节点传递过来的
* 例:startEvent 的表单上有 days、reason、startTime、endTime 四个字段,但是当前的 UserTask 的表单上只有 days 和 type 两个字段,那么 days 的值,就是 startEvent 当时填入的值,type 则没有值
*/
@Test
void getTaskFormData() {
Task task = taskService.createTaskQuery().singleResult();
// 查询某一个 Task 的表单数据
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
// 获取动态表单的各种属性列表并遍历
List<FormProperty> formPropertyList = taskFormData.getFormProperties();
for (FormProperty formProperty : formPropertyList) {
String id = formProperty.getId();
String name = formProperty.getName();
String value = formProperty.getValue();
FormType type = formProperty.getType();
// 类型的名字
String typeName = type.getName();
Object info = "";
// 对于枚举和日期类型,可以获取该字段的额外信息
if (type instanceof EnumFormType) {
// 如果当前字段是一个枚举类型,获取枚举类型的值
info = type.getInformation("values");
} else if (type instanceof DateFormType) {
// 如果当前字段是一个时间类型,获取时间格式
info = type.getInformation("datePattern");
}
log.info(")_(" + "id:{},name:{},value:{},type:{},info:{}", id, name, value, typeName, info);
}
}
5. 保存和完成
保存只是保存表单;完成就是完成当前的 UserTask。
5.1 保存表单数据
1
2
3
4
5
6
7
8
9
10
/**
* 修改某一个 UserTask 中的表单数据
*/
@Test
void saveFormData() {
Task task = taskService.createTaskQuery().singleResult();
Map<String, String> vars = new HashMap<>();
vars.put("days", "10");
formService.saveFormData(task.getId(), vars);
}
5.2 完成一个表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* taskService.complete();
* 此方法也可以用来完成表单数据,但在表单完成的时候,不会进行表单数据的校验
*/
@Test
void submitTaskFormData() {
Task task = taskService.createTaskQuery().singleResult();
Map<String, String> vars = new HashMap<>();
vars.put("days", "10");
vars.put("reason", "休息10天");
vars.put("startTime", "2023-01-21");
vars.put("endTime", "2023-01-30");
vars.put("type", "askForLeave");
// 这种完成方式,对于传递的表单数据会进行校验,但是日期的格式不会校验
formService.submitTaskFormData(task.getId(), vars);
}
2 外置表单
ExternalFormDemo01.bpmn20.xml 外置表单就是提前准备好的 HTML 文件。
1. 准备流程图
在用户启动流程的时候,需要提交请假的表单数据,然后在组长审批和经理审批的节点上,可以查看提交的表单数据。
表单文件:
askForLeave.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form action="">
<table>
<tr>
<td>请假天数:</td>
<td><input type="text" name="days"></td>
</tr>
<tr>
<td>请假理由:</td>
<td><input type="text" name="reason"></td>
</tr>
<tr>
<td>起始时间:</td>
<td><input type="date" name="startTime"></td>
</tr>
<tr>
<td>结束时间:</td>
<td><input type="date" name="endTime"></td>
</tr>
</table>
</form>
leaderApproval.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form action="">
<table>
<tr>
<td>请假天数:</td>
<td><input type="text" name="days" value="${days}"></td>
</tr>
<tr>
<td>请假理由:</td>
<td><input type="text" name="reason" value="${reason}"></td>
</tr>
<tr>
<td>起始时间:</td>
<td><input type="date" name="startTime" value="${startTime}"></td>
</tr>
<tr>
<td>结束时间:</td>
<td><input type="date" name="endTime" value="${endTime}"></td>
</tr>
</table>
</form>
2. 部署流程
提供流程部署的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 部署带表单的流程
* HTML 表单,部署的时候需要跟流程一起部署,需要跟流程具备相同的部署 ID,否则,在流程运行的时候,查询不到这个流程所使用的外置表单
* 部署流程时,同时上传该流程的流程部署文件和该流程所使用的外置表单
*
* @param files
* @return
*/
@PostMapping("/deployWithForm")
public Response deployWithForm(MultipartFile[] files) throws IOException {
DeploymentBuilder deploymentBuilder = repositoryService
// 开始流程部署的构建
.createDeployment()
.name("部署工作流的名称")
.category("部署工作流的分类")
.key("部署工作流的 KEY");
for (MultipartFile file : files) {
deploymentBuilder.addInputStream(file.getOriginalFilename(), file.getInputStream());
}
// 完成项目的部署
Deployment deployment = deploymentBuilder.deploy();
return Response.success("部署成功", deployment.getId());
}
部署过程:
部署成功之后,DEPLOYMENT_ID 的值是一模一样的。
3. 查看流程启动节点上的表单
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 查询启动节点上定义的外置表单(属性、内容)
*/
@Test
void getStartFormKeyAndRenderedStartForm() {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("ExternalFormDemo01").latestVersion().singleResult();
// 查询启动节点上,外置表单的 key
String startFormKey = formService.getStartFormKey(processDefinition.getId());
log.info(")_(" + "startFormKey:{}", startFormKey);
// 查询启动节点上,渲染之后的流程表单(此方法只针对外置表单,动态表单不支持此方法)
String renderedStartForm = (String) formService.getRenderedStartForm(processDefinition.getId());
log.info(")_(" + "renderedStartForm:{}", renderedStartForm);
}
如果是一个 Web 工程,那么就可以通过 Ajax 请求去获取这个结果集,这个结果集就是一个已经渲染过的 HTML,拿到这个 HTML 之后,就可以将之放到一个 div 中,然后在前端页面中渲染出来。
查询外置表单的 SQL:
1
select * from ACT_GE_BYTEARRAY where DEPLOYMENT_ID_ = ? AND NAME_ = ?
可以看到,查询外置表单需要根据部署的 ID 去查询,而获取流程的部署 ID 是比较容易的,因此建议将流程和表单一起部署,这样两者就具备相同的部署 ID。
4. 启动流程
启动流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 启动带外置表单的流程
*/
@Test
void submitStartFormDataWithExternalForm() {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("ExternalFormDemo01").latestVersion().singleResult();
Map<String, String> vars = new HashMap<>();
vars.put("days", "3");
vars.put("reason", "去西安看看大唐不夜城");
vars.put("startTime", "2023-01-24");
vars.put("endTime", "2023-01-26");
ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), vars);
}
在 UserTask 处理之前,先查看一下这个 UserTask 所对应的流程信息:
1
2
3
4
5
6
7
8
9
10
/**
* 在组长审批之前,查看一下这个 UserTask 外置表单的数据
*/
@Test
void getRenderedTaskForm() {
Task task = taskService.createTaskQuery().singleResult();
// 获取渲染之后的外置表单,这里会自动的读取流程变量,并将表单中的值给渲染出来
String renderedTaskForm = (String) formService.getRenderedTaskForm(task.getId());
log.info(")_(" + "renderedTaskForm:{}", renderedTaskForm);
}
流程审批:
1
2
3
4
5
6
7
8
9
10
/**
* 流程审批
*/
@Test
void test10() {
Task task = taskService.createTaskQuery().singleResult();
Map<String, String> vars = new HashMap<>();
vars.put("days", "100");
formService.submitTaskFormData(task.getId(),vars);
}
5. JSON 配置外置表单
JsonFormatExternalFormDemo01.bpmn20.xml 可以利用 Spring Boot 的自动部署来配置 JSON 外置表单。 在 application.properties 中配置外置表单的位置和后缀:
1
2
3
4
5
# JSON 外置表单的默认位置
flowable.form.resource-location=classpath*:/forms/
# JSON 外置表单的默认后缀
# .form 也是 Java 中 Swing 配置的默认后缀,所以在 IDEA 中,.form 后缀的文件,会被当成 Swing 的配置打开,此时,就需要先在一个外部编辑器里边编辑好这个 .form 文件
flowable.form.resource-suffixes=**.form
提供一个 JSON 表单:
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
{
"key":"application_form.form",
"name":"请假审批表单",
"fields":[
{
"id":"days",
"name":"请假天数",
"type":"string",
"required":true,
"placeholder":"empty"
},{
"id":"reason",
"name":"请假理由",
"type":"string",
"required":true,
"placeholder":"empty"
},{
"id":"startTime",
"name":"开始时间",
"type":"date",
"required":true,
"placeholder":"empty"
},{
"id":"endTime",
"name":"结束时间",
"type":"date",
"required":true,
"placeholder":"empty"
}
]
}
在流程中,使用 JSON 表单:
将流程图下载下来之后,放到 classpath:/processes/ 目录下,让流程图自动部署。
执行任务之前,查看任务需要填写的表单信息:
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
/**
* 查看 UserTask 需要填写的表单内容
*/
@Test
void getTaskFormModel() {
Task task = taskService.createTaskQuery().singleResult();
// 获取 UserTask 上的表单信息
FormInfo formInfo = taskService.getTaskFormModel(task.getId());
String formInfoKey = formInfo.getKey();
String formInfoId = formInfo.getId();
String formInfoName = formInfo.getName();
String formInfoDescription = formInfo.getDescription();
int formInfoVersion = formInfo.getVersion();
log.info(")_(" + "key:{},id:{},name:{},description:{},version:{}", formInfoKey, formInfoId, formInfoName, formInfoDescription, formInfoVersion);
SimpleFormModel simpleFormModel = (SimpleFormModel) formInfo.getFormModel();
// 获取表单中的各个字段
List<FormField> formFieldList = simpleFormModel.getFields();
for (FormField formField : formFieldList) {
String formFieldId = formField.getId();
String formFieldType = formField.getType();
Object formFieldValue = formField.getValue();
String formFieldName = formField.getName();
log.info(")_(" + "表单上的字段:id:{},type:{},value:{},name:{}", formFieldId, formFieldType, formFieldValue, formFieldName);
}
}
任务提交之前,这里能够看到的表单字段的 value 都是 null,任务提交之后,表单字段的 value 就是提交具体的值了。