一、源码说明

与 Bpmn Designer 类似,但 Bpmn Viewer 与 Bpmn Mocker 相对而言功能项比较单一,所以依赖项也更少。

源码结构如下:

image-20240701113048073

与 Bpmn Designer 相比,Viewer 与 Mocker 更加推荐 组件化 方式使用。

components 中,包含 ViewerMockerTippyPopoverPresetViewer 四个默认导出组件。

二、组件说明

1. TippyPopover

该组件基于 tippyjs 开发,主要是作为一个 Popover 信息弹窗 使用,核心参数是 target 目标元素或元素 id,以及一个默认插槽。

其他参数与 tippyjs 的默认提供的配置参数基本一致。

2. Viewer

Viewer 组件作为 bpmn xml 的预览组件,默认该组件 只提供流程图预览,信息弹窗需要与 TippyPopover 结合使用。

其中,BpmnViewer 除了接收一个默认 xml 字符串之外,还接收一个 loading 布尔类型参数,用来设置画布区域的 loading 效果。

另外除了 createNewProcess 方法用来设置新的 xml 流程图之外,还提供了 setPassedNodes, setActiveNodes, setProcessingMarker 三个方法,分别用来设置 已经过节点、当前节点、处理中节点 三种节点样式。

在实际项目中,两者需要结合使用,参照 packages/bpmn-viewer/components/PresetViewer.vue:

<script setup lang="ts">
  import type { ComponentInstance } from 'vue'
  import type Viewer from 'bpmn-js/lib/Viewer'
  import type ElementRegistry from 'diagram-js/lib/core/ElementRegistry'
  import BpmnViewer from '@/components/Viewer/index.vue'
  import BpmnMocker from '@/components/Mocker/index.vue'
  import { is } from 'bpmn-js/lib/util/ModelUtil'
  import { getOneActivityVoByProcessInstanceIdAndActivityId } from '@/api/bpmn'
  import { Message } from '@arco-design/web-vue'

  const viewer = ref<ComponentInstance<typeof BpmnViewer>>()

  const viewerXmlLoading = ref(false)
  const shapeAndPops = ref<string[]>([])
  const nodeInfoDetails = ref<Record<string, any>>({})
  const processStateStyleMap = {
    processing: 'arcoblue',
    finish: 'green',
    pending: 'orangered'
  }

  const bpmnViewer = shallowRef<Viewer | undefined>()

  const processId = ref('78baf1d05bb742ec9bd17ff5f98b27a4')

  const reloadViewerXML = async () => {
    try {
      shapeAndPops.value = []
      nodeInfoDetails.value = {}

      viewerXmlLoading.value = true
      // @ts-expect-error
      const { data, success, msg } = await getHighLightedNodeVoByProcessInstanceId(processId.value)
      if (success) {
        const {
          activeActivityIds = [],
          hisActiveActivityIds = [],
          modelXml,
          highLightedFlows = []
        } = data || {}
        if (!modelXml) return

        await viewer.value?.createNewProcess(modelXml)

        viewer.value?.setPassedNodes([...highLightedFlows, ...hisActiveActivityIds])
        viewer.value?.setActiveNodes([...activeActivityIds])
        viewer.value?.setProcessingMarker([...activeActivityIds])

        initPopup()
      } else {
        Message.error(msg)
      }
      console.log(data)
    } finally {
      viewerXmlLoading.value = false
    }
  }

  const initPopup = () => {
    const registry = bpmnViewer.value?.get<ElementRegistry>('elementRegistry')
    if (!registry) return
    shapeAndPops.value = registry
      .filter((el: BpmnElement) => is(el, 'bpmn:Activity'))
      .map((e) => e.id)
  }

  const setBpmnViewer = (v) => (bpmnViewer.value = v)

  const getNodeDetails = (element: BpmnElement) => {
    if (is(element, 'bpmn:Activity')) {
      if (!nodeInfoDetails.value[element.id]) {
        nodeInfoDetails.value[element.id] = { loading: true }
        return mockNodeDetailGetter(element.id)
      }
    }
  }
</script>

<template>
  <ASpace align="center">
    <AButton type="primary" @click="reloadViewerXML">加载Viewer流程图</AButton>
  </ASpace>
  <ADivider />
  <BpmnViewer
    ref="viewer"
    :loading="viewerXmlLoading"
    @viewer-init="setBpmnViewer"
    @element-hover="getNodeDetails"
  />
  <tippy-popover
    v-for="shapeId in shapeAndPops"
    :key="shapeId"
    :target="`[data-element-id=${shapeId}]`"
  >
    <ASpin :loading="nodeInfoDetails[shapeId] && nodeInfoDetails[shapeId].loading">
      <div v-if="nodeInfoDetails[shapeId]" class="node-details-info">
        <div class="details_header">{{ nodeInfoDetails[shapeId].name }}</div>
        <div class="details_label">审批人员</div>
        <div class="details_value">
          <ATag color="arcoblue">{{ nodeInfoDetails[shapeId].approver }}</ATag>
        </div>
        <div class="details_label">节点状态</div>
        <div class="details_value">
          <ATag :color="processStateStyleMap[nodeInfoDetails[shapeId].status]">{{
            nodeInfoDetails[shapeId].statusName
          }}</ATag>
        </div>
        <div class="details_label">开始时间</div>
        <div class="details_value">{{ nodeInfoDetails[shapeId].startDate }}</div>
        <div class="details_label">结束时间</div>
        <div class="details_value">{{
          nodeInfoDetails[shapeId].status === 'processing' ? '-' : nodeInfoDetails[shapeId].endDate
        }}</div>
        <div class="details_label">审批耗时</div>
        <div class="details_value">{{
          nodeInfoDetails[shapeId].status === 'processing' ? '-' : nodeInfoDetails[shapeId].duration
        }}</div>
      </div>
    </ASpin>
  </tippy-popover>
</template>

3. PresetViewer

该组件为包含预设信息显示以及流程图展示的组件。

image-20240519160453744

该组件接受两个参数 procInstIdmodelKey,用来查询流程图与节点详情;对外暴露一个 reloadViewerXML,用来重新加载流程图 xml。

组件内部的弹窗内容显示,通过 DetailFieldMaps.ts 进行抽离。

在 V6.2.1 中,针对用户任务节点做了特殊处理。

DetailFieldMaps 信息数据处理

在这个文件中,主要包含两个 静态数据SPECIAL_TASK_TYPESSTATIC_TASK_TYPES,其中 SPECIAL_TASK_TYPES 依赖通过该节点信息向后端请求到的信息数据,而 STATIC_TASK_TYPES 则直接从当前节点中获取数据。

两者分别对应 SPECIAL_TASK_ATTR_KEYSSTATIC_TASK_ATTR_KEYS 两个常量,用来负责控制弹窗中的具体数据显示。

SPECIAL_TASK_ATTR_KEYSSTATIC_TASK_ATTR_KEYS 内部都有对应的 ts 声明,在修改 SPECIAL_TASK_TYPESSTATIC_TASK_TYPES 时会同步推导新的类型进行 lint 提示。

SPECIAL_TASK_ATTR_KEYSSTATIC_TASK_ATTR_KEYS 对应的属性配置对象中,都包含相同字段:headerattrs,这里对这些属性进行说明:

SPECIAL_TASK_ATTR_KEYS 中:

  • header 显示在最顶部的信息,不会进行国际化处理。文字默认加粗放大。
  • attrs 需要显示的属性字段数组,通过遍历该数组生成信息列表。其中信息 label 默认使用该数组元素值,需要在 i18n 中有对应配置;信息 value 部分默认后端返回信息的 label 对应属性值。
  • formatters 可选属性,用来配置 attrs 中直接显示时不满足要求的情况;数据是由 attrs 中的部分数组元素组成的 formatter 函数对象,每个 formatter 函数参数都是完整的信息数据对象。
  • useTag:可选属性,用来配置 attrs 中那些属性需要用 Tag 组件包装;数据是由 attrs 中的部分数组元素组成的对象,对象属性值可以是 arco-design-vue/tag 支持的 color 参数类型,也可以是一个返回 color 类型的函数

STATIC_TASK_ATTR_KEYS 中:

  • headerattrs 与上述一致
  • loading 默认为 false,用来显示弹出层的加载状态
  • values:由 attrs 中的数组元素组成的对象,作为显示信息 value 显示;由于这部分的处理不需要依赖后台请求,数据状态完全可控,所以这里不需要 formatters 配置。
  • tags:由 attrs 中的数组部分元素组成的对象,值为 arco-design-vue/tag 支持的 color 参数类型。

使用组件方式引用时,这部分数据可以根据业务需求进行二次开发,以满足实际业务。

4.Mocker

与 Viewer 不同的是,Mocker 本身与外部没有什么特殊交互,只有一个 createNewProcess 方法用来更新 xml 流程图,以及一个 setSequenceFlows 用来设置 互斥网关 的默认流转路径。

使用方式与 Viewer 类似,参照 packages/bpmn-viewer/src/App.vue:

<script setup lang="ts">
  import type { ComponentInstance } from 'vue'
  import type Viewer from 'bpmn-js/lib/Viewer'
  import BpmnMocker from '@/components/Mocker/index.vue'
  import {
    getCustomFlowSequenceFlows,
    getHighLightedNodeVoByProcessInstanceId
  } from '@/api/bpmn'
  import { Message } from '@arco-design/web-vue'

  const mocker = ref<ComponentInstance<typeof BpmnMocker>>()

  const mockerXmlLoading = ref(false)

  const reloadMockerXML = async () => {
    try {
      mockerXmlLoading.value = true
      // @ts-expect-error
      const { data, success, msg } = await getHighLightedNodeVoByProcessInstanceId(processId.value)
      if (success) {
        const { modelXml } = data || {}
        if (!modelXml) return

        await mocker.value?.createNewProcess(modelXml)

        await getDefaultFlows()
      } else {
        Message.error(msg)
      }
    } finally {
      mockerXmlLoading.value = false
    }
  }

  const getDefaultFlows = async () => {
    try {
      // @ts-expect-error
      const { data, success, msg } = await getCustomFlowSequenceFlows({
        dataJson: '{}',
        companyId: '0',
        deptId: '0',
        userCode: '12',
        procInstId: processId.value,
        modelKey: processId.value
      })
      if (success) {
        mocker.value?.setSequenceFlows(data as string[])
      } else {
        Message.error(msg)
      }
    } catch (e) {
      console.error(e)
    }
  }
</script>

<template>
  <ASpace align="center">
    <AButton type="primary" @click="reloadMockerXML">加载Mocker流程图</AButton>
  </ASpace>
  <ADivider />
  <BpmnMocker ref="mocker" :loading="mockerXmlLoading" />
</template>

image-20240519162542839

三、使用方式

具体使用可以参考 Bpmn Designer 的组件化使用方式。

如果使用的不是 @arco-design/web-vue 作为 UI 组件库的话,建议将 vite.lib.config.tsbuild.rollupOptions.external 内的 @arco-design/web-vue 移除。

四、组件 API

1. TippyPopover

props 参数

nametypedescdefaultremark
targetstring | HTMLElement触发显示的目标元素或者目标元素id-
selectorParentHTMLElementtarget 的祖先元素,用来限制 target 的查找范围documentv6.2.1 新增参数
appendToBodyboolean是否插入到 body 标签true
theme'default' | 'light' | 'light-border' | 'material' | 'translucent'弹出窗口样式'default'
arrowboolean | string控制箭头显示或者自定义的箭头样式true
delaynumber | [number, number]弹窗显示与消失的延迟时间0
durationnumber | [number, number]动画整体的持续时间[300, 250]
followCursorboolean | 'horizontal' | 'vertical' | 'initial'确定箭头是否跟随用户的鼠标光标false
hideOnClickboolean | 'toggle'确定是否在点击外部时隐藏true
inertiaboolean是否允许自定义不同状态的动画样式false
interactiveboolean确定弹窗内是否会存在具有可交互的内容,以便鼠标选停在弹窗内不会隐藏弹窗true
interactiveBordernumber用于设置弹窗周围的不可见边框大小2
interactiveDebouncenumber用于设置鼠标在弹窗内移入/移出时的防抖时间0
maxWidthnumber | string弹窗最大宽度,可以设置字符串来配置 vw 之类的宽度350
offset[number, number]设置箭头部分的最大偏移量,详见:popper docs[0, 10]
zIndexnumber指定插入到文档流中的zindex值2000
placementPopper.Placement弹窗默认显示位置'top'
animationstring | boolean默认动画样式,或者禁止动画'scale'
triggerstring触发弹窗显示的事件,多个事件以空格分开'mouseenter click'
triggerTargetElement | [Element, Element] | null添加触发事件侦听器的元素。允许您将 Tippy 的位置与其触发源分开null

slots 插槽

namedescparams
default默认插槽-

2. BpmnViewer

props 参数

nametypedescdefault
xmlstring默认初始 xml-
loadingboolean画布的加载状态false

events 事件

nameparamsdesc
viewer-initbpmnViewer: Viewerbpmn-js 的 Viewer 实例对象
viewer-destroybpmnViewer: undefined此时默认已经是 undefined
element-hoverelement: BpmnElementbpmn-js 中的元素实例对象,包含连线、节点、Label 等

methods 方法

nameparamsdescreturns
createNewProcessxml: string新的 xml 字符串-
setPassedNodesids: string[]为指定节点添加 已处理 状态的样式-
setActiveNodesids: string[]为指定节点添加 当前待处理 状态的样式-
setProcessingMarkerids: string[]为指定节点添加 已处理 状态的 css 类名-
getModelerbpmnViewer: Viewer获取当前的 viewer 实例

3. PresetViewer

props 参数

nametypedescdefault
procInstIdstring流程 id-
theme'dark' | 'light'初始颜色主题'light'
local'zh_CN' | 'en_US'初始国际化于语言'zh_CN'

methods 方法

nameparamsdescreturns
reloadViewerXMLprocessId: string传入新的流程id,重新查询 xml 与默认配置生成新的预览-
toggleTheme-切换主题
toggleLanglang: 'zh_CN' | 'en_US'切换语言

4. Mocker

props 参数

nametypedescdefault
xmlstring默认初始 xml-
loadingboolean画布的加载状态false
theme'dark' | 'light'初始颜色主题'light'
local'zh_CN' | 'en_US'初始国际化于语言'zh_CN'

methods 方法

nameparamsdescreturns
createNewProcessxml: string新的 xml 字符串-
setSequenceFlowsids: string[]设置 互斥网关 的默认流转路径-
toggleTheme-切换主题
toggleLanglang: 'zh_CN' | 'en_US'切换语言