| | |
| | | <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="审批人" |
| | | min-width="100" |
| | | align="center" |
| | | v-if="selectActivityType === 'bpmn:UserTask'" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="发起人" |
| | | prop="assigneeUser.nickname" |
| | | min-width="100" |
| | | align="center" |
| | | v-else |
| | | /> |
| | | <el-table-column label="部门" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
| | | </template> |
| | | </el-table-column> |
| | | <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 字符串 |
| | | xml: { |
| | | type: String, |
| | | default: '' |
| | | required: true |
| | | }, |
| | | prefix: { |
| | | // 使用哪个引擎 |
| | | type: String, |
| | | default: 'camunda' |
| | | }, |
| | | 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) |
| | | |
| | | const initBpmnModeler = () => { |
| | | if (bpmnModeler) return |
| | | bpmnModeler = new BpmnViewer({ |
| | | container: bpmnCanvas.value, |
| | | bpmnRenderer: {} |
| | | }) |
| | | /** Zoom:恢复 */ |
| | | const processReZoom = () => { |
| | | defaultZoom.value = 1 |
| | | bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto') |
| | | } |
| | | |
| | | /* 创建新的流程图 */ |
| | | const createNewDiagram = async (xml) => { |
| | | // 将字符串转换成图显示出来 |
| | | let newId = `Process_${new Date().getTime()}` |
| | | let newName = `业务流程_${new Date().getTime()}` |
| | | let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix) |
| | | /** 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) |
| | | } |
| | | |
| | | /** 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) |
| | | } |
| | | |
| | | /** 流程图预览清空 */ |
| | | const clearViewer = () => { |
| | | if (processCanvas.value) { |
| | | processCanvas.value.innerHTML = '' |
| | | } |
| | | if (bpmnViewer.value) { |
| | | bpmnViewer.value.destroy() |
| | | } |
| | | bpmnViewer.value = null |
| | | } |
| | | |
| | | /** 添加自定义箭头 */ |
| | | // 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 onSelectElement = (element: any) => { |
| | | // 清空原选中 |
| | | selectActivityType.value = undefined |
| | | dialogTitle.value = undefined |
| | | if (!element || !processInstance.value?.id) { |
| | | return |
| | | } |
| | | |
| | | // 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 视图 */ |
| | | const importXML = async (xml: string) => { |
| | | // 清空流程图 |
| | | clearViewer() |
| | | |
| | | // 初始化流程图 |
| | | if (xml != null && xml !== '') { |
| | | 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') |
| | | 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) { |
| | | console.error(e) |
| | | // console.error(`[Process Designer Warn]: ${e?.message || e}`); |
| | | clearViewer() |
| | | } finally { |
| | | isLoading.value = false |
| | | // 高亮流程 |
| | | setProcessStatus(props.view) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 高亮流程图 */ |
| | | // TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html |
| | | const highlightDiagram = async () => { |
| | | const activityList = activityLists.value |
| | | if (activityList.length === 0) { |
| | | /** 高亮流程 */ |
| | | const setProcessStatus = (view: any) => { |
| | | // 设置相关变量 |
| | | if (!view || !view.processInstance) { |
| | | return |
| | | } |
| | | // 参考自 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) { |
| | | processInstance.value = view.processInstance |
| | | tasks.value = view.tasks |
| | | if (isLoading.value || !bpmnViewer.value) { |
| | | 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') |
| | | 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') |
| | | } |
| | | } |
| | | }) |
| | | } 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 (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') |
| | | } |
| | | }) |
| | | 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) |
| | | |
| | | // 特殊:处理 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') |
| | | } |
| | | } |
| | | |
| | | const getActivityHighlightCss = (activity) => { |
| | | return activity.endTime ? 'highlight' : 'highlight-todo' |
| | | } |
| | | |
| | | 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' |
| | | } |
| | | return '' |
| | | } |
| | | |
| | | const getActivityOutgoing = (activity) => { |
| | | // 如果有 outgoing,则直接使用它 |
| | | if (activity.outgoing && activity.outgoing.length > 0) { |
| | | return activity.outgoing |
| | | } |
| | | // 如果没有,则遍历获得起点为它的【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) |
| | | }) |
| | | } |
| | | // 流程图的元素被 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) { |
| | | 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 |
| | | } |
| | | }) |
| | | 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>` |
| | | } |
| | | 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>` |
| | | } |
| | | } |
| | | // 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> |