内部审批系统开发总结(1)-基于logicflow引擎和bpmn规范设定流程

363 阅读5分钟

背景

公司内部存在多套不同厂家的审批系统(每刻报、纷享销客等)用于不同部门(销售、财务、硬件库管等),并且不同系统之间数据无法共享,在公司节流大背景下,决定使用自研的审批系统(包含移动端),替换掉多家审批系统。

项目组成

对于审批系统来说,包含以下功能模块:

  • 拖拽不同表单控件生成的审批表单,包含控件:单行文本、多行文本、数字控件、下拉选择、成员多选/多选、部门选择、关联历史数据等
  • 基于logicflow和bpmn规范设定的审批流程,自定义边(带有条件判定决定流转方向)和点(带有审批权限和操作权限)
  • 人员/部门管理模块
  • 权限设定模块
  • 历史数据管理模块

针对以上功能规划,前端方面决定基于vite + Vue3 + antd-vue为主要技术栈,自定义表单控件格式和自定义表单渲染解决拖拽可配置的审批表单,logicflow进行定制化开发解决可配置的审批流程,因为后端使用activity7框架,所以前端的审批流定义xml文件基于bpmn规范

BPMN规范

BPMN 2.0(Business Process Model and Notation),简单来说是一套业务流程模型与符号建模标准,精准执行的语义来描述元素操作,以XML为载体,以符号可视化业务bpmn2.0简介

流程设定组成

流程设定主要包含以下节点:

  • 开始节点(StartEvent)
  • 结束节点(EndEvent)
  • 流程节点(UserTask)
  • 条件节点(ExclusiveGateway)
  • 并行节点(ParallelGateway)

开始节点/流程节点需要包含当前流程节点审批人(部门成员、部门负责人、连续部门负责人等)、当前流程节点下显示的表单控件的显示隐藏、当前流程节点下更改操作流程比如回转、提交、暂存等操作,同时回转、连续负责人审批等操作需要在xml文件里面加入侦听器

条件节点需要包含控制流程的条件,本质是有多少出边,就有多少判断条件,同时每个边都可以设置对应优先级,按照优先级从高到低,自动根据表单内容和条件配置控制审批流程走向

括号里面对应的是bpmn规范中对应的定义节点类型,需要保证最后解析出来的xml文件

logicflow自定义节点

这里拿流程节点举例,和官方样例不同的一点是可以是有createVNode方法和render方法直接将vue组件转换html并挂载的方式 此处是定义ProcessTaskNodeByHTML节点,并将ProcessTaskBasicInfo组件挂载上去

流程设定.jpg

import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
import { checkNodeIsDelete } from '@/utils/flowSet';
import dayjs from "dayjs";
import { getBpmnId } from '@/extension/bpmn/bpmnIds';
import {createVNode, render} from "vue";
import ProcessTaskBasicInfo from "./processTaskBasicInfo.vue";
import {message} from "ant-design-vue";
import { toRaw } from 'vue';

class ProcessTaskNodeByHTML extends HtmlNode {
  setHtml(rootEl) {
    const { properties, id } = this.props.model;
    const { graphModel, model } = this.props;
    const vNode = createVNode(ProcessTaskBasicInfo, {
      graphModel: graphModel,
      id: id,
      text: properties.text,
      properties: properties,
    })

    render(vNode, rootEl)
  }
}

class ProcessTaskNodeByHTMLModel extends HtmlNodeModel {

  initNodeData(data) {
    super.initNodeData(data);
    // 定义变得规则
    const customRuleList = this.customNodeRule();
    this.sourceRules.push(...customRuleList);
    this.width = 160;
    this.height = 40;
    this.radius = 2;
  }

  customNodeRule() {
    const unStartTaskNodeRule = {
      message: "下一个节点不是流程开始节点",
      validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
        return targetNode.type !== "StartTaskNode";
      },
    }
    // 流程节点不能连接自己
    const connectionNotMyselfNodeRule = {
      message: "流程节点不能连接自己",
      validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
        return sourceNode.id !== targetNode.id;
      },
    }
    return [
      unStartTaskNodeRule,
      // connectionNotConditionGatewayNodeRule,
      connectionNotMyselfNodeRule,
    ];
  }

  setAttributes() {
    this.text.draggable = false;
    this.text.editable = false; // 禁止节点文本编辑
    // this.editable = false;
    this.width = 160;
    this.height = 40;
    this.radius = 2;

    const customMenu = [
      {
        text: "删除节点",
        callback: (node) => {
          if (checkNodeIsDelete(window.lf, node)) {
            this.graphModel.deleteNode(node.id);
          }
        }
      },
      {
        text: "复制节点",
        callback: (node) => {
          const {
            status = 0,
          } = window.currVersionInfo;
          if (status !== 1) {
            message.error(`${ status === 0 ? '历史状态' : '启用中状态'}节点和边不可删除`);
            return false;
          }
          const currNodeModel = this.graphModel.getNodeModelById(node.id);
          const { type, x, y } = currNodeModel;
          const idN = `Activity_${getBpmnId()}_${dayjs().valueOf()}`;
          const nodeConfig = {
            type,
            id: idN,
            x: currNodeModel.x + 180,
            y: currNodeModel.y,
            text: currNodeModel.text,
            properties: {
              ...toRaw(currNodeModel.properties),
              id: idN,
            },
          };
           window?.lf?.addNode(nodeConfig);
        },
      },
    ];
    // 重新赋值新的菜单节点
    this.menu = customMenu;
  }

  getTextStyle() {
    const style = super.getTextStyle();
    style.display = 'none';
    return style;
  }

  getNodeStyle() {
    const style = super.getNodeStyle();
    return style;
  }
}

export default {
  type: "ProcessTaskNodeByHTML",
  view: ProcessTaskNodeByHTML,
  model: ProcessTaskNodeByHTMLModel
};

此处是ProcessTaskNodeByHTML组件的具体定义

<template>
  <div class="uml-wrapper">
    <div class="uml-body" :id="id">
      <img
          class="uml-logo"
          :id="`${id}_logo`"
          style="z-index: 1;"
          :src="processEditImg"
      />
      <div class="uml-text">{{ text || '' }}</div>
      <img
          v-if="appManageStore?.currVersionInfo?.status === 1"
          class="uml-menu-logo"
          :id="`${id}_menu_logo`"
          @click="(...arg) => setData(arg[0], id)"
          @mousedown.stop="stop"
          :src="processMore"
      />
    </div>
  </div>
</template>

<script setup>
import useStore from '@/store'
import {
  defineProps,
} from 'vue';
const store = useStore();
const { appManageStore } = store;

const processEditImg = new URL('@/assets/flowSet/processEdit.png', import.meta.url).href;
const processMore = new URL('@/assets/flowSet/processMore.png', import.meta.url).href;
const processFinishEditImg = new URL('@/assets/flowSet/processFinishEdit.png', import.meta.url).href;
const processUnStartEditImg = new URL('@/assets/flowSet/processUnStartEdit.png', import.meta.url).href;
const processAdultingEditImg = new URL('@/assets/flowSet/processAdultingEdit.png', import.meta.url).href;
const processErrorEditImg = new URL('@/assets/flowSet/processErrorEdit.png', import.meta.url).href;

const emits = defineEmits(['menuClick']);
const props = defineProps({
  graphModel: {
    type: Object,
    default: () => {},
  },
  text: {
    type: String,
    default: '',
  },
  id: {
    type: String,
    default: '',
  },
  properties: {
    type: Object,
    default: () => {}
  }
})

function stop(ev) {
  ev.stopPropagation();
};


const setData = (e, currNodeId = '') => {
  const { graphModel } = props;
  const currNodeModel = graphModel?.getNodeModelById(currNodeId)
  const domPos = graphModel?.getPointByClient({ x: e?.x || e?.clientX, y: e?.y || e?.clientY })
  graphModel?.eventCenter.emit('node:contextmenu', {
    e,
    data: currNodeModel,
    position: domPos,
  });
};
</script>

<style lang="less" scoped>
.uml-wrapper {
  background: rgb(247, 248, 250);
  border: 1px solid #E5E6EB;
  color: #1D2129;
  stroke: #E5E6EB;
  stroke-width: 1;
  height: 40px;
  padding: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: PingFangSC-Regular;
  font-size: 14px;
  font-weight: normal;

  &:hover {
    background: #E8F3FF;
    border: 1px solid #8AB1FF;
    color: #1664FF;

    .uml-menu-logo {
      display: inline-block;
    }
  }

  .uml-body {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .uml-text {
    width: 84px;
    margin: 0 10px;
    overflow:hidden; //超出的文本隐藏
    text-overflow:ellipsis; //溢出用省略号显示
    white-space:nowrap; //溢出不换行
  }

  .uml-logo {
    width: 16px;
    height: 16px;
  }

  .uml-menu-logo {
    width: 16px;
    height: 16px;
    cursor: pointer;
    display: none;
  }
}

.uml-body {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.uml-text {
  width: 84px;
  margin: 0 10px;
  overflow:hidden; //超出的文本隐藏
  text-overflow:ellipsis; //溢出用省略号显示
  white-space:nowrap; //溢出不换行
}

.uml-logo {
  width: 16px;
  height: 16px;
}

.uml-menu-logo {
  width: 16px;
  height: 16px;
  cursor: pointer;
  display: none;
}

.currProcessingNode {
  /* 00颜色/3状态色/S10 */
  background: #165DFF !important;
  border: 1px solid #165DFF !important;
  border-radius: 4px !important;
  stroke: #165DFF !important;
  color: #ffffff !important;;
  stroke-width: 1;
  padding: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: PingFangSC-Regular;
  font-size: 14px;
  font-weight: normal;
}
</style>

logicflow自定义边

这里是带有条件的自定义边,在边的中间绑定部门判断条件,比如(部门、人员、时间、数字、文本)(等于、小于、大于、不等于)等操作,同时还需要限制条件边一定要从条件节点作为开始节点。需要注意的是,如果采用自定义边的情况需要自定义计算条件显示框的位置和拖动折线是否的判断是否折线的位置。

测试-条件-gif.gif

import { h, PolylineEdge, PolylineEdgeModel } from "@logicflow/core"
import ConditionPloylineGroup from './conditionPloylineGroup.vue';
import { createApp } from 'vue'
import {message} from "ant-design-vue";
import { checkNodeIsDelete } from '@/utils/flowSet';

const DEFAULT_WIDTH = 240;
const DEFAULT_HEIGHT = 120;


const gatewayAddImg = new URL('@/assets/flowSet/ifGatewayAdd.png', import.meta.url).href;
const gatewayDeleteImg = new URL('@/assets/flowSet/ifGatewayDelete.png', import.meta.url).href;

class ConditionPolylineModel extends PolylineEdgeModel {


  initEdgeData(data) {
    super.initEdgeData(data);
    this.offset = 20;
    this.text.editable = false;
    // 禁止节点文本编辑
    this.editable = false;
    this.draggable = true;
    // 边是否可拖动
    this.isHitable = true;
    // 边是否hover时候,显示操作区域
    this.isHovered = false;
  }

  getTextStyle() {
    const style = super.getTextStyle();
    style.display = 'none';
    style.background.fill = 'rgba(0,0,0,0)';
    return style;
  }

}


class ConditionPolylineView extends PolylineEdge {

  generateGroup() {
    const { model } = this.props;
    const { properties } = model;
    const { name, level = 0, conditionConfig = {} } = properties;
    const id = model.id;
    this.ploylineGroup = createApp(ConditionPloylineGroup, {
      name,
      level,
      conditionConfig,
      key: `condition_ployline_graph_container_${id}`
    });
    const htmlContainer = document.getElementById(`${id}`);
    this.ploylineGroup.mount(htmlContainer);
  }

  pointFilter(points) {
    const allPoints = points;
    let i = 1;
    while (i < allPoints.length - 1) {
      const [x, y] = allPoints[i - 1];
      const [x1, y1] = allPoints[i];
      const [x2, y2] = allPoints[i + 1];
      if ((x === x1 && x1 === x2) || (y === y1 && y1 === y2)) {
        allPoints.splice(i, 1);
      } else {
        i++;
      }
    }
    return allPoints;
  }


  searchMiddle(arr) {
    if (arr.length <= 1) return false;
    let first = 0;
    let last = arr.length - 1;
    while (first !== last && first + 1 !== last && last - 1 !== first) {
      first++;
      last--;
    }
    if (first === last) {
      return [arr[--first], arr[last]];
    }
    return [arr[first], arr[last]];
  }

  handleEdgeCopy(props) {
    const { model } = this.props;
    const currNodeModel = window.lf.getEdgeModelById(model.id);
    const parentNodeId = model?.sourceNodeId;
    const allEdgesByIncomeParentNode = window.lf.getNodeOutgoingEdge(parentNodeId).filter(item => item.type === 'ConditionPolyline');
    const currLayerConditionPolylineLen = allEdgesByIncomeParentNode?.length || 0;
    window.lf.addEdge({
      type: model.type,
      sourceNodeId: model.sourceNodeId,
      targetNodeId: model.targetNodeId,
      startPoint: model.startPoint,
      endPoint: model.endPoint,
      properties: {
        ...currNodeModel.properties,
        name: currNodeModel.properties.name,
        level: currLayerConditionPolylineLen + 1,
      }
    });
  }

  handleEdgeDelete() {
    const selectedEdgeAndNodeList = window.lf.getSelectElements();
    if (!selectedEdgeAndNodeList?.nodes?.length && !selectedEdgeAndNodeList?.edges?.length) {
      message.error('请选中节点或者边!')
      return;
    }
    checkNodeIsDelete(window.lf);
  }


  getEdge() {
    const { model } = this.props;
    const {
      id, points, isAnimation,
      arrowConfig, radius = 0,
      startPoint, endPoint,
    } = model;
    const style = model.getEdgeStyle();
    const animationStyle = model.getEdgeAnimationStyle();
    const points2 = this.pointFilter(
      points.split(" ").map((p) => p.split(",").map((a) => Number(a)))
    );
    const [startX, startY] = points2[0];
    let d = `M${startX} ${startY}`;
    // 1) 如果一个点不为开始和结束,则在这个点的前后增加弧度开始和结束点。
    // 2) 判断这个点与前一个点的坐标
    //    如果x相同则前一个点的x也不变,
    //    y为(这个点的y 大于前一个点的y, 则 为 这个点的y - 5;小于前一个点的y, 则为这个点的y+5)
    //    同理,判断这个点与后一个点的x,y是否相同,如果x相同,则y进行加减,如果y相同,则x进行加减
    for (let i = 1; i < points2.length - 1; i++) {
      const [preX, preY] = points2[i - 1];
      const [currentX, currentY] = points2[i];
      const [nextX, nextY] = points2[i + 1];
      if (currentX === preX && currentY !== preY) {
        const y = currentY > preY ? currentY - radius : currentY + radius;
        d = `${d} L ${currentX} ${y}`;
      }
      if (currentY === preY && currentX !== preX) {
        const x = currentX > preX ? currentX - radius : currentX + radius;
        d = `${d} L ${x} ${currentY}`;
      }
      d = `${d} Q ${currentX} ${currentY}`;
      if (currentX === nextX && currentY !== nextY) {
        const y = currentY > nextY ? currentY - radius : currentY + radius;
        d = `${d} ${currentX} ${y}`;
      }
      if (currentY === nextY && currentX !== nextX) {
        const x = currentX > nextX ? currentX - radius : currentX + radius;
        d = `${d} ${x} ${currentY}`;
      }
    }
    const [endX, endY] = points2[points2.length - 1];
    d = `${d} L ${endX} ${endY}`;
    const attrs = {
      d,
      style: isAnimation ? animationStyle : {},
      ...style,
      ...arrowConfig,
      fill: "none"
    };

    const {
      customWidth = DEFAULT_WIDTH,
      customHeight = DEFAULT_HEIGHT
    } = model.getProperties();

    // 计算边的弹窗位置
    const modelPos = model.getTextPosition();
    const pointsList = this.pointsList;
    if (pointsList?.length > 1) {
      const res = this.searchMiddle(pointsList);
      if (res) {
        const [middle, next] = res;
        let {x: x1, y: y1} = middle;
        let {x: x2, y: y2} = next;
        modelPos.x = x1 - (x1 - x2) / 2;
        modelPos.y = y1 - (y1 - y2) / 2;
      }
    }

    const positionData = {
      x: modelPos.x - customWidth / 2,
      y: modelPos.y - customHeight / 2,
      width: customWidth,
      height: customHeight
    };



    setTimeout(() => {
      this.generateGroup();
    }, 0);

    return h("g", {}, [
      h("path", {
        ...attrs
      }),
      h("foreignObject", { ...positionData, style: { position: 'relavetive' } }, [
        h("div", {
          id,
          className: "lf-custom-edge-wrapper",
          style: { position: 'relative', zIndex: 1 },
        }),
        h("div", {
          id: `custom_menu_container_${id}`,
          className: 'conditionPolylineControlContainer',
          style: { opacity: model?.isSelected ? 1 : 0 }
        }, [
          h('div', {
            id: `custom_menu_container_group_${id}`,
            className: 'conditionPolylineControlGroup',
          }, [
            h("div", {
              id: `custom_menu_item_${id}_delete`,
              className: 'conditionPolylineCustomBtn',
              onClick: this.handleEdgeDelete,
            }, [
              h("img", {
                src: gatewayDeleteImg,
              }),
            ]),
            h("div", {
              id: `custom_menu_item_${id}_copy`,
              className: 'conditionPolylineCustomBtn',
              onClick: () => {
                this.handleEdgeCopy(this.props)
              },
            }, [
              h("img", {
                src: gatewayAddImg,
              }),
            ]),
          ])
        ]),
      ])
    ]);
  }
}



export default {
  type: "ConditionPolyline",
  view: ConditionPolylineView,
  model: ConditionPolylineModel
};

logicflow自定义解析xml

根据logicflow官方文档说明,转换xml的文件是基于官方的plugin叫做BpmnAdapter。因为logicflow提供adapter适配器接口,可以自定义数据转换格式和逻辑,所以这个插件本质是复写logicflow的adpaterIn和adapterOut的逻辑,可以直接clone下来对应插件逻辑按照当前需求进行定制化开发。 插件地址:bpmnAdapter插件源码地址

为了保证xml文件相对干净并且配合流程设定的多版本管理,这里只保留必要的属性,xml文件在upload之后,具体每个节点的配置信息,会单独走第二个接口和upload成功之后的文件编号进行二次关联。

简单举个例子,比如我们要针对不同流程负责人配置,在不同<userTask>标签下生成不同的侦听器,我们可以在convertLf2ProcessData方法里面加入自定义的generateCustomPropertiesByDynamicIdsAndPrincipalType方法

function generateCustomPropertiesByDynamicIdsAndPrincipalType(principalType = '', dynamicPrincipalIds = '', properties, extraConfig = {}) {
  const taskId = properties.id || '';
  // 代表真正的开始节点,需要加入特殊的listener处理
  if (extraConfig?.isRealStartNode) {
    properties['activiti:assignee'] = "${STARTER_USER}";
    // 设置extension特殊属性
    properties['eventExtensionElements'] = {
      event: 'create',
      delegateExpression: "${START_NODE_TASK_LISTENER}",
    }
  } else {
    // 无需负责人
    if (principalType === 'NONE') {
      return properties;
    }
    // 流程发起人
    if (principalType === 'STARTER_USER') {
      properties['activiti:assignee'] = "${STARTER_USER}";
    }
    // 用户
    if (principalType === 'USERS') {
      properties['activiti:candidateUsers'] = "${" + `${taskId}_USERS` + "}";
    }
    // 部门负责人
    if (principalType === 'CONTENT') {
      properties['activiti:assignee'] = "${" + `${taskId}_CONTENT` + "}"
      // 设置extension特殊属性
      properties['eventExtensionElements'] = {
        event: 'create',
        delegateExpression: "${CONTENT_PRINCIPAL_TASK_LISTENER}",
      }
    }

    // 部门负责人
    if (principalType === 'ORG_PRINCIPAL') {
      properties['activiti:assignee'] = "${ORG_LEVEL}";
      // 设置extension特殊属性
      properties['eventExtensionElements'] = {
        event: 'create',
        delegateExpression: "${ORG_PRINCIPAL_TASK_LISTENER}",
      }
      // 设置动态连续负责人的处理逻辑
      properties['multiInstanceLoopCharacteristics'] = {
        'isSequential': "true",
        "activiti:collection": `${taskId}_ORG_PRINCIPAL`,
        "activiti:elementVariable": "ORG_LEVEL",
      }
    }
    // 连续多级部门负责人
    if (principalType === 'ORG_PRINCIPAL_ALL') {
      properties['activiti:assignee'] = "${ORG_LEVEL}";
      // 设置extension特殊属性
      properties['eventExtensionElements'] = {
        event: 'create',
        delegateExpression: "${ORG_PRINCIPAL_TASK_LISTENER}",
      }
      // 设置动态连续负责人的处理逻辑
      properties['multiInstanceLoopCharacteristics'] = {
        'isSequential': "true",
        "activiti:collection": `${taskId}_ORG_PRINCIPAL_ALL`,
        "activiti:elementVariable": "ORG_LEVEL",
      }
    }
  }

  return properties;
}

比如为了方便流程回退操作之后,重新发起流程,因为activity7框架本质不支持此种操作,所以为了满足这种逻辑,需要在图上看到的开始节点之前,在转换过程中,在开始节点直接重新插入一个虚拟的开始节点。这里有一个特殊的逻辑,是根据bpmn规范,开始节点只能存在一个,所以在插入后,还需要将adapterIn和apadterOut之后针对图上可显示的开始节点进行userTask和startTask的转换。


/**
 * 作用: 为了流程回退,专门进行虚拟节点转换
 */
// 思路: 查找当前的startEvent, 然后添加一条edge和一个node,连接当前startEvent,并且将startEvent转换为userTask
function generateVirtualStartEventByCurrProcessJSON(data) {
  const startEventInfo = data.nodes.find(item => item.type === 'StartTaskNode');
  const startEventIndex = startEventInfo ? data.nodes.findIndex(item => item.id === startEventInfo.id) : -1;
  if (startEventInfo) {
    //复制开始节点信息
    const { properties, text, ...otherParams } = startEventInfo;
    const tempStartEventNode = {
      ...otherParams,
      text: {
        ...text,
        value: '虚拟-开始节点',
      },
      type: startEventInfo.type,
      id: `Event_Start_${getBpmnId()}`,
      name: '虚拟-开始节点'
    }
    // 插入节点
    // data.nodes.push(tempStartEventNode);
    data.nodes[startEventIndex]['type'] = 'ProcessTaskNodeByHTML';
    // 特殊处理,懒得想更好的处理条件了,判断如果有这个属性,代表插入特殊的Listener的结构
    if (data.nodes[startEventIndex]?.['properties']) {
      data.nodes[startEventIndex]['properties']['isRealStartNode'] = true;
    }
    // 插入节点
    data.nodes.unshift(tempStartEventNode);
    // 插入边
    const tempEdgeByStartNode = {
      id: `Flow_${getBpmnId()}`,
      type: "polyline",
      sourceNodeId: tempStartEventNode.id,
      targetNodeId: startEventInfo.id,
      pointsList: [
        {
          x: tempStartEventNode.x,
          y: tempStartEventNode.y,
        },
        {
          x: startEventInfo.x,
          y: startEventInfo.y,
        },
      ],
      startPoint: {
        x: tempStartEventNode.x,
        y: tempStartEventNode.y,
      },
      endPoint: {
        x: startEventInfo.x,
        y: startEventInfo.y,
      }
    }
    data.edges.push(tempEdgeByStartNode);
  }
  return data;
}

举个例子,在adapterOut进行的定制化操作

const CustomBpmnAdapter = {
  pluginName: 'custom-bpmn-adapter',
  install(lf) {
    lf.adapterOut = this.adapterOut;
  },
  shapeConfigMap: new Map(),
  setCustomShape(key, val) {
    this.shapeConfigMap.set(key, val);
  },
  adapterOut(data) {
    // 生成虚拟开始节点,为了回退操作
    data = generateVirtualStartEventByCurrProcessJSON(data);

    // 生成UEL模板
    function generateUELTemplate(conditionConfig) {
      let conditionText = '';
      if (conditionConfig?.conditionType === 'formType-Name') {
        if (conditionConfig?.conditionOperator === 'empty') {
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = ` not empty ${conditionConfig.conditionId}`;
        } else {
          let customReplaceOperator = conditionConfig.conditionOperator;
          if (conditionConfig.conditionOperator === '=') {
            customReplaceOperator = 'eq';
          }
          if (conditionConfig.conditionOperator === '!=') {
            customReplaceOperator = 'ne';
          }
          conditionText = `${conditionConfig.conditionId} ${customReplaceOperator} "${conditionConfig.componentInputValue}"`;
        }
      } else if (conditionConfig?.conditionType === 'formType-DatePicker') {
        if (conditionConfig?.conditionOperator === 'empty') {
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = `not empty ${conditionConfig.conditionId}`;
        } else {
          const customReplaceOperator = conditionConfig?.conditionOperator === '=' ? '==' : conditionConfig?.conditionOperator;
          conditionText = `${conditionConfig.conditionId} ${customReplaceOperator} ${conditionConfig?.componentInputValue ? dayjs(conditionConfig?.componentInputValue).valueOf() : ''}`;
        }
      } else if (conditionConfig?.conditionType === 'formType-Num') {
        if (conditionConfig?.conditionOperator === 'empty') {
          // conditionText = `${conditionConfig.conditionId} == ''`;
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = `not empty ${conditionConfig.conditionId}`;
        } else {
          const customReplaceOperator = conditionConfig.conditionOperator === '=' ? '==' : conditionConfig.conditionOperator;
          conditionText = `not empty ${conditionConfig.conditionId} and ${conditionConfig.conditionId}${customReplaceOperator}${conditionConfig?.componentInputValue}`;
        }
      } else if (
        conditionConfig?.conditionType === 'creator'
        || conditionConfig?.conditionType === 'createDep'
      ) {
        if (conditionConfig?.conditionOperator === 'empty') {
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = `not empty ${conditionConfig.conditionId}`;
        } else {

          if (conditionConfig?.conditionType === 'createDep') {
            conditionText = conditionConfig?.childrenTreeNodeIds?.map(item => {
              const customReplaceOperator = conditionConfig?.conditionOperator === '=' ? '==' : conditionConfig?.conditionOperator;
              return `${conditionConfig.conditionId} ${customReplaceOperator} ${item}`
            }) || '';
             
            if (conditionText && conditionConfig?.conditionOperator === '!=') {
              conditionText = conditionText.join(' and ');
            } else if (conditionText) {
              conditionText = conditionText.join(' or ');
            }
          } else {
            conditionText = conditionConfig?.selectedTagList?.filter(item => item.value)?.map(item => {
              const customReplaceOperator = conditionConfig?.conditionOperator === '=' ? '==' : conditionConfig?.conditionOperator;
              return `${conditionConfig.conditionId} ${customReplaceOperator} ${item.value}`
            }) || '';
            
            if (conditionText && conditionConfig?.conditionOperator === '!=') {
              conditionText = conditionText.join(' and ');
            } else if (conditionText) {
              conditionText = conditionText.join(' or ');
            }
          }
        }
      } else if (conditionConfig?.conditionType === 'formType-Select') {
        if (conditionConfig?.conditionOperator === 'empty') {
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = `not empty ${conditionConfig.conditionId}`;
        } else {
          let customReplaceOperator = conditionConfig.conditionOperator;
          if (conditionConfig.conditionOperator === '=') {
            customReplaceOperator = 'eq';
          }
          if (conditionConfig.conditionOperator === '!=') {
            customReplaceOperator = 'ne';
          }
          conditionText = conditionConfig?.selectedTagList?.filter(item => item.value)?.map(item => {
            return `${conditionConfig.conditionId} ${customReplaceOperator} "${item.value}"`
          }) || '';
          if (conditionText && conditionConfig?.conditionOperator === '!=') {
            conditionText = conditionText.join(' and ');
          } else if (conditionText) {
            conditionText = conditionText.join(' or ');
          }
        }
      } else if (
        conditionConfig?.conditionType === 'formType-MemberMultiple'
        || conditionConfig?.conditionType === 'formType-MemberSingular'
      ) {
        if (conditionConfig?.conditionOperator === 'empty') {
          conditionText = `empty ${conditionConfig.conditionId}`;
        } else if (conditionConfig?.conditionOperator === 'notEmpty') {
          conditionText = `not empty ${conditionConfig.conditionId}`;
        } else {
          const customReplaceOperator = conditionConfig?.conditionOperator === '=' ? '==' : conditionConfig?.conditionOperator;
          conditionText = conditionConfig?.selectedTagList?.filter(item => item.value)?.map(item => {
            return `${conditionConfig.conditionId} ${customReplaceOperator} ${item.value}`
          }) || '';
          if (conditionText && conditionConfig?.conditionOperator === '!=') {
            conditionText = conditionText.join(' and ');
          } else if (conditionText) {
            conditionText = conditionText.join(' or ');
          }
        }
      } else if (conditionConfig?.conditionType === 'customOtherCondition') {
        conditionText = 'true';
      } else {
        conditionText = '';
      }
      return conditionText;
    }


    // UEL模板插入到边中
    data.edges = data.edges.reduce((preItem, currItem) => {
      if (currItem.type === 'ConditionPolyline') {
        const { conditionConfig } = currItem.properties;
        let conditionText = generateUELTemplate(conditionConfig);
        currItem.properties.conditionExpression = {
          'cdata': conditionText,
        }
      }
      preItem.push(currItem);
      return preItem;
    }, []);
    // edge可以按照level排序,保证插入xml文件中标签的顺序
    data.edges.sort((a, b) => {
      if (!a?.properties?.level || !b?.properties?.level) {
        return -1;
      }
      return a.properties.level >= b.properties.level ? 1 : -1;
    })
    // 重新组合edges

    const conditionPolylineEdgeList =  data.edges.filter(item => item.type === 'ConditionPolyline').sort((a, b) => {
      return a.properties.level > b.properties.level ? 1 : -1;
    })

    const unConditionPolylineEdgeList = data.edges.filter(item => item.type !== 'ConditionPolyline');


    data.edges = [...unConditionPolylineEdgeList, ...conditionPolylineEdgeList]

    const bpmnProcessData = {
      '-id': `Process_${getBpmnId()}`,
      '-name': `Process_${getBpmnId()}`,
      '-isExecutable': 'true',
    };
    convertLf2ProcessData(bpmnProcessData, data);
    const bpmnDiagramData = {
      '-id': 'BPMNPlane_1',
      '-bpmnElement': bpmnProcessData['-id'],
    };
    convertLf2DiagramData(bpmnDiagramData, data);
    let bpmnData = {
      'definitions': {
        '-xmlns': 'http://www.omg.org/spec/BPMN/20100524/MODEL',
        '-xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance",
        '-xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
        '-xmlns:activiti': 'http://activiti.org/bpmn',
        '-xmlns:bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI',
        '-xmlns:omgdc': 'http://www.omg.org/spec/DD/20100524/DC',
        '-xmlns:omgdi': 'http://www.omg.org/spec/DD/20100524/DI',
        '-typeLanguage': 'http://www.w3.org/2001/XMLSchema',
        '-expressionLanguage': 'http://www.w3.org/1999/XPath',
        '-targetNamespace': 'http://www.activiti.org/test',
        'process': bpmnProcessData,
        'bpmndi:BPMNDiagram': {
          '-id': 'BPMNDiagram_1',
          'bpmndi:BPMNPlane': bpmnDiagramData,
        },
      },
    };
    return bpmnData;
  },
};

之后我们可以在项目中直接引用定制化改好的plugin,在项目中plugin叫做CustomBpmnAdapter

后续

在下一篇文章,主要总结一下自定义拖拽表单组件和渲染表单的相关逻辑,