Flowable31动态表单-----------------------终章

首先我们需要明白的是动态表单主要分为两类:内置、外置。在这分别介绍一下内置外置表单。

一、内置表单中的动态表单 (Flowable Forms)

Flowable 自带的表单引擎(Flowable Forms)支持一定程度的动态能力。因为是内置方案,它的实现相对简单,但不那么灵活。
实现机制
内置表单的动态性主要通过在表单的 JSON 定义中使用 表达式(Expressions) 来实现。你可以在表单设计器中为组件的 visibility (可见性) 或 disabled (禁用) 等属性设置条件表达式。
语法: 表达式通常使用 {{…}} 包裹。
原理: Flowable Form 的前端渲染引擎会实时监听表单中各个字段值的变化。当某个值改变时,它会重新计算所有依赖该值的表达式,并根据计算结果(true 或 false)来更新对应组件的显示状态。
举例:请假类型联动
场景:创建一个请假表单,当“请假类型”选择“其他”时,才显示“具体事由”这个文本框。
创建表单字段:
一个下拉框,ID 为 type,选项包含“年假”、“病假”、“其他”。
一个文本输入框,ID 为 otherReason。
但是一般很少用内置的表单!!!!所以在这不详细讲了。

二、外置表单中的动态表单 (External Forms)

二、外置表单中的动态表单 (External Forms)
这是实现动态表单最主流、最强大、最灵活的方式。
实现机制
在外置表单模式下,Flowable 引擎完全不关心表单长什么样、有什么动态效果。所有的动态逻辑都由你的前端应用(Vue/React/Angular 等)全权负责。
你拥有前端框架提供的所有“武器”来实现任何你能想到的动态效果:
条件渲染: Vue 的 v-if/v-show,React 的 {condition && }。
数据绑定: Vue 的 v-model,React 的 state + onChange。
侦听器 (Watchers): 监听某个数据的变化,然后执行复杂的逻辑,比如去后端请求新的数据来填充另一个下拉框。
计算属性 (Computed Properties): 根据一个或多个数据派生出新的状态。
事件处理: 响应用户的各种点击、输入事件。
在这我们同个一个举例来详细了解一下外置表单。

场景设定

流程:员工提交请假申请 -> 直属领导审批 -> 流程结束。
角色:员工(employee),领导(manager)。
技术栈:
后端:Spring Boot
流程引擎:Flowable
前端:Vue + Vue Router + Axios

第 1 步:设计 BPMN 流程并定义 formKey
这是所有工作的基础。我们创建一个名为 leave-process.bpmn20.xml 的文件。
核心节点与 formKey 设置:
申请请假 (User Task):
ID: applyTask
负责人: 由流程发起人自己处理,所以用 ${initiator}
formKey: leave-apply-form (这是我们给前端Vue组件起的名字)
领导审批 (User Task):
ID: approvalTask
负责人: manager (为简化,我们写死;实际项目中会是动态的)
formKey: leave-approval-form (这是审批页面的Vue组件名)
BPMN 核心 XML 代码:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/2.0/MODEL"
             xmlns:flowable="http://flowable.org/bpmn"
             targetNamespace="http://flowable.org/examples">

    <process id="leaveProcess" name="请假流程">

        <startEvent id="startEvent" flowable:initiator="applyUser"/>

        <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/>

        <!-- 申请节点:员工填写请假单 -->
        <userTask id="applyTask" name="填写请假单" flowable:assignee="${applyUser}">
            <extensionElements>
                <!-- 关键点1: 定义外置表单的Key -->
                <flowable:formKey>leave-apply-form</flowable:formKey>
            </extensionElements>
        </userTask>

        <sequenceFlow id="flow2" sourceRef="applyTask" targetRef="approvalTask"/>

        <!-- 审批节点:领导审批 -->
        <userTask id="approvalTask" name="领导审批" flowable:assignee="manager">
             <extensionElements>
                <!-- 关键点2: 定义审批表单的Key -->
                <flowable:formKey>leave-approval-form</flowable:formKey>
            </extensionElements>
        </userTask>

        <sequenceFlow id="flow3" sourceRef="approvalTask" targetRef="decision"/>

        <!-- 排他网关:根据审批结果决定走向 -->
        <exclusiveGateway id="decision"/>
        
        <!-- 如果同意,走向结束 -->
        <sequenceFlow id="flowApproved" sourceRef="decision" targetRef="endEvent">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[${approved == true}]]>
            </conditionExpression>
        </sequenceFlow>

        <!-- 如果拒绝,回到申请节点(重新提交) -->
        <sequenceFlow id="flowRejected" sourceRef="decision" targetRef="applyTask">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[${approved == false}]]>
            </conditionExpression>
        </sequenceFlow>

        <endEvent id="endEvent"/>

    </process>
</definitions>

注意:
我们将这个 .bpmn20.xml 文件放到 Spring Boot 项目的 src/main/resources/processes/ 目录下。
我们这里为了演示,把“发起”和“填写表单”分开了。在实际中,常常是发起流程时就直接提交了表单数据,从而直接完成第一个任务。我们这里的演示流程是:发起 -> 任务列表出现“填写请假单” -> 填写并提交。

第 2 步:后端 Spring Boot 开发
创建 REST API 接口 (LeaveController.java)
这是连接前后端的桥梁。

@RestController
@RequestMapping("/api/leave")
@RequiredArgsConstructor // 使用Lombok注入
@CrossOrigin // 允许跨域,方便本地Vue开发调试
public class LeaveController {

    private final RuntimeService runtimeService;
    private final TaskService taskService;

    /**
     * 1. 启动请假流程
     * @param userId 申请人ID
     */
    @PostMapping("/start/{userId}")
    public String startProcess(@PathVariable String userId) {
        // 流程变量,设置申请人
        Map<String, Object> variables = new HashMap<>();
        variables.put("applyUser", userId);

        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leaveProcess", variables);
        return "流程已启动,实例ID: " + processInstance.getId();
    }

    /**
     * 2. 获取用户的待办任务列表
     * @param userId 用户ID
     * @return 任务列表
     */
    @GetMapping("/tasks/{userId}")
    public List<Map<String, Object>> getTasks(@PathVariable String userId) {
        List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).orderByTaskCreateTime().desc().list();
        
        // 返回给前端一个简化的DTO列表
        return tasks.stream().map(task -> {
            Map<String, Object> map = new HashMap<>();
            map.put("taskId", task.getId());
            map.put("taskName", task.getName());
            map.put("createTime", task.getCreateTime());
            map.put("formKey", task.getFormKey()); // **核心:把formKey传给前端**
            map.put("processInstanceId", task.getProcessInstanceId());
            return map;
        }).collect(Collectors.toList());
    }

    /**
     * 3. 获取任务关联的流程变量 (用于表单数据回显)
     * @param taskId 任务ID
     * @return 流程变量
     */
    @GetMapping("/tasks/{taskId}/variables")
    public Map<String, Object> getTaskVariables(@PathVariable String taskId) {
        return taskService.getVariables(taskId);
    }
    
    /**
     * 4. 完成任务 (提交表单)
     * @param taskId 任务ID
     * @param variables 表单提交的数据,以JSON格式的Map传入
     */
    @PostMapping("/tasks/{taskId}/complete")
    public void completeTask(@PathVariable String taskId, @RequestBody Map<String, Object> variables) {
        taskService.complete(taskId, variables);
    }
}

第 3 步:前端 Vue 开发

假设你已经创建了一个 Vue 项目,并安装了 vue-router 和 axios。

  1. API 服务 (src/api/leave.js)
    将后端接口封装成函数,便于组件调用。
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'http://localhost:8080/api/leave', // 你的Spring Boot地址
  headers: {
    'Content-Type': 'application/json',
  },
});

export default {
  startProcess(userId) {
    return apiClient.post(`/start/${userId}`);
  },
  getTasks(userId) {
    return apiClient.get(`/tasks/${userId}`);
  },
  getTaskVariables(taskId) {
    return apiClient.get(`/tasks/${taskId}/variables`);
  },
  completeTask(taskId, variables) {
    return apiClient.post(`/tasks/${taskId}/complete`, variables);
  },
};
  1. Vue Router 配置 (src/router/index.js)
    这是实现 formKey 动态路由的核心。
import { createRouter, createWebHistory } from 'vue-router';
import TaskList from '../views/TaskList.vue';
import FormWrapper from '../views/FormWrapper.vue'; // 一个动态加载表单的包装器组件

const routes = [
  {
    path: '/',
    redirect: '/tasks/employee', // 默认重定向到员工的任务列表
  },
  {
    path: '/tasks/:userId', // 任务列表页,:userId是动态的
    name: 'TaskList',
    component: TaskList,
    props: true, // 将路由参数userId作为props传递给组件
  },
  {
    // **核心路由:根据formKey动态展示表单**
    path: '/form/:formKey',
    name: 'Form',
    component: FormWrapper,
    props: true,
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
  1. 视图组件
    a. TaskList.vue (任务列表)
<template>
  <div>
    <h2>{{ userId }} 的待办任务</h2>
    <button @click="startLeaveProcess" v-if="userId === 'employee'">发起请假流程</button>
    <ul>
      <li v-for="task in tasks" :key="task.taskId">
        {{ task.taskName }} - 创建时间: {{ new Date(task.createTime).toLocaleString() }}
        <button @click="handleTask(task)">去处理</button>
      </li>
    </ul>
  </div>
</template>

<script>
import api from '../api/leave';

export default {
  props: ['userId'],
  data() {
    return {
      tasks: [],
    };
  },
  methods: {
    async fetchTasks() {
      const response = await api.getTasks(this.userId);
      this.tasks = response.data;
    },
    async startLeaveProcess() {
      try {
        await api.startProcess(this.userId);
        alert('流程已启动!');
        this.fetchTasks(); // 刷新任务列表
      } catch (error) {
        console.error("启动流程失败:", error);
      }
    },
    handleTask(task) {
      // **关键跳转:使用formKey和taskId导航到表单页**
      this.$router.push({
        name: 'Form',
        params: { formKey: task.formKey },
        query: { taskId: task.taskId },
      });
    },
  },
  created() {
    this.fetchTasks();
  },
  watch: {
    // 监听userId变化,例如从 /tasks/employee 切换到 /tasks/manager
    userId() {
        this.fetchTasks();
    }
  }
};
</script>

b. FormWrapper.vue (表单包装器/分发器)
这个组件非常巧妙,它根据路由参数 formKey 来决定到底渲染哪个具体的表单组件。

<template>
  <div>
    <!-- Vue的动态组件,:is 属性绑定到要渲染的组件 -->
    <component :is="formComponent" :task-id="taskId" />
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue';

// 定义formKey到组件的映射
const formMap = {
  'leave-apply-form': defineAsyncComponent(() => import('../components/LeaveApplyForm.vue')),
  'leave-approval-form': defineAsyncComponent(() => import('../components/LeaveApprovalForm.vue')),
};

export default {
  props: ['formKey'],
  computed: {
    formComponent() {
      return formMap[this.formKey] || null; // 根据formKey返回对应的组件
    },
    taskId() {
      return this.$route.query.taskId; // 从路由的query中获取taskId
    },
  },
};
</script>

c. LeaveApplyForm.vue (请假申请表单)

<template>
  <div class="form-container">
    <h3>填写请假单</h3>
    <p>任务ID: {{ taskId }}</p>
    <div>
      <label>请假天数:</label>
      <input type="number" v-model.number="formData.days" />
    </div>
    <div>
      <label>请假事由:</label>
      <textarea v-model="formData.reason"></textarea>
    </div>
    <button @click="submitForm">提交申请</button>
  </div>
</template>

<script>
import api from '../api/leave';
export default {
  props: ['taskId'],
  data() {
    return {
      formData: {
        days: 0,
        reason: '',
      },
    };
  },
  methods: {
    async submitForm() {
      try {
        await api.completeTask(this.taskId, this.formData);
        alert('申请已提交!');
        this.$router.push('/tasks/employee'); // 提交后返回任务列表
      } catch (error) {
        console.error('提交失败:', error);
      }
    },
  },
};
</script>

d. LeaveApprovalForm.vue (领导审批表单)
这个表单需要先加载数据显示,然后再提交。

<template>
  <div class="form-container">
    <h3>审批请假单</h3>
    <div v-if="loading">正在加载数据...</div>
    <div v-else>
      <p><strong>申请人:</strong> {{ variables.applyUser }}</p>
      <p><strong>请假天数:</strong> {{ variables.days }}</p>
      <p><strong>请假事由:</strong> {{ variables.reason }}</p>
      <hr />
      <div>
        <label>审批意见:</label>
        <textarea v-model="approval.comment"></textarea>
      </div>
      <button @click="handleApproval(true)">同意</button>
      <button @click="handleApproval(false)" style="background-color: red;">拒绝</button>
    </div>
  </div>
</template>

<script>
import api from '../api/leave';
export default {
  props: ['taskId'],
  data() {
    return {
      loading: true,
      variables: {}, // 存放从后端加载的流程变量
      approval: {
        comment: '',
        approved: null, // 将在点击按钮时设置
      },
    };
  },
  methods: {
    async fetchVariables() {
      try {
        const response = await api.getTaskVariables(this.taskId);
        this.variables = response.data;
      } catch (error) {
        console.error('加载表单数据失败:', error);
      } finally {
        this.loading = false;
      }
    },
    async handleApproval(isApproved) {
      this.approval.approved = isApproved;
      try {
        await api.completeTask(this.taskId, this.approval);
        alert('审批完成!');
        this.$router.push('/tasks/manager'); // 返回领导的任务列表
      } catch (error) {
        console.error('审批失败:', error);
      }
    },
  },
  created() {
    this.fetchVariables(); // 组件创建时加载数据
  },
};
</script>

第 4 步:演示整个流程
启动 Spring Boot 后端。
启动 Vue 前端。
员工操作:
浏览器访问 http://localhost:8081/tasks/employee (假设Vue运行在8081端口)。
点击 “发起请假流程”,后端启动流程实例,任务列表刷新,出现 “填写请假单” 任务。
点击 “去处理”,Vue Router 根据 formKey (leave-apply-form) 导航到表单页,FormWrapper 加载 LeaveApplyForm.vue。
填写天数和事由,点击 “提交申请”。数据作为流程变量提交,第一个任务完成。
领导操作:
浏览器访问 http://localhost:8081/tasks/manager。
任务列表显示 “领导审批” 任务。
点击 “去处理”,Vue Router 根据 formKey (leave-approval-form) 导航,FormWrapper 加载 LeaveApprovalForm.vue。
LeaveApprovalForm 组件会调用后端API,获取并显示员工提交的请假天数和事由(数据回显)。
领导填写审批意见,点击 “同意”。approved: true 和 comment 作为流程变量提交,任务完成。
流程结束:
流程引擎根据 approved == true 的条件,流向结束节点。
如果领导点击 “拒绝”,流程会回到 “填写请假单” 节点,员工会在自己的任务列表里再次看到这个任务。

前端架构的核心思想

前端的核心任务是:根据后端传来的 formKey,动态地展示正确的表单组件,并处理好数据的加载和提交。
为了实现这个目标,我们设计了以下几个关键部分:
API 服务层 (api/leave.js): 专职与后端打交道,让组件代码更干净。
路由 (router/index.js): 充当交通警察的角色。它负责解析 URL,并将用户引导到正确的“页面”或“视图”。
任务列表视图 (views/TaskList.vue): 用户的工作台。展示所有待办事项,是所有操作的入口。
表单分发器/包装器 (views/FormWrapper.vue): 这是整个前端设计的**“枢纽”或“魔法盒”。它本身不展示任何具体表单,而是根据指令(formKey)决定调用哪个真正的表单组件。
具体表单组件 (components/Leave*.vue): 真正负责 UI 展示和用户交互的
“工人”**。

用户在 TaskList.vue 点击 "去处理"
      │
      └─> 1. handleTask(task) 被调用
            │
            └─> 2. this.$router.push({ name: 'Form', params: { formKey: '...' }, query: { taskId: '...' }})
                  │
                  └─> 3. Vue Router 匹配到 /form/:formKey 路由,浏览器 URL 变化
                        │
                        └─> 4. FormWrapper.vue 组件被渲染
                              │
                              ├─> 5. FormWrapper 从路由接收 props: { formKey: '...' }
                              │
                              └─> 6. FormWrapper 从路由 query 中获取 taskId: '...'
                                    │
                                    └─> 7. formComponent 计算属性根据 formKey 从 formMap 中找到对应的组件(如 LeaveApprovalForm)
                                          │
                                          └─> 8. <component :is="LeaveApprovalForm" :task-id="taskId"> 被渲染
                                                │
                                                └─> 9. LeaveApprovalForm.vue 组件被创建
                                                      │
                                                      ├─> 10. 它接收到 taskId 这个 prop
                                                      │
                                                      └─> 11. 在 created() 钩子中,使用 this.taskId 调用 API (getTaskVariables) 加载数据
                                                            │
                                                            └─> 12. 用户填写表单,点击提交
                                                                  │
                                                                  └─> 13. 调用 API (completeTask) 并携带 taskId 和表单数据
                                                                        │
                                                                        └─> 14. 提交成功后,路由跳转回任务列表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值