基于Flowable的流程挂接自定义业务表单的设计与实践

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS SQL Server,基础系列 2核4GB
简介: 文章讨论了如何在Flowable流程引擎中挂接自定义业务表单,以及相关设计和实践的步骤。文章中包含了一些前后端代码示例,如Vue组件的模板和脚本部分,这些代码用于实现与Flowable流程引擎交互的界面。例如,有一个按钮组件用于提交申请,点击后会触发applySubmit方法,该方法会与后端API进行交互,处理流程启动、查询关联流程等逻辑。

问题背景:

     实际企业的工作流程流转中都会涉及到各种业务,业务又是各种实际的业务表单,所以基于这种业务表单的流程流转显得特别实用,在不改变现有的业务的情况下,增加一个流程的流转支持,同时进行业务的控制与回写,这些都需要一个灵活结合业务表单的流程设计。

     本文就是为了这种需求而实现的设计与实践,供大家参考使用。


第一章:总体设计思路

   总体流程图如下:

自定义业务表单设计图.png

1.1 前端的流程功能组件

  • 提交申请按钮:主要提供在业务表单里增加流程申请的按钮,以便启动业务表单流程。
  • 撤回申请按钮:提供流程的撤回操作,以便在业务里也能进行流程的基本操作。
  • 退回申请按钮:同样也是在业务提供退回申请的操作,满足流程业务需要。
  • 历史流程信息:提供流程流转中的相关信息,包括一些审批历史记录以及流程图所处的位置等。

1.2 结合业务表单增加流程按钮或页面

主要是根据业务本身需要增加相应的流程业务操作按钮或界面:

  • 业务表单里:可以在具体的业务表单比如采购订单里,增加一个提交申请按钮,方便操作。
  • 业务列表里:也可以在业务表单的列表里增加流程业务按钮,以便可以批量操作。

1.3 现有的业务表单与流程进行关联

主要是对现有的业务表单与流程关联起来,以便后续流程与业务的结合:

  • 业务方面:主要包括业务表单名称,业务表单相关服务名称以及业务数据库表。
  • 流程方面:主要包括流程key,流程名称,路由地址与前端注入方式等。

1.4 前端根据后端关联信息动态显示

根据前面的一些关联信息进行前端表单的动态显示:

  • 根据流转中业务表单服务名称找到对应的业务表单组件
  • 根据找到的页面进行动态显示需要的表单组件

1.5 业务流程接口类,实现业务完成后的处理

业务层实现接口方法,用于流程处理后的回调,以便满足业务上的这种特殊需求:

  • 流程处理完后的回调
  • 根据业务ID返回业务表单数据
  • 返回当前节点的流程变量
  • 返回当前节点的候选人



第二章:后端主要设计

2.1 业务层实现接口方法

主要代码如下:

public interface FlowCallBackServiceI {
    /**
     * 流程处理完成后的回调
     * @param business 里面包含流程运行的现状信息,业务层可根据其信息判断,书写增强业务逻辑的代码,<br/>
     *                 1、比如将其中关键信息存入业务表,即可单表业务操作,否则需要关联flow_my_business表获取流程信息<br/>
     *                 2、比如在流程进行到某个节点时(business.taskId),需要特别进行某些业务逻辑操作等等<br/>
     */
    void afterFlowHandle(FlowMyBusiness business);


    /**
     * 根据业务id返回业务表单数据<br/>
     * @param dataId
     * @return
     */
    Object getBusinessDataById(String dataId);

    /**
     * 返回当前节点的流程变量
     * @param taskNameId 节点定义id
     * @param values 前端传入的变量,里面包含dataId
     * @return
     */
    Map<String, Object> flowValuesOfTask(String taskNameId, Map<String, Object> values);

    /**
     * 返回当前节点的候选人username
     * @param taskNameId 节点定义id
     * @param values 前端传入的变量,里面包含dataId
     * @return
     */
    List<String> flowCandidateUsernamesOfTask(String taskNameId, Map<String, Object> values);
}

2.2 实际业务类的流程实现

这里以testDemo服务为例,主要增加FlowCallBackServiceI的实现类,如:

@Service("testDemoService")
public class TestDemoServiceImpl extends ServiceImpl<TestDemoMapper, TestDemo> implements ITestDemoService, FlowCallBackServiceI {

  @Autowired
    FlowCommonService flowCommonService;
  
  @Override
  public void afterFlowHandle(FlowMyBusiness business) {
    //流程操作后做些什么
        business.getTaskNameId();//接下来审批的节点
        business.getValues();//前端传进来的参数
        business.getActStatus();//流程状态 ActStatus.java
        //....其他
    
  }

  @Override
  public Object getBusinessDataById(String dataId) {
    return this.getById(dataId);
  }

  @Override
  public Map<String, Object> flowValuesOfTask(String taskNameId, Map<String, Object> values) {
    // TODO Auto-generated method stub
    return null;
  }

  @Override
  public List<String> flowCandidateUsernamesOfTask(String taskNameId, Map<String, Object> values) {
    // TODO Auto-generated method stub
    return null;
  }
  
  @Override
    public boolean save(TestDemo testDemo) {
        /**新增数据**/
        testDemo.setId(IdUtil.fastSimpleUUID());
        //Comparison method violates its general contract!有时候出现这个错误加的,原因不明
        System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
        return super.save(testDemo);
    }
    @Override
    public boolean removeById(Serializable id) {
        /**删除数据,移除流程关联信息**/
        flowCommonService.delActBusiness(id.toString());
        return super.removeById(id);
    }

  @Override
  public IPage<TestDemo> myPage(Page<TestDemo> page, QueryWrapper<TestDemo> queryWrapper) {
    return this.baseMapper.myPage(page, queryWrapper);
  }

}


2.3 业务列表增加流程相关信息

这里主要是以列表为例增加流程相关的方式,代码如下:

public Result<IPage<TestDemo>> queryPageList(TestDemo testDemo,
                   @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
                   @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
                   HttpServletRequest req) {
    Map<String, String[]> ParameterMap = new HashMap<String, String[]>(req.getParameterMap());
    String[] column = new String[]{""};
    if(ParameterMap!=null&&  ParameterMap.containsKey("column")) {
      column[0] = ParameterMap.get("column")[0];
      column[0] = "t."+ column[0];
      ParameterMap.replace("column", column);
      log.info("修改的排序规则>>列:" + ParameterMap.get("column")[0]);      
    }
    QueryWrapper<TestDemo> queryWrapper = QueryGenerator.initQueryWrapper(testDemo, req.getParameterMap());
    Page<TestDemo> page = new Page<TestDemo>(pageNo, pageSize);
    //IPage<TestDemo> pageList = testDemoService.page(page, queryWrapper);
    IPage<TestDemo> pageList = testDemoService.myPage(page, queryWrapper);
    return Result.OK(pageList);
  }




第三章:前端主要设计

3.1 流程前端组件的设计

这里主要介绍两个,流程提交与历史信息

  • 流程提交申请组件:代码如下:
<template>
  <span>
    <a-button :type="btnType" @click="applySubmit()" :loading="submitLoading">{{text}}</a-button>
    <a-modal :z-index="100" :title="firstInitiatorTitle" @cancel="firstInitiatorOpen = false" v-model:visible="firstInitiatorOpen"
      :width="'50%'" append-to-body>
       <a-descriptions bordered layout="vertical">
         <a-descriptions-item :span="3">
               <a-badge status="processing" text="选择提醒" />
          </a-descriptions-item>
          <a-descriptions-item label="重新发起新流程按钮" labelStyle="{ color: '#fff', fontWeight: 'bold', fontSize='18px'}">
            重新发起新流程会删除之前发起的任务,重新开始.
          </a-descriptions-item>
          <a-descriptions-item label="继续发起老流程按钮">
            继续发起流程就在原来流程基础上继续流转.
          </a-descriptions-item>
       </a-descriptions>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" @click="ReStartByDataId(true)">重新发起新流程</el-button>
        <el-button type="primary" @click="ReStartByDataId(false)">继续发起老流程</el-button>
        <el-button @click="firstInitiatorOpen = false">取 消</el-button>
      </span>
    </a-modal>

    <!--挂载关联多个流程-->
    <a-modal @cancel="flowOpen = false" :title="flowTitle" v-model:visible="flowOpen" width="70%" append-to-body>
      <el-row :gutter="64">
        <el-col :span="20" :xs="64" style="width: 100%">
          <el-table ref="singleTable" :data="processList" border highlight-current-row style="width: 100%">
             <el-table-column type="selection" width="55" align="center" />
             <el-table-column label="主键" align="center" prop="id" v-if="true"/>
             <el-table-column label="业务表单名称" align="center" prop="businessName" />
             <el-table-column label="业务服务名称" align="center" prop="businessService" />
             <el-table-column label="流程名称" align="center" prop="flowName" />
             <el-table-column label="关联流程发布主键" align="center" prop="deployId" />
             <el-table-column label="前端路由地址" align="center" prop="routeName" />
             <el-table-column label="组件注入方法" align="center" prop="component" />
             <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
               <template #default="scope">
                 <el-button size="small" type="primary" @click="selectProcess(scope.row)">确定</el-button>
               </template>
             </el-table-column>
            </el-table>
        </el-col>
      </el-row>
    </a-modal>

  </span>
</template>

<script lang="ts" setup>
  import {
    startByDataId,
    isFirstInitiator,
    deleteActivityAndJoin,
    getProcesss
  } from "@/api/workflow/process";

  defineOptions({ name: 'ActApplyBtn' })
  const props = defineProps({
    btnType: {
      type: String,
      default: 'primary',
      required: false
    },
    dataId: {
      type: String,
      default: '',
      required: true
    },
    serviceName: {
      type: String,
      default: '',
      required: true
    },
    variables: {
      type: Object,
      default: {},
    },
    text: {
      type: String,
      default: '提交申请',
      required: false
    }
  })

  const emit = defineEmits([
    'success'
  ])

  const router = useRouter()
  const { proxy } = getCurrentInstance() as ComponentInternalInstance

  const modalVisible = ref(false)
  const submitLoading = ref(false)
  const form = ref<any>({})
  const firstInitiatorOpen = ref(false)
  const firstInitiatorTitle = ref('')
  // 关联流程数据
  const processList = ref<any>([])
  const flowOpen = ref(false)
  const flowTitle = ref('')
  const selectFlowId = ref('')  //选择或使用的流程ID
  const error = ref('')

  const selectProcess = (row) => {
    selectFlowId.value = row.id;
    flowOpen.value = false;
    var params = Object.assign({
      dataId: props.dataId
    }, props.variables);
    startByDataId(props.dataId, selectFlowId.value, props.serviceName, params)
      .then(res => {
        //console.log("startByDataId res",res);
        if (res.code == 200 ) {
          proxy?.$modal.msgSuccess(res.msg);
          emit('success');
        } else {
          proxy?.$modal.msgError(res.msg);
        }
      })
      .finally(() => (submitLoading.value = false));
  }
  const ReStartByDataId = (isNewFlow: boolean) => {
    if(isNewFlow) {
      submitLoading.value = true;
      deleteActivityAndJoin(props.dataId,props.variables)
      .then(res => {
        if (res.code == 200 && res.result) { //若删除成功
          var params = Object.assign({
            dataId: props.dataId
          }, props.variables);
          startByDataId(props.dataId, selectFlowId.value, props.serviceName, params)
            .then(res => {
              if (res.code == 200) {
                firstInitiatorOpen.value = false;
                proxy?.$modal.msgSuccess(res.message);
                emit('success');
              } else {
                proxy?.$modal.msgError(res.message);
              }
            })
        }
      })
      .finally(() => (submitLoading.value = false));
    }
    else {//继续原有流程流转,跳到流程处理界面上
      //console.log("props.variables",props.variables);
      router.push({
        path: '/flowable/task/record/index',
        query: {
          procInsId: props.variables.processInstanceId,
          deployId: props.variables.deployId,
          taskId: props.variables.taskId,
          businessKey: props.dataId,
          nodeType: "",
          category: "zdyyw",
          finished: true
        },
      })
    }
  }
  const applySubmit = () => {
    if (props.dataId && props.dataId.length < 1) {
      error.value = '必须传入参数dataId';
      proxy?.$modal.msgError(error.value);
      return;
    }
    if (props.serviceName && props.serviceName.length < 1) {
      error.value = '必须传入参数serviceName';
      proxy?.$modal.msgError(error.value);
      return;
    } else {
      error.value = '';
    }
    //对于自定义业务,判断是否是驳回或退回的第一个发起人节点
    submitLoading.value = true;
    isFirstInitiator(props.dataId, props.variables)
      .then(res => {
        if (res.code === 200 && res.data) { //若是,弹出窗口选择重新发起新流程还是继续老流程
          firstInitiatorTitle.value = "根据自己需要进行选择"
          firstInitiatorOpen.value = true;
        }
        else {
          submitLoading.value = true;
          const processParams = {
             serviceName: props.serviceName
          }
          getProcesss(processParams).then(res => {/**查询关联流程信息 */
            processList.value = res.data;
              submitLoading.value = false;
              if (processList.value && processList.value.length > 1) {
                flowOpen.value = true;
              }
              else if (processList.value && processList.value.length === 1) {
                selectFlowId.value = res.data[0].id;
                var params = Object.assign({
                  dataId: props.dataId
                }, props.variables);
                startByDataId(props.dataId, selectFlowId.value, props.serviceName, params)
                  .then(res => {
                    console.log("startByDataId res",res);
                    if (res.code == 200 ) {
                      proxy?.$modal.msgSuccess(res.msg);
                      emit('success');
                    } else {
                      proxy?.$modal.msgError(res.msg);
                    }
                  })
                  .finally(() => (submitLoading.value = false));
              } else {
                proxy?.$modal.msgError("检查该业务是否已经关联流程!");
              }
          })
          .finally(() => (submitLoading.value = false));
        }
      })
      .finally(() => (submitLoading.value = false));
    }
</script>


  • 历史信息组件:主要代码如下:
<style lang="less">
</style>
<template>
  <span>
      <a-button :type="btnType"  @click="history()" >{{text}}</a-button>
      <a-modal title="审批历史" v-model:visible="modalLsVisible" :mask-closable="true" :width="'70%'" :footer="null">
          <div v-if="modalLsVisible" style="max-height: 550px; overflow-y: scroll;">
              <HistoricDetail ref="historicDetail" :data-id="dataId"></HistoricDetail>
          </div>
      </a-modal>
  </span>
</template>

<script setup lang="ts">
  import HistoricDetail from './HistoricDetail';

  defineOptions({ name: 'ActHistoricDetailBtn' })
  const props = defineProps({
    btnType: {
      type: String,
      default: 'primary',
      required: false ,
    },
    dataId: {
      type: String,
      default: '',
      required: true
    },
    text: {
      type: String,
      default: '审批历史',
      required: false
    }
  })
  const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  const modalLsVisible = ref(false)
  const history = () => {
    if (!props.dataId) {
        proxy?.$modal.msgError('流程实例ID不存在');
        return;
    }
    modalLsVisible.value = true;
  }

</script>
<style lang="less">
</style>
<template>
  <div class="search">
    <el-tabs tab-position="top" v-model="activeName" :value="processed === true ? 'approval' : 'form'" @tab-click="changeTab">
      <el-tab-pane label="表单信息" name="form">
        <div v-if="customForm.visible"> <!-- 自定义表单 -->
            <component ref="refCustomForm" :disabled="customForm.disabled" :is="customForm.formComponent" :model="customForm.model"
                        :customFormData="customForm.customFormData" :isNew = "customForm.isNew"></component>
        </div>
        <div style="margin-left:10%;margin-bottom: 30px">
           <!--对上传文件进行显示处理,临时方案 add by nbacheng 2022-07-27 -->
           <el-upload action="#" :on-preview="handleFilePreview" :file-list="fileList" v-if="fileDisplay" />
        </div>
      </el-tab-pane >

      <el-tab-pane label="流转记录" name="record">
        <el-card class="box-card" shadow="never">
          <el-col :span="20" :offset="2">
            <div class="block">
              <el-timeline>
                <el-timeline-item v-for="(item,index) in historyProcNodeList" :key="index" :icon="setIcon(item.endTime)" :color="setColor(item.endTime)">
                  <p style="font-weight: 700">{{ item.activityName }}</p>
                  <el-card v-if="item.activityType === 'startEvent'" class="box-card" shadow="hover">
                    {{ item.assigneeName }} 在 {{ item.createTime }} 发起流程
                  </el-card>
                  <el-card v-if="item.activityType === 'userTask'" class="box-card" shadow="hover">
                    <el-descriptions :column="5" :labelStyle="{'font-weight': 'bold'}">
                      <el-descriptions-item label="实际办理">{{ item.assigneeName || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="候选办理">{{ item.candidate || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="接收时间">{{ item.createTime || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="办结时间">{{ item.endTime || '-' }}</el-descriptions-item>
                      <el-descriptions-item label="耗时">{{ item.duration || '-'}}</el-descriptions-item>
                    </el-descriptions>
                    <div v-if="item.commentList && item.commentList.length > 0">
                      <div v-for="(comment, index) in item.commentList" :key="index">
                        <el-divider content-position="left">
                          <el-tag :type="approveTypeTag(comment.type)" size="small">{{ commentType(comment.type) }}</el-tag>
                          <el-tag type="info" effect="plain" size="small">{{ comment.time }}</el-tag>
                        </el-divider>
                        <span>{{ comment.fullMessage }}</span>
                      </div>
                    </div>
                  </el-card>
                  <el-card v-if="item.activityType === 'endEvent'" class="box-card" shadow="hover">
                    {{ item.createTime }} 结束流程
                  </el-card>
                </el-timeline-item>
              </el-timeline>
            </div>
          </el-col>
        </el-card>
      </el-tab-pane>

      <el-tab-pane label="流程跟踪" name="track">
        <el-card class="box-card" shadow="never">
          <process-viewer :key="`designer-${loadIndex}`" :style="'height:' + height" :xml="xmlData"
                          :finishedInfo="finishedInfo" :allCommentList="historyProcNodeList"
          />
        </el-card>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup lang="ts">
  import {detailProcessByDataId} from "@/api/workflow/process";
  import ProcessViewer from '@/components/ProcessViewer'
  import {
      useFlowable
  } from '@/views/workflow/hooks/useFlowable'

  defineOptions({ name: 'HistoricDetail' })
  const props = defineProps({
    dataId: {
        type: String,
        default: '',
        required: true
    }
  })
  const commentType = computed(() => {
    return val => {
      switch (val) {
        case '1': return '通过'
        case '2': return '退回'
        case '3': return '驳回'
        case '4': return '委派'
        case '5': return '转办'
        case '6': return '终止'
        case '7': return '撤回'
        case '8': return '拒绝'
        case '9': return '跳过'
        case '10': return '前加签'
        case '11': return '后加签'
        case '12': return '多实例加签'
        case '13': return '跳转'
        case '14': return '收回'
      }
    }
  })

  const approveTypeTag = computed(() => {
    return val => {
      switch (val) {
        case '1': return 'success'
        case '2': return 'warning'
        case '3': return 'danger'
        case '4': return 'primary'
        case '5': return 'success'
        case '6': return 'danger'
        case '7': return 'info'
      }
    }
  })

  const { getFormComponent } = useFlowable()
  const height = ref(document.documentElement.clientHeight - 205 + 'px;')
  // 模型xml数据
  const loadIndex = ref(0)
  const xmlData = ref(null)
  const finishedInfo = ref({
    finishedSequenceFlowSet: [],
    finishedTaskSet: [],
    unfinishedTaskSet: [],
    rejectedTaskSet: []
  })
  const historyProcNodeList = ref<any>([])
  const processed = ref(false)
  const activeName = ref('form') //获取当然tabname
  const processFormList = ref<any>([])
  const customForm =  ref({ //自定义业务表单
    formId: '',
    title: '',
    disabled: false,
    visible: false,
    formComponent: null,
    model: {},
    /*流程数据*/
    customFormData: {},
    isNew: false,
    disableSubmit: true
  })
  const fileDisplay = ref(false) // formdesigner是否显示上传的文件控件
  const fileList = ref<any>([]) //表单设计器上传的文件列表
  const key = ref<any>()

  const init = () => {
   // 获取流程变量
   detailProcesssByDataId(props.dataId);
  }
  const detailProcesssByDataId = (dataId) => {
    const params = {dataId: dataId}
    detailProcessByDataId(params).then(res => {
      console.log("detailProcessByDataId res=",res);
      if (res.code === 200 && res.data != null) {
        const data = res.data;
        xmlData.value = data.bpmnXml;
        processFormList.value = data.processFormList;
        if(processFormList.value.length == 1 &&
           processFormList.value[0].formValues.hasOwnProperty('routeName')) {
           customForm.value.disabled = true;
           customForm.value.visible = true;
           customForm.value.formComponent = getFormComponent(processFormList.value[0].formValues.routeName).component;
           customForm.value.model = processFormList.value[0].formValues.formData;
           customForm.value.customFormData = processFormList.value[0].formValues.formData;
           console.log("detailProcess customForm.value",customForm.value);
        }
        historyProcNodeList.value = data.historyProcNodeList;
        finishedInfo.value = data.flowViewer;
      }
    })
  }
 
  const setIcon = (val) => {
    if (val) {
      return "el-icon-check";
    } else {
      return "el-icon-time";
    }
  }
  const setColor = (val) => {
    if (val) {
      return "#2bc418";
    } else {
      return "#b3bdbb";
    }
  }
 
  onMounted(() => {
    init();
  });

</script>
<style lang="scss" scoped>
.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}
.clearfix:after {
  clear: both
}

.box-card {
  width: 100%;
  margin-bottom: 20px;
}

.el-tag + .el-tag {
  margin-left: 10px;
}

.el-row {
  margin-bottom: 20px;
  &:last-child {
    margin-bottom: 0;
  }
}
.el-col {
  border-radius: 4px;
}

.button-new-tag {
  margin-left: 10px;
}
</style>

3.2 寻找业务表单的设计

根据业务服务名称找到对应的业务表单,代码如下

import { listCustomForm } from "@/api/workflow/customForm";

export const useFlowable = () => {
  const customformList = ref<any>([]);
  const formQueryParams = reactive<any>({
    pageNum: 1,
    pageSize: 1000
  });

  const allFormComponent = computed(() => {
    return customformList.value;
  })
  /* 挂载自定义业务表单列表 */
   const ListCustomForForm = async () => {
    listCustomForm(formQueryParams).then(res => {
      let  cfList = res.rows;
        cfList.forEach((item, index) => {
          let cms = {
              text:item.flowName,
              routeName:item.routeName,
              component: markRaw(defineAsyncComponent( () => import(/* @vite-ignore */`../../${item.routeName}.vue`))),
              businessTable:'wf_demo'
          }
          customformList.value.push(cms);
        })
    })
  }
  const getFormComponent = (routeName) => {
    return allFormComponent.value.find((item) => item.routeName === routeName) || {}
  }

  onMounted(() => {
    ListCustomForForm();
  });

  return {
    allFormComponent,
    getFormComponent
  }
}

3.3 业务表单的设计

这里主要是列表的testDemo为例,代码如下

<template>
  <div class="app-container">
    <el-form v-model="queryParams" ref="queryFormRef" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="用户账号" prop="userName">
        <el-input
          v-model="queryParams.userName"
          placeholder="请输入用户账号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="用户昵称" prop="nickName">
        <el-input
          v-model="queryParams.nickName"
          placeholder="请输入用户昵称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="用户邮箱" prop="email">
        <el-input
          v-model="queryParams.email"
          placeholder="请输入用户邮箱"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="头像地址" prop="avatar">
        <el-input
          v-model="queryParams.avatar"
          placeholder="请输入头像地址"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="small"
          @click="handleAdd"
          v-hasPermi="['workflow:demo:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="small"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['workflow:demo:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="small"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['workflow:demo:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="small"
          @click="handleExport"
          v-hasPermi="['workflow:demo:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="20" align="center" />
      <el-table-column label="用户ID" align="center" prop="demoId" v-if="true"/>
      <el-table-column label="用户账号" align="center" prop="userName" />
      <el-table-column label="用户昵称" align="center" prop="nickName" />
      <el-table-column label="流程状态" align="center" prop="actStatus" />
      <el-table-column label="待处理节点" align="center" prop="taskName" />
      <el-table-column label="处理人" align="center" prop="todoUsers" />
      <el-table-column label="操作" align="center" width="420">
        <template #default="scope">
          <act-apply-btn @success="getList" :data-id="String(scope.row.demoId)" :serviceName="serviceName" :variables="scope.row"></act-apply-btn>
          <a-divider type="vertical" />
          <act-historic-detail-btn :data-id="String(scope.row.demoId)"></act-historic-detail-btn>
          <a-divider type="vertical" />
          <el-button
            size="default"
            type="primary"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['workflow:demo:edit']"
          >修改</el-button>
          <el-button
            size="default"
            type="primary"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['workflow:demo:remove']"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total>0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改DEMO对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="用户账号" prop="userName">
          <el-input v-model="form.userName" placeholder="请输入用户账号" />
        </el-form-item>
        <el-form-item label="用户昵称" prop="nickName">
          <el-input v-model="form.nickName" placeholder="请输入用户昵称" />
        </el-form-item>
        <el-form-item label="用户邮箱" prop="email">
          <el-input v-model="form.email" placeholder="请输入用户邮箱" />
        </el-form-item>
        <el-form-item label="头像地址" prop="avatar">
          <el-input v-model="form.avatar" placeholder="请输入头像地址" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script setup name="Demo" lang="ts">
  import { listDemo, getDemo, delDemo, addDemo, updateDemo } from "@/api/workflow/demo";
  import ActApplyBtn from "../components/ActApplyBtn";
  import ActHistoricDetailBtn from "../components/ActHistoricDetailBtn";

  const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  const queryFormRef = ref(null)
  const formRef = ref<ElFormInstance>();
  // 按钮loading
  const buttonLoading = ref(false)
  // 遮罩层
  const loading = ref(true)
  // 选中数组
  const ids = ref<any>([])
  // 非单个禁用
  const single = ref(true)
  // 非多个禁用
  const multiple = ref(true)
  // 显示搜索条件
  const showSearch = ref(true)
  // 总条数
  const total = ref(0)
  // DEMO表格数据
  const demoList = ref<any>([])
  // 弹出层标题
  const title = ref("")
  // 是否显示弹出层
  const open = ref(false)
  // 查询参数
  const queryParams = ref ({
    pageNum: 1,
    pageSize: 10,
    userName: undefined,
    nickName: undefined,
    email: undefined,
    avatar: undefined,
    status: undefined,
  })
  const serviceName = ref('wfDemoService')
  // 表单参数
  const form = ref({
    demoId: undefined,
    userName: '',
    nickName: '',
  })
  // 表单校验
  const rules = {
    demoId: [
      { required: true, message: "DEMO-ID不能为空", trigger: "blur" }
    ],
    userName: [
      { required: true, message: "用户账号不能为空", trigger: "blur" }
    ]
  }
  /** 查询DEMO列表 */
  const getList = () => {
    loading.value = true;
    listDemo(queryParams.value).then(response => {
      demoList.value = response.rows;
      total.value = response.total;
      loading.value = false;
    });
  }
  // 取消按钮
  const cancel = () => {
    open.value = false;
    reset();
  }
  // 表单重置
  const reset = () => {
    form.value = {
      demoId: undefined,
      userName: '',
      nickName: '',
      email: undefined,
      avatar: undefined,
      status: undefined,
      delFlag: undefined,
      createBy: undefined,
      createTime: undefined,
      updateBy: undefined,
      updateTime: undefined,
      remark: undefined
    };
    formRef.value?.resetFields();
  }
  /** 搜索按钮操作 */
  const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
  }
  /** 重置按钮操作 */
  const resetQuery = () => {
    queryFormRef.value?.resetFields();
    handleQuery();
  }
  // 多选框选中数据
  const handleSelectionChange = (selection) => {
    ids.value = selection.map(item => item.demoId)
    single.value = selection.length!==1
    multiple.value = !selection.length
  }
  /** 新增按钮操作 */
  const handleAdd = () => {
    reset();
    open.value = true;
    title.value = "添加DEMO";
  }
  /** 修改按钮操作 */
  const handleUpdate = (row) => {
    loading.value = true;
    reset();
    const demoId = row.demoId || ids.value
    getDemo(demoId).then(response => {
      loading.value = false;
      form.value = response.data;
      open.value = true;
      title.value = "修改DEMO";
    });
  }
  /** 提交按钮 */
  const submitForm = () => {
    formRef.value?.validate(async (valid: boolean) => {
      if (valid) {
        buttonLoading.value = true;
        if (form.value.demoId != null) {
          updateDemo(form.value).then(response => {
            proxy?.$modal.msgSuccess("修改成功");
            open.value = false;
            getList();
          }).finally(() => {
            buttonLoading.value = false;
          });
        } else {
          addDemo(form.value).then(response => {
            proxy?.$modal.msgSuccess("新增成功");
            open.value = false;
            getList();
          }).finally(() => {
            buttonLoading.value = false;
          });
        }
      }
    });
  }
  /** 删除按钮操作 */
  const handleDelete = (row) => {
    const demoIds = row.demoId || ids.value;
    proxy?.$modal.confirm('是否确认删除DEMO编号为"' + demoIds + '"的数据项?').then(() => {
      loading.value = true;
      return delDemo(demoIds);
    }).then(() => {
      loading.value = false;
      getList();
      proxy?.$modal.msgSuccess("删除成功");
    }).catch(() => {
    }).finally(() => {
      loading.value = false;
    });
  }
  /** 导出按钮操作 */
  const handleExport = () => {
    proxy?.download('workflow/demo/export', {
      ...queryParams.value
    }, `demo_${new Date().getTime()}.xlsx`)
  }

  onMounted(() => {
    getList();
  })

</script>
<style lang="scss" scoped>
.small-padding-demo {
  .cell {
    padding-left: 5px;
    padding-right: 5px;
  }
}

.fixed-width-demo {
  .el-button--mini {
    padding: 7px 10px;
    width: 60px;
  }
}
</style>


第四章:主要界面设计

4.1 业务表单与流程的关联配置

image.png

点击关联后如下:

image.png

4.2 testDemo业务列表

  • 这里以testDemo列表为例,如下:

image.png

  • 点击历史信息可以看到

image.png

image.png

image.png

第五章:总结

      从上面我们可以看到,通过前后端技术实现了业务表单的流程嵌入,同时可以不影响原来的业务逻辑,大大简化了业务表单的流程审批开发,可以专注于业务的逻辑开发。

5.1 前端方面

     关键是通过动态组件来动态显示对应的业务表单,实现不同业务的通用显示。

5.2 后端方面

     提供流程的通用接口,可以扩展实际业务流程的业务需求。

     总之,总体上在不影响原先业务的开发设计上,简单方便地加上流程功能,快速实现实际业务的需要。


相关文章
22activiti - 流程管理定义(查询流程状态)
22activiti - 流程管理定义(查询流程状态)
137 0
|
6月前
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(一)
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(一)
318 1
|
6月前
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(三)
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(三)
317 1
|
6月前
|
前端开发
基于jeecgboot的flowable流程支持退回到发起人节点表单修改功能
基于jeecgboot的flowable流程支持退回到发起人节点表单修改功能
610 0
|
6月前
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(二)
基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(二)
166 0
|
3月前
|
开发工具 Android开发
Android项目架构设计问题之外部客户方便地设置回调如何解决
Android项目架构设计问题之外部客户方便地设置回调如何解决
27 0
|
6月前
|
移动开发 前端开发
基于若依的ruoyi-nbcio流程管理系统中自定义业务流程发布动态更新业务流程关联信息
基于若依的ruoyi-nbcio流程管理系统中自定义业务流程发布动态更新业务流程关联信息
121 2
|
6月前
|
前端开发
基于jeecgboot的flowable流程增加节点表单的支持(四)
基于jeecgboot的flowable流程增加节点表单的支持(四)
76 1
|
6月前
基于jeecgboot的flowable流程增加节点表单的支持(三)
基于jeecgboot的flowable流程增加节点表单的支持(三)
150 1
|
6月前
|
前端开发
基于jeecgboot的flowable增加流程节点抄送功能
基于jeecgboot的flowable增加流程节点抄送功能
511 0