From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue |  379 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 379 insertions(+), 0 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
new file mode 100644
index 0000000..34a54c8
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -0,0 +1,379 @@
+<template>
+  <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 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({
+  xml: {
+    type: String,
+    required: true
+  },
+  view: {
+    type: Object,
+    require: true
+  }
+})
+
+const processCanvas = ref()
+const bpmnViewer = ref<BpmnViewer | null>(null)
+const customDefs = ref()
+const defaultZoom = ref(1) // 榛樿缂╂斁姣斾緥
+const isLoading = ref(false) // 鏄惁鍔犺浇涓�
+
+const processInstance = ref<any>({}) // 娴佺▼瀹炰緥
+const tasks = ref([]) // 娴佺▼浠诲姟
+
+const dialogVisible = ref(false) // 寮圭獥鍙鎬�
+const dialogTitle = ref<string | undefined>(undefined) // 寮圭獥鏍囬
+const selectActivityType = ref<string | undefined>(undefined) // 閫変腑 Task 鐨勬椿鍔ㄧ紪鍙�
+const selectTasks = ref<any[]>([]) // 閫変腑鐨勪换鍔℃暟缁�
+
+/** Zoom锛氭仮澶� */
+const processReZoom = () => {
+  defaultZoom.value = 1
+  bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
+}
+
+/** 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銆乵arker-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 {
+      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)
+    }
+  }
+}
+
+/** 楂樹寒娴佺▼ */
+const setProcessStatus = (view: any) => {
+  // 璁剧疆鐩稿叧鍙橀噺
+  if (!view || !view.processInstance) {
+    return
+  }
+  processInstance.value = view.processInstance
+  tasks.value = view.tasks
+  if (isLoading.value || !bpmnViewer.value) {
+    return
+  }
+  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')
+        }
+      }
+    })
+  }
+  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')
+      }
+    })
+  }
+
+  // 鐗规畩锛氬鐞� 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')
+      }
+    })
+  }
+}
+
+watch(
+  () => props.xml,
+  (newXml) => {
+    importXML(newXml)
+  },
+  { immediate: true }
+)
+
+watch(
+  () => props.view,
+  (newView) => {
+    setProcessStatus(newView)
+  },
+  { immediate: true }
+)
+
+/** mounted锛氬垵濮嬪寲 */
+onMounted(() => {
+  importXML(props.xml)
+  setProcessStatus(props.view)
+})
+
+/** unmount锛氶攢姣� */
+onBeforeUnmount(() => {
+  clearViewer()
+})
+</script>

--
Gitblit v1.8.0