|
@@ -1,664 +1,374 @@
|
|
|
<template>
|
|
|
- <div class="my-process-designer">
|
|
|
- <div class="my-process-designer__container">
|
|
|
- <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div>
|
|
|
+ <div class="process-viewer">
|
|
|
+ <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
|
|
|
+ <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 -->
|
|
|
+ <defs ref="customDefs">
|
|
|
+ <marker
|
|
|
+ id="sequenceflow-end-white-success"
|
|
|
+ viewBox="0 0 20 20"
|
|
|
+ refX="11"
|
|
|
+ refY="10"
|
|
|
+ markerWidth="10"
|
|
|
+ markerHeight="10"
|
|
|
+ orient="auto"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ class="success-arrow"
|
|
|
+ d="M 1 5 L 11 10 L 1 15 Z"
|
|
|
+ style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
|
|
|
+ />
|
|
|
+ </marker>
|
|
|
+ <marker
|
|
|
+ id="conditional-flow-marker-white-success"
|
|
|
+ viewBox="0 0 20 20"
|
|
|
+ refX="-1"
|
|
|
+ refY="10"
|
|
|
+ markerWidth="10"
|
|
|
+ markerHeight="10"
|
|
|
+ orient="auto"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ class="success-conditional"
|
|
|
+ d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
|
|
|
+ style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
|
|
|
+ />
|
|
|
+ </marker>
|
|
|
+ </defs>
|
|
|
+
|
|
|
+ <!-- 审批记录 -->
|
|
|
+ <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
|
|
|
+ <el-row>
|
|
|
+ <el-table
|
|
|
+ :data="selectTasks"
|
|
|
+ size="small"
|
|
|
+ border
|
|
|
+ header-cell-class-name="table-header-gray"
|
|
|
+ >
|
|
|
+ <el-table-column
|
|
|
+ label="序号"
|
|
|
+ header-align="center"
|
|
|
+ align="center"
|
|
|
+ type="index"
|
|
|
+ width="50"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ label="审批人"
|
|
|
+ prop="assigneeUser.nickname"
|
|
|
+ min-width="100"
|
|
|
+ align="center"
|
|
|
+ v-if="selectActivityType === 'bpmn:UserTask'"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ label="发起人"
|
|
|
+ prop="assigneeUser.nickname"
|
|
|
+ min-width="100"
|
|
|
+ align="center"
|
|
|
+ v-else
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ label="部门"
|
|
|
+ prop="assigneeUser.deptName"
|
|
|
+ min-width="100"
|
|
|
+ align="center"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ :formatter="dateFormatter"
|
|
|
+ align="center"
|
|
|
+ label="开始时间"
|
|
|
+ prop="createTime"
|
|
|
+ min-width="140"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ :formatter="dateFormatter"
|
|
|
+ align="center"
|
|
|
+ label="结束时间"
|
|
|
+ prop="endTime"
|
|
|
+ min-width="140"
|
|
|
+ />
|
|
|
+ <el-table-column align="center" label="审批状态" prop="status" min-width="90">
|
|
|
+ <template #default="scope">
|
|
|
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ align="center"
|
|
|
+ label="审批建议"
|
|
|
+ prop="reason"
|
|
|
+ min-width="120"
|
|
|
+ v-if="selectActivityType === 'bpmn:UserTask'"
|
|
|
+ />
|
|
|
+ <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatPast2(scope.row.durationInMillis) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-row>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- Zoom:放大、缩小 -->
|
|
|
+ <div style="position: absolute; top: 0; left: 0; width: 100%">
|
|
|
+ <el-row type="flex" justify="end">
|
|
|
+ <el-button-group key="scale-control" size="default">
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ :plain="true"
|
|
|
+ :disabled="defaultZoom <= 0.3"
|
|
|
+ :icon="ZoomOut"
|
|
|
+ @click="processZoomOut()"
|
|
|
+ />
|
|
|
+ <el-button size="default" style="width: 90px">
|
|
|
+ {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ :plain="true"
|
|
|
+ :disabled="defaultZoom >= 3.9"
|
|
|
+ :icon="ZoomIn"
|
|
|
+ @click="processZoomIn()"
|
|
|
+ />
|
|
|
+ <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
|
|
|
+ </el-button-group>
|
|
|
+ </el-row>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
+import '../theme/index.scss'
|
|
|
import BpmnViewer from 'bpmn-js/lib/Viewer'
|
|
|
-import DefaultEmptyXML from './plugins/defaultEmpty'
|
|
|
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
|
|
-import { formatDate } from '@/utils/formatTime'
|
|
|
-import { isEmpty } from '@/utils/is'
|
|
|
-
|
|
|
-defineOptions({ name: 'MyProcessViewer' })
|
|
|
+import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
|
|
|
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
|
|
|
+import { DICT_TYPE } from '@/utils/dict'
|
|
|
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
|
|
|
+import { BpmProcessInstanceStatus } from '@/utils/constants'
|
|
|
|
|
|
const props = defineProps({
|
|
|
- value: {
|
|
|
- // BPMN XML 字符串
|
|
|
- type: String,
|
|
|
- default: ''
|
|
|
- },
|
|
|
- prefix: {
|
|
|
- // 使用哪个引擎
|
|
|
+ xml: {
|
|
|
type: String,
|
|
|
- default: 'camunda'
|
|
|
+ required: true
|
|
|
},
|
|
|
- activityData: {
|
|
|
- // 活动的数据。传递时,可高亮流程
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
- processInstanceData: {
|
|
|
- // 流程实例的数据。传递时,可展示流程发起人等信息
|
|
|
+ view: {
|
|
|
type: Object,
|
|
|
- default: () => {}
|
|
|
- },
|
|
|
- taskData: {
|
|
|
- // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
+ require: true
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-provide('configGlobal', props)
|
|
|
+const processCanvas = ref()
|
|
|
+const bpmnViewer = ref<BpmnViewer | null>(null)
|
|
|
+const customDefs = ref()
|
|
|
+const defaultZoom = ref(1) // 默认缩放比例
|
|
|
+const isLoading = ref(false) // 是否加载中
|
|
|
|
|
|
-const emit = defineEmits(['destroy'])
|
|
|
+const processInstance = ref<any>({}) // 流程实例
|
|
|
+const tasks = ref([]) // 流程任务
|
|
|
|
|
|
-let bpmnModeler
|
|
|
+const dialogVisible = ref(false) // 弹窗可见性
|
|
|
+const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
|
|
|
+const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号
|
|
|
+const selectTasks = ref<any[]>([]) // 选中的任务数组
|
|
|
|
|
|
-const xml = ref('')
|
|
|
-const activityLists = ref<any[]>([])
|
|
|
-const processInstance = ref<any>(undefined)
|
|
|
-const taskList = ref<any[]>([])
|
|
|
-const bpmnCanvas = ref()
|
|
|
-// const element = ref()
|
|
|
-const elementOverlayIds = ref<any>(null)
|
|
|
-const overlays = ref<any>(null)
|
|
|
+/** Zoom:恢复 */
|
|
|
+const processReZoom = () => {
|
|
|
+ defaultZoom.value = 1
|
|
|
+ bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
|
|
|
+}
|
|
|
|
|
|
-const initBpmnModeler = () => {
|
|
|
- if (bpmnModeler) return
|
|
|
- bpmnModeler = new BpmnViewer({
|
|
|
- container: bpmnCanvas.value,
|
|
|
- bpmnRenderer: {}
|
|
|
- })
|
|
|
+/** Zoom:放大 */
|
|
|
+const processZoomIn = (zoomStep = 0.1) => {
|
|
|
+ let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
|
|
|
+ if (newZoom > 4) {
|
|
|
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
|
|
|
+ }
|
|
|
+ defaultZoom.value = newZoom
|
|
|
+ bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
|
|
|
}
|
|
|
|
|
|
-/* 创建新的流程图 */
|
|
|
-const createNewDiagram = async (xml) => {
|
|
|
- // 将字符串转换成图显示出来
|
|
|
- let newId = `Process_${new Date().getTime()}`
|
|
|
- let newName = `业务流程_${new Date().getTime()}`
|
|
|
- let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
|
|
|
- try {
|
|
|
- let { warnings } = await bpmnModeler.importXML(xmlString)
|
|
|
- if (warnings && warnings.length) {
|
|
|
- warnings.forEach((warn) => console.warn(warn))
|
|
|
- }
|
|
|
- // 高亮流程图
|
|
|
- await highlightDiagram()
|
|
|
- const canvas = bpmnModeler.get('canvas')
|
|
|
- canvas.zoom('fit-viewport', 'auto')
|
|
|
- } catch (e) {
|
|
|
- console.error(e)
|
|
|
- // console.error(`[Process Designer Warn]: ${e?.message || e}`);
|
|
|
+/** Zoom:缩小 */
|
|
|
+const processZoomOut = (zoomStep = 0.1) => {
|
|
|
+ let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
|
|
|
+ if (newZoom < 0.2) {
|
|
|
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
|
|
|
}
|
|
|
+ defaultZoom.value = newZoom
|
|
|
+ bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
|
|
|
}
|
|
|
|
|
|
-/* 高亮流程图 */
|
|
|
-// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
|
|
|
-const highlightDiagram = async () => {
|
|
|
- const activityList = activityLists.value
|
|
|
- if (activityList.length === 0) {
|
|
|
- return
|
|
|
+/** 流程图预览清空 */
|
|
|
+const clearViewer = () => {
|
|
|
+ if (processCanvas.value) {
|
|
|
+ processCanvas.value.innerHTML = ''
|
|
|
}
|
|
|
- // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
|
|
|
- // 再次基础上,增加不同审批结果的颜色等等
|
|
|
- let canvas = bpmnModeler.get('canvas')
|
|
|
- let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务
|
|
|
- let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务
|
|
|
- let findProcessTask = false //是否已经高亮了进行中的任务
|
|
|
- //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
|
|
|
- let removeTaskDefinitionKeyList = []
|
|
|
- // debugger
|
|
|
- bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {
|
|
|
- let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
|
|
|
- if (!activity) {
|
|
|
- return
|
|
|
- }
|
|
|
- if (n.$type === 'bpmn:UserTask') {
|
|
|
- // 用户任务
|
|
|
- // 处理用户任务的高亮
|
|
|
- const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId
|
|
|
- if (!task) {
|
|
|
- return
|
|
|
- }
|
|
|
- // 进行中的任务已经高亮过了,则不高亮后面的任务了
|
|
|
- if (findProcessTask) {
|
|
|
- removeTaskDefinitionKeyList.push(n.id)
|
|
|
- return
|
|
|
- }
|
|
|
- // 高亮任务
|
|
|
- canvas.addMarker(n.id, getResultCss(task.status))
|
|
|
- //标记是否高亮了进行中任务
|
|
|
- if (task.status === 1) {
|
|
|
- findProcessTask = true
|
|
|
- }
|
|
|
- // 如果非通过,就不走后面的线条了
|
|
|
- if (task.status !== 2) {
|
|
|
- return
|
|
|
- }
|
|
|
- // 处理 outgoing 出线
|
|
|
- const outgoing = getActivityOutgoing(activity)
|
|
|
- outgoing?.forEach((nn: any) => {
|
|
|
- // debugger
|
|
|
- let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
|
|
|
- // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
|
|
|
- if (targetActivity) {
|
|
|
- canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')
|
|
|
- } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
|
|
|
- // TODO 芋艿:这个流程,暂时没走到过
|
|
|
- canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')
|
|
|
- canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')
|
|
|
- } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
|
|
|
- // TODO 芋艿:这个流程,暂时没走到过
|
|
|
- if (!todoActivity && endActivity.key === n.id) {
|
|
|
- canvas.addMarker(nn.id, 'highlight')
|
|
|
- canvas.addMarker(nn.targetRef.id, 'highlight')
|
|
|
- }
|
|
|
- if (!activity.endTime) {
|
|
|
- canvas.addMarker(nn.id, 'highlight-todo')
|
|
|
- canvas.addMarker(nn.targetRef.id, 'highlight-todo')
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
- } else if (n.$type === 'bpmn:ExclusiveGateway') {
|
|
|
- // 排它网关
|
|
|
- // 设置【bpmn:ExclusiveGateway】排它网关的高亮
|
|
|
- canvas.addMarker(n.id, getActivityHighlightCss(activity))
|
|
|
- // 查找需要高亮的连线
|
|
|
- let matchNN: any = undefined
|
|
|
- let matchActivity: any = undefined
|
|
|
- n.outgoing?.forEach((nn: any) => {
|
|
|
- let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
|
|
|
- if (!targetActivity) {
|
|
|
- return
|
|
|
- }
|
|
|
- // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
|
|
|
- // 1. 一个是 UserTask => EndEvent
|
|
|
- // 2. 一个是 EndEvent
|
|
|
- // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
|
|
|
- // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
|
|
|
- if (!matchActivity || matchActivity.type === 'endEvent') {
|
|
|
- matchNN = nn
|
|
|
- matchActivity = targetActivity
|
|
|
- }
|
|
|
- })
|
|
|
- if (matchNN && matchActivity) {
|
|
|
- canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
|
|
|
- }
|
|
|
- } else if (n.$type === 'bpmn:ParallelGateway') {
|
|
|
- // 并行网关
|
|
|
- // 设置【bpmn:ParallelGateway】并行网关的高亮
|
|
|
- canvas.addMarker(n.id, getActivityHighlightCss(activity))
|
|
|
- n.outgoing?.forEach((nn: any) => {
|
|
|
- // 获得连线是否有指向目标。如果有,则进行高亮
|
|
|
- const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
|
|
|
- if (targetActivity) {
|
|
|
- canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线
|
|
|
- // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
|
|
|
- canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))
|
|
|
- }
|
|
|
- })
|
|
|
- } else if (n.$type === 'bpmn:StartEvent') {
|
|
|
- // 开始节点
|
|
|
- canvas.addMarker(n.id, 'highlight')
|
|
|
- n.outgoing?.forEach((nn) => {
|
|
|
- // outgoing 例如说【bpmn:SequenceFlow】连线
|
|
|
- // 获得连线是否有指向目标。如果有,则进行高亮
|
|
|
- let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
|
|
|
- if (targetActivity) {
|
|
|
- canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线
|
|
|
- canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)
|
|
|
- }
|
|
|
- })
|
|
|
- } else if (n.$type === 'bpmn:EndEvent') {
|
|
|
- // 结束节点
|
|
|
- if (!processInstance.value || processInstance.value.status === 1) {
|
|
|
- return
|
|
|
- }
|
|
|
- canvas.addMarker(n.id, getResultCss(processInstance.value.status))
|
|
|
- } else if (n.$type === 'bpmn:ServiceTask') {
|
|
|
- //服务任务
|
|
|
- if (activity.startTime > 0 && activity.endTime === 0) {
|
|
|
- //进入执行,标识进行色
|
|
|
- canvas.addMarker(n.id, getResultCss(1))
|
|
|
- }
|
|
|
- if (activity.endTime > 0) {
|
|
|
- // 执行完成,节点标识完成色, 所有outgoing标识完成色。
|
|
|
- canvas.addMarker(n.id, getResultCss(2))
|
|
|
- const outgoing = getActivityOutgoing(activity)
|
|
|
- outgoing?.forEach((out) => {
|
|
|
- canvas.addMarker(out.id, getResultCss(2))
|
|
|
- })
|
|
|
- }
|
|
|
- } else if (n.$type === 'bpmn:SequenceFlow') {
|
|
|
- let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
|
|
|
- if (targetActivity) {
|
|
|
- canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
- if (!isEmpty(removeTaskDefinitionKeyList)) {
|
|
|
- taskList.value = taskList.value.filter(
|
|
|
- (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
|
|
|
- )
|
|
|
+ if (bpmnViewer.value) {
|
|
|
+ bpmnViewer.value.destroy()
|
|
|
}
|
|
|
+ bpmnViewer.value = null
|
|
|
}
|
|
|
|
|
|
-const getActivityHighlightCss = (activity) => {
|
|
|
- return activity.endTime ? 'highlight' : 'highlight-todo'
|
|
|
+/** 添加自定义箭头 */
|
|
|
+// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
|
|
|
+const addCustomDefs = () => {
|
|
|
+ if (!bpmnViewer.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const canvas = bpmnViewer.value?.get('canvas')
|
|
|
+ const svg = canvas?._svg
|
|
|
+ svg.appendChild(customDefs.value)
|
|
|
}
|
|
|
|
|
|
-const getResultCss = (status) => {
|
|
|
- if (status === 1) {
|
|
|
- // 审批中
|
|
|
- return 'highlight-todo'
|
|
|
- } else if (status === 2) {
|
|
|
- // 已通过
|
|
|
- return 'highlight'
|
|
|
- } else if (status === 3) {
|
|
|
- // 不通过
|
|
|
- return 'highlight-reject'
|
|
|
- } else if (status === 4) {
|
|
|
- // 已取消
|
|
|
- return 'highlight-cancel'
|
|
|
- } else if (status === 5) {
|
|
|
- // 退回
|
|
|
- return 'highlight-return'
|
|
|
- } else if (status === 6) {
|
|
|
- // 委派
|
|
|
- return 'highlight-todo'
|
|
|
- } else if (status === 7) {
|
|
|
- // 审批通过中
|
|
|
- return 'highlight-todo'
|
|
|
- } else if (status === 0) {
|
|
|
- // 待审批
|
|
|
- return 'highlight-todo'
|
|
|
+/** 节点选中 */
|
|
|
+const onSelectElement = (element: any) => {
|
|
|
+ // 清空原选中
|
|
|
+ selectActivityType.value = undefined
|
|
|
+ dialogTitle.value = undefined
|
|
|
+ if (!element || !processInstance.value?.id) {
|
|
|
+ return
|
|
|
}
|
|
|
- return ''
|
|
|
-}
|
|
|
|
|
|
-const getActivityOutgoing = (activity) => {
|
|
|
- // 如果有 outgoing,则直接使用它
|
|
|
- if (activity.outgoing && activity.outgoing.length > 0) {
|
|
|
- return activity.outgoing
|
|
|
+ // UserTask 的情况
|
|
|
+ const activityType = element.type
|
|
|
+ selectActivityType.value = activityType
|
|
|
+ if (activityType === 'bpmn:UserTask') {
|
|
|
+ dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
|
|
|
+ selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
|
|
|
+ dialogVisible.value = true
|
|
|
+ } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
|
|
|
+ dialogTitle.value = '审批信息'
|
|
|
+ selectTasks.value = [
|
|
|
+ {
|
|
|
+ assigneeUser: processInstance.value.startUser,
|
|
|
+ createTime: processInstance.value.startTime,
|
|
|
+ endTime: processInstance.value.endTime,
|
|
|
+ status: processInstance.value.status,
|
|
|
+ durationInMillis: processInstance.value.durationInMillis
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ dialogVisible.value = true
|
|
|
}
|
|
|
- // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
|
|
|
- const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements
|
|
|
- const outgoing: any[] = []
|
|
|
- flowElements.forEach((item: any) => {
|
|
|
- if (item.$type !== 'bpmn:SequenceFlow') {
|
|
|
- return
|
|
|
- }
|
|
|
- if (item.sourceRef.id === activity.key) {
|
|
|
- outgoing.push(item)
|
|
|
- }
|
|
|
- })
|
|
|
- return outgoing
|
|
|
}
|
|
|
-const initModelListeners = () => {
|
|
|
- const EventBus = bpmnModeler.get('eventBus')
|
|
|
- // 注册需要的监听事件
|
|
|
- EventBus.on('element.hover', function (eventObj) {
|
|
|
- let element = eventObj ? eventObj.element : null
|
|
|
- elementHover(element)
|
|
|
- })
|
|
|
- EventBus.on('element.out', function (eventObj) {
|
|
|
- let element = eventObj ? eventObj.element : null
|
|
|
- elementOut(element)
|
|
|
- })
|
|
|
+
|
|
|
+/** 初始化 BPMN 视图 */
|
|
|
+const importXML = async (xml: string) => {
|
|
|
+ // 清空流程图
|
|
|
+ clearViewer()
|
|
|
+
|
|
|
+ // 初始化流程图
|
|
|
+ if (xml != null && xml !== '') {
|
|
|
+ try {
|
|
|
+ bpmnViewer.value = new BpmnViewer({
|
|
|
+ additionalModules: [MoveCanvasModule],
|
|
|
+ container: processCanvas.value
|
|
|
+ })
|
|
|
+ // 增加点击事件
|
|
|
+ bpmnViewer.value.on('element.click', ({ element }) => {
|
|
|
+ onSelectElement(element)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 初始化 BPMN 视图
|
|
|
+ isLoading.value = true
|
|
|
+ await bpmnViewer.value.importXML(xml)
|
|
|
+ // 自定义成功的箭头
|
|
|
+ addCustomDefs()
|
|
|
+ } catch (e) {
|
|
|
+ clearViewer()
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ // 高亮流程
|
|
|
+ setProcessStatus(props.view)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-// 流程图的元素被 hover
|
|
|
-const elementHover = (element) => {
|
|
|
- element.value = element
|
|
|
- !elementOverlayIds.value && (elementOverlayIds.value = {})
|
|
|
- !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
|
|
|
- // 展示信息
|
|
|
- // console.log(activityLists.value, 'activityLists.value')
|
|
|
- // console.log(element.value, 'element.value')
|
|
|
- const activity = activityLists.value.find((m) => m.key === element.value.id)
|
|
|
- // console.log(activity, 'activityactivityactivityactivity')
|
|
|
- if (!activity) {
|
|
|
+
|
|
|
+/** 高亮流程 */
|
|
|
+const setProcessStatus = (view: any) => {
|
|
|
+ // 设置相关变量
|
|
|
+ processInstance.value = view.processInstance
|
|
|
+ tasks.value = view.tasks
|
|
|
+ if (isLoading.value || !processInstance.value || !bpmnViewer.value) {
|
|
|
return
|
|
|
}
|
|
|
- if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') {
|
|
|
- let html = `<div class="element-overlays">
|
|
|
- <p>Elemet id: ${element.value.id}</p>
|
|
|
- <p>Elemet type: ${element.value.type}</p>
|
|
|
- </div>` // 默认值
|
|
|
- if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
|
|
|
- html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
|
|
|
- <p>部门:${processInstance.value.startUser.deptName}</p>
|
|
|
- <p>创建时间:${formatDate(processInstance.value.createTime)}`
|
|
|
- } else if (element.value.type === 'bpmn:UserTask') {
|
|
|
- let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
|
|
|
- if (!task) {
|
|
|
- return
|
|
|
- }
|
|
|
- let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
|
|
|
- let dataResult = ''
|
|
|
- optionData.forEach((element) => {
|
|
|
- if (element.value == task.status) {
|
|
|
- dataResult = element.label
|
|
|
+ const {
|
|
|
+ unfinishedTaskActivityIds,
|
|
|
+ finishedTaskActivityIds,
|
|
|
+ finishedSequenceFlowActivityIds,
|
|
|
+ rejectedTaskActivityIds
|
|
|
+ } = view
|
|
|
+ const canvas = bpmnViewer.value.get('canvas')
|
|
|
+ const elementRegistry = bpmnViewer.value.get('elementRegistry')
|
|
|
+
|
|
|
+ // 已完成节点
|
|
|
+ if (Array.isArray(finishedSequenceFlowActivityIds)) {
|
|
|
+ finishedSequenceFlowActivityIds.forEach((item: any) => {
|
|
|
+ if (item != null) {
|
|
|
+ canvas.addMarker(item, 'success')
|
|
|
+ const element = elementRegistry.get(item)
|
|
|
+ const conditionExpression = element.businessObject.conditionExpression
|
|
|
+ if (conditionExpression) {
|
|
|
+ canvas.addMarker(item, 'condition-expression')
|
|
|
}
|
|
|
- })
|
|
|
- html = `<p>审批人:${task.assigneeUser.nickname}</p>
|
|
|
- <p>部门:${task.assigneeUser.deptName}</p>
|
|
|
- <p>结果:${dataResult}</p>
|
|
|
- <p>创建时间:${formatDate(task.createTime)}</p>`
|
|
|
- // html = `<p>审批人:${task.assigneeUser.nickname}</p>
|
|
|
- // <p>部门:${task.assigneeUser.deptName}</p>
|
|
|
- // <p>结果:${getIntDictOptions(
|
|
|
- // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
|
|
|
- // task.status
|
|
|
- // )}</p>
|
|
|
- // <p>创建时间:${formatDate(task.createTime)}</p>`
|
|
|
- if (task.endTime) {
|
|
|
- html += `<p>结束时间:${formatDate(task.endTime)}</p>`
|
|
|
- }
|
|
|
- if (task.reason) {
|
|
|
- html += `<p>审批建议:${task.reason}</p>`
|
|
|
}
|
|
|
- } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) {
|
|
|
- if (activity.startTime > 0) {
|
|
|
- html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
|
|
|
- }
|
|
|
- if (activity.endTime > 0) {
|
|
|
- html += `<p>结束时间:${formatDate(activity.endTime)}</p>`
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (Array.isArray(finishedTaskActivityIds)) {
|
|
|
+ finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未完成节点
|
|
|
+ if (Array.isArray(unfinishedTaskActivityIds)) {
|
|
|
+ unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 被拒绝节点
|
|
|
+ if (Array.isArray(rejectedTaskActivityIds)) {
|
|
|
+ rejectedTaskActivityIds.forEach((item: any) => {
|
|
|
+ if (item != null) {
|
|
|
+ canvas.addMarker(item, 'danger')
|
|
|
}
|
|
|
- console.log(html)
|
|
|
- } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
|
|
|
- let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
|
|
|
- let dataResult = ''
|
|
|
- optionData.forEach((element) => {
|
|
|
- if (element.value == processInstance.value.status) {
|
|
|
- dataResult = element.label
|
|
|
- }
|
|
|
- })
|
|
|
- html = `<p>结果:${dataResult}</p>`
|
|
|
- // html = `<p>结果:${getIntDictOptions(
|
|
|
- // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
|
|
|
- // processInstance.value.status
|
|
|
- // )}</p>`
|
|
|
- if (processInstance.value.endTime) {
|
|
|
- html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
|
|
|
+ if (
|
|
|
+ [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
|
|
|
+ processInstance.value.status
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
|
|
|
+ endNodes.forEach((item: any) => {
|
|
|
+ canvas.removeMarker(item.id, 'success')
|
|
|
+ if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
|
|
|
+ canvas.addMarker(item.id, 'cancel')
|
|
|
+ } else {
|
|
|
+ canvas.addMarker(item.id, 'danger')
|
|
|
}
|
|
|
- }
|
|
|
- // console.log(html, 'html111111111111111')
|
|
|
- elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
|
|
|
- position: { left: 0, bottom: 0 },
|
|
|
- html: `<div class="element-overlays">${html}</div>`
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 流程图的元素被 out
|
|
|
-const elementOut = (element) => {
|
|
|
- toRaw(overlays.value).remove({ element })
|
|
|
- elementOverlayIds.value[element.id] = null
|
|
|
-}
|
|
|
+watch(
|
|
|
+ () => props.xml,
|
|
|
+ (newXml) => {
|
|
|
+ importXML(newXml)
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
|
|
|
+watch(
|
|
|
+ () => props.view,
|
|
|
+ (newView) => {
|
|
|
+ setProcessStatus(newView)
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+/** mounted:初始化 */
|
|
|
onMounted(() => {
|
|
|
- xml.value = props.value
|
|
|
- activityLists.value = props.activityData
|
|
|
- // 初始化
|
|
|
- initBpmnModeler()
|
|
|
- createNewDiagram(xml.value)
|
|
|
- // 初始模型的监听器
|
|
|
- initModelListeners()
|
|
|
+ importXML(props.xml)
|
|
|
+ setProcessStatus(props.view)
|
|
|
})
|
|
|
|
|
|
+/** unmount:销毁 */
|
|
|
onBeforeUnmount(() => {
|
|
|
- // this.$once('hook:beforeDestroy', () => {
|
|
|
- // })
|
|
|
- if (bpmnModeler) bpmnModeler.destroy()
|
|
|
- emit('destroy', bpmnModeler)
|
|
|
- bpmnModeler = null
|
|
|
+ clearViewer()
|
|
|
})
|
|
|
-
|
|
|
-watch(
|
|
|
- () => props.value,
|
|
|
- (newValue) => {
|
|
|
- xml.value = newValue
|
|
|
- createNewDiagram(xml.value)
|
|
|
- }
|
|
|
-)
|
|
|
-watch(
|
|
|
- () => props.activityData,
|
|
|
- (newActivityData) => {
|
|
|
- activityLists.value = newActivityData
|
|
|
- createNewDiagram(xml.value)
|
|
|
- }
|
|
|
-)
|
|
|
-watch(
|
|
|
- () => props.processInstanceData,
|
|
|
- (newProcessInstanceData) => {
|
|
|
- processInstance.value = newProcessInstanceData
|
|
|
- createNewDiagram(xml.value)
|
|
|
- }
|
|
|
-)
|
|
|
-watch(
|
|
|
- () => props.taskData,
|
|
|
- (newTaskListData) => {
|
|
|
- taskList.value = newTaskListData
|
|
|
- createNewDiagram(xml.value)
|
|
|
- }
|
|
|
-)
|
|
|
</script>
|
|
|
-
|
|
|
-<style lang="scss">
|
|
|
-/** 处理中 */
|
|
|
-.highlight-todo.djs-connection > .djs-visual > path {
|
|
|
- stroke: #1890ff !important;
|
|
|
- stroke-dasharray: 4px !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
|
|
|
- fill: #1890ff !important;
|
|
|
- stroke: #1890ff !important;
|
|
|
- stroke-dasharray: 4px !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-todo.djs-connection > .djs-visual > path) {
|
|
|
- stroke: #1890ff !important;
|
|
|
- stroke-dasharray: 4px !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
|
|
|
- fill: #1890ff !important;
|
|
|
- stroke: #1890ff !important;
|
|
|
- stroke-dasharray: 4px !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-/** 通过 */
|
|
|
-.highlight.djs-shape .djs-visual > :nth-child(1) {
|
|
|
- fill: green !important;
|
|
|
- stroke: green !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight.djs-shape .djs-visual > :nth-child(2) {
|
|
|
- fill: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight.djs-shape .djs-visual > path {
|
|
|
- fill: green !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight.djs-connection > .djs-visual > path {
|
|
|
- stroke: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
|
|
|
- fill: green !important; /* color elements as green */
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
|
|
|
- fill: green !important;
|
|
|
- stroke: green !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
|
|
|
- fill: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight.djs-shape .djs-visual > path) {
|
|
|
- fill: green !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight.djs-connection > .djs-visual > path) {
|
|
|
- stroke: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-.djs-element.highlight > .djs-visual > path {
|
|
|
- stroke: green !important;
|
|
|
-}
|
|
|
-
|
|
|
-/** 不通过 */
|
|
|
-.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
|
|
|
- fill: red !important;
|
|
|
- stroke: red !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
|
|
|
- fill: red !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-reject.djs-shape .djs-visual > path {
|
|
|
- fill: red !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: red !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-reject.djs-connection > .djs-visual > path {
|
|
|
- stroke: red !important;
|
|
|
- marker-end: url(#sequenceflow-end-white-success) !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
|
|
|
- fill: red !important; /* color elements as green */
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
|
|
|
- fill: red !important;
|
|
|
- stroke: red !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
|
|
|
- fill: red !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-reject.djs-shape .djs-visual > path) {
|
|
|
- fill: red !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: red !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-reject.djs-connection > .djs-visual > path) {
|
|
|
- stroke: red !important;
|
|
|
-}
|
|
|
-
|
|
|
-/** 已取消 */
|
|
|
-.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
|
|
|
- fill: grey !important;
|
|
|
- stroke: grey !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
|
|
|
- fill: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-cancel.djs-shape .djs-visual > path {
|
|
|
- fill: grey !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-cancel.djs-connection > .djs-visual > path {
|
|
|
- stroke: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
|
|
|
- fill: grey !important; /* color elements as green */
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
|
|
|
- fill: grey !important;
|
|
|
- stroke: grey !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
|
|
|
- fill: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-cancel.djs-shape .djs-visual > path) {
|
|
|
- fill: grey !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
|
|
|
- stroke: grey !important;
|
|
|
-}
|
|
|
-
|
|
|
-/** 回退 */
|
|
|
-.highlight-return.djs-shape .djs-visual > :nth-child(1) {
|
|
|
- fill: #e6a23c !important;
|
|
|
- stroke: #e6a23c !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-return.djs-shape .djs-visual > :nth-child(2) {
|
|
|
- fill: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-return.djs-shape .djs-visual > path {
|
|
|
- fill: #e6a23c !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-return.djs-connection > .djs-visual > path {
|
|
|
- stroke: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
|
|
|
- fill: #e6a23c !important; /* color elements as green */
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
|
|
|
- fill: #e6a23c !important;
|
|
|
- stroke: #e6a23c !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
|
|
|
- fill: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-return.djs-shape .djs-visual > path) {
|
|
|
- fill: #e6a23c !important;
|
|
|
- fill-opacity: 0.2 !important;
|
|
|
- stroke: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.highlight-return.djs-connection > .djs-visual > path) {
|
|
|
- stroke: #e6a23c !important;
|
|
|
-}
|
|
|
-
|
|
|
-.element-overlays {
|
|
|
- width: 200px;
|
|
|
- padding: 8px;
|
|
|
- color: #fafafa;
|
|
|
- background: rgb(0 0 0 / 60%);
|
|
|
- border-radius: 4px;
|
|
|
- box-sizing: border-box;
|
|
|
-}
|
|
|
-</style>
|