From 19f59c243e8df97c8b9fd9dba4e758be8235d68b Mon Sep 17 00:00:00 2001
From: huang <1532065656@qq.com>
Date: 星期二, 25 十一月 2025 17:02:54 +0800
Subject: [PATCH] 添加卧转立扫码、卧转立、大车、大理片笼基础任务流转逻辑

---
 mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue |  482 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 465 insertions(+), 17 deletions(-)

diff --git a/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue b/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
index 0569fd3..1502b50 100644
--- a/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
+++ b/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -4,11 +4,34 @@
       <div>
         <h3>浠诲姟鎵ц鐩戞帶</h3>
         <p>瀹炴椂鏌ョ湅鏈�鏂扮殑澶氳澶囦换鍔�</p>
+        <p v-if="sseConnected" class="sse-status connected">
+          <el-icon><Connection /></el-icon>
+          瀹炴椂鐩戞帶宸茶繛鎺�
+        </p>
+        <p v-else class="sse-status disconnected">
+          <el-icon><Close /></el-icon>
+          瀹炴椂鐩戞帶鏈繛鎺�
+        </p>
       </div>
-      <el-button :loading="loading" @click="fetchTasks">
-        <el-icon><Refresh /></el-icon>
-        鍒锋柊
-      </el-button>
+      <div class="action-buttons">
+        <el-button :loading="loading" @click="fetchTasks">
+          <el-icon><Refresh /></el-icon>
+          鍒锋柊
+        </el-button>
+        <el-button
+          v-if="!sseConnected"
+          type="success"
+          @click="connectSSE"
+          :loading="sseConnecting"
+        >
+          <el-icon><VideoPlay /></el-icon>
+          寮�鍚疄鏃剁洃鎺�
+        </el-button>
+        <el-button v-else type="danger" @click="disconnectSSE">
+          <el-icon><VideoPause /></el-icon>
+          鍏抽棴瀹炴椂鐩戞帶
+        </el-button>
+      </div>
     </div>
 
     <el-table
@@ -17,17 +40,29 @@
       height="300"
       stripe
       @row-click="handleRowClick"
+      row-key="taskId"
     >
       <el-table-column prop="taskId" label="浠诲姟缂栧彿" min-width="160" />
       <el-table-column prop="groupId" label="璁惧缁処D" width="120" />
       <el-table-column prop="status" label="鐘舵��" width="120">
         <template #default="{ row }">
-          <el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
+          <el-tag :type="statusType(row.status)">
+            {{ getStatusLabel(row.status) }}
+          </el-tag>
         </template>
       </el-table-column>
-      <el-table-column prop="currentStep" label="杩涘害" width="120">
+      <el-table-column prop="currentStep" label="杩涘害" width="140">
         <template #default="{ row }">
-          {{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}
+          <div class="progress-cell">
+            <span>{{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}</span>
+            <el-progress
+              :percentage="getProgressPercentage(row)"
+              :status="getProgressStatus(row.status)"
+              :stroke-width="6"
+              :show-text="false"
+              style="margin-top: 4px;"
+            />
+          </div>
         </template>
       </el-table-column>
       <el-table-column label="寮�濮嬫椂闂�" min-width="160">
@@ -40,20 +75,61 @@
           {{ formatDateTime(row.endTime) }}
         </template>
       </el-table-column>
+      <el-table-column label="鎿嶄綔" width="120" fixed="right">
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            size="small"
+            @click.stop="handleRowClick(row)"
+          >
+            鏌ョ湅璇︽儏
+          </el-button>
+          <el-button
+            v-if="row.status === 'RUNNING'"
+            link
+            type="danger"
+            size="small"
+            @click.stop="handleCancelTask(row)"
+          >
+            鍙栨秷
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
 
-    <el-drawer v-model="drawerVisible" size="40%" title="浠诲姟姝ラ璇︽儏">
-      <el-timeline v-loading="stepsLoading" :reverse="false">
+    <el-drawer v-model="drawerVisible" size="40%" :title="`浠诲姟姝ラ璇︽儏 - ${currentTaskId || ''}`">
+      <div class="drawer-header" v-if="currentTask">
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="浠诲姟鐘舵��">
+            <el-tag :type="statusType(currentTask.status)">
+              {{ getStatusLabel(currentTask.status) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="杩涘害">
+            {{ currentTask.currentStep || 0 }} / {{ currentTask.totalSteps || 0 }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+      <el-timeline v-loading="stepsLoading" :reverse="false" style="margin-top: 20px;">
         <el-timeline-item
           v-for="step in steps"
           :key="step.id"
           :timestamp="formatDateTime(step.startTime) || '-'"
-          :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
+          :type="getStepTimelineType(step.status)"
         >
           <div class="step-title">{{ step.stepName }}</div>
-          <div class="step-desc">鐘舵�侊細{{ step.status }}</div>
+          <div class="step-desc">
+            <el-tag :type="getStepStatusType(step.status)" size="small">
+              {{ getStepStatusLabel(step.status) }}
+            </el-tag>
+          </div>
           <div class="step-desc">鑰楁椂锛歿{ formatDuration(step.durationMs) }}</div>
-          <div class="step-desc" v-if="step.errorMessage">
+          <div class="step-desc" v-if="step.retryCount > 0">
+            閲嶈瘯娆℃暟锛歿{ step.retryCount }}
+          </div>
+          <div class="step-desc error-message" v-if="step.errorMessage">
+            <el-icon><Warning /></el-icon>
             閿欒锛歿{ step.errorMessage }}
           </div>
         </el-timeline-item>
@@ -63,14 +139,25 @@
 </template>
 
 <script setup>
-import { onMounted, ref, watch } from 'vue'
-import { ElMessage } from 'element-plus'
-import { Refresh } from '@element-plus/icons-vue'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  Refresh,
+  Connection,
+  Close,
+  VideoPlay,
+  VideoPause,
+  Warning
+} from '@element-plus/icons-vue'
 import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
 
 const props = defineProps({
   groupId: {
     type: [String, Number],
+    default: null
+  },
+  taskId: {
+    type: String,
     default: null
   }
 })
@@ -81,6 +168,13 @@
 const stepsLoading = ref(false)
 const steps = ref([])
 const currentTaskId = ref(null)
+const currentTask = ref(null)
+
+// SSE鐩稿叧
+const sseConnected = ref(false)
+const sseConnecting = ref(false)
+let eventSource = null
+const baseURL = import.meta.env.VITE_API_BASE_URL || ''
 
 const fetchTasks = async () => {
   try {
@@ -98,8 +192,223 @@
   }
 }
 
+// SSE杩炴帴
+const connectSSE = () => {
+  if (eventSource) {
+    disconnectSSE()
+  }
+  
+  sseConnecting.value = true
+  try {
+    // 鏋勫缓SSE URL - 鍚庣鍙敮鎸� taskId 鍙傛暟锛屼笉鏀寔 groupId
+    let url = `${baseURL}/api/plcSend/task/notification/sse`
+    // 濡傛灉娌℃湁鎸囧畾 taskId锛屽垯鐩戝惉鎵�鏈変换鍔★紙涓嶄紶鍙傛暟锛�
+    if (props.taskId) {
+      url += `?taskId=${encodeURIComponent(props.taskId)}`
+    }
+    // 娉ㄦ剰锛氬悗绔笉鏀寔 groupId 鍙傛暟锛屽鏋滈渶瑕佺洃鍚煇涓粍鐨勬墍鏈変换鍔★紝
+    // 闇�瑕佸湪鍓嶇鏍规嵁 groupId 鑾峰彇浠诲姟鍒楄〃锛岀劧鍚庝负姣忎釜浠诲姟鍒涘缓杩炴帴
+    // 鎴栬�呬娇鐢ㄤ笉浼犲弬鏁扮殑鏂瑰紡鐩戝惉鎵�鏈変换鍔★紝鐒跺悗鍦ㄥ墠绔繃婊�
+
+    eventSource = new EventSource(url)
+
+    eventSource.onopen = () => {
+      sseConnected.value = true
+      sseConnecting.value = false
+      ElMessage.success('瀹炴椂鐩戞帶宸茶繛鎺�')
+    }
+
+    eventSource.onerror = (error) => {
+      console.error('SSE杩炴帴閿欒:', error)
+      sseConnected.value = false
+      sseConnecting.value = false
+      if (eventSource?.readyState === EventSource.CLOSED) {
+        ElMessage.warning('瀹炴椂鐩戞帶杩炴帴宸叉柇寮�')
+        // 灏濊瘯閲嶈繛
+        setTimeout(() => {
+          if (!sseConnected.value) {
+            connectSSE()
+          }
+        }, 3000)
+      }
+    }
+
+    // 鐩戝惉杩炴帴鎴愬姛浜嬩欢
+    eventSource.addEventListener('connected', (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        console.log('SSE杩炴帴鎴愬姛:', data)
+      } catch (error) {
+        console.error('瑙f瀽杩炴帴娑堟伅澶辫触:', error)
+      }
+    })
+
+    // 鐩戝惉浠诲姟鐘舵�佹洿鏂�
+    eventSource.addEventListener('taskStatus', (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        // 濡傛灉鎸囧畾浜� groupId锛屽彧鏇存柊璇ョ粍鐨勪换鍔�
+        if (!props.groupId || !data.groupId || String(data.groupId) === String(props.groupId)) {
+          updateTaskFromSSE(data)
+        }
+      } catch (error) {
+        console.error('瑙f瀽浠诲姟鐘舵�佸け璐�:', error)
+      }
+    })
+
+    // 鐩戝惉姝ラ鏇存柊
+    eventSource.addEventListener('stepUpdate', (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        // 濡傛灉鏁版嵁涓寘鍚� taskId锛屾鏌ユ槸鍚﹀尮閰嶅綋鍓嶆煡鐪嬬殑浠诲姟
+        if (data.taskId && data.taskId === currentTaskId.value) {
+          updateStepFromSSE(data)
+        } else if (!data.taskId) {
+          // 濡傛灉娌℃湁 taskId锛屽彲鑳芥槸姝ラ鏁版嵁鐩存帴浼犻��
+          updateStepFromSSE(data)
+        }
+      } catch (error) {
+        console.error('瑙f瀽姝ラ鏇存柊澶辫触:', error)
+      }
+    })
+
+    // 鐩戝惉姝ラ鍒楄〃鏇存柊
+    eventSource.addEventListener('stepsUpdate', (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        if (data.taskId === currentTaskId.value && Array.isArray(data.steps)) {
+          steps.value = data.steps
+        }
+      } catch (error) {
+        console.error('瑙f瀽姝ラ鍒楄〃澶辫触:', error)
+      }
+    })
+  } catch (error) {
+    console.error('鍒涘缓SSE杩炴帴澶辫触:', error)
+    ElMessage.error('杩炴帴瀹炴椂鐩戞帶澶辫触: ' + error.message)
+    sseConnecting.value = false
+  }
+}
+
+const disconnectSSE = () => {
+  if (eventSource) {
+    eventSource.close()
+    eventSource = null
+  }
+  sseConnected.value = false
+  sseConnecting.value = false
+}
+
+// 浠嶴SE鏇存柊浠诲姟鐘舵��
+const updateTaskFromSSE = (data) => {
+  if (!data || !data.taskId) return
+
+  // 濡傛灉鎸囧畾浜� groupId锛屽彧澶勭悊璇ョ粍鐨勪换鍔�
+  if (props.groupId && data.groupId && String(data.groupId) !== String(props.groupId)) {
+    return
+  }
+
+  const taskIndex = tasks.value.findIndex(t => t.taskId === data.taskId)
+  if (taskIndex >= 0) {
+    // 鏇存柊浠诲姟 - 淇濈暀鍘熸湁瀛楁锛屽彧鏇存柊SSE浼犳潵鐨勫瓧娈�
+    const existingTask = tasks.value[taskIndex]
+    tasks.value[taskIndex] = {
+      ...existingTask,
+      status: data.status || existingTask.status,
+      currentStep: data.currentStep !== undefined ? data.currentStep : existingTask.currentStep,
+      totalSteps: data.totalSteps !== undefined ? data.totalSteps : existingTask.totalSteps,
+      startTime: data.startTime ? new Date(data.startTime) : existingTask.startTime,
+      endTime: data.endTime ? new Date(data.endTime) : existingTask.endTime,
+      errorMessage: data.errorMessage || existingTask.errorMessage
+    }
+    // 濡傛灉褰撳墠鏌ョ湅鐨勬槸杩欎釜浠诲姟锛屼篃鏇存柊
+    if (currentTaskId.value === data.taskId) {
+      currentTask.value = tasks.value[taskIndex]
+    }
+  } else {
+    // 鏂颁换鍔★紝娣诲姞鍒板垪琛紙闇�瑕佽浆鎹㈡椂闂存埑涓篋ate瀵硅薄锛�
+    const newTask = {
+      ...data,
+      startTime: data.startTime ? new Date(data.startTime) : null,
+      endTime: data.endTime ? new Date(data.endTime) : null
+    }
+    tasks.value.unshift(newTask)
+  }
+}
+
+// 浠嶴SE鏇存柊姝ラ
+const updateStepFromSSE = (data) => {
+  if (!data) return
+  
+  // 濡傛灉鏁版嵁涓寘鍚� taskId锛屾鏌ユ槸鍚﹀尮閰嶅綋鍓嶆煡鐪嬬殑浠诲姟
+  if (data.taskId && data.taskId !== currentTaskId.value) {
+    return
+  }
+
+  // 濡傛灉褰撳墠娌℃湁鎵撳紑浠诲姟璇︽儏锛屼笉鏇存柊姝ラ
+  if (!currentTaskId.value) {
+    return
+  }
+
+  // 浣跨敤 id 鎴� stepOrder 鏉ユ煡鎵炬楠�
+  const stepIndex = data.id 
+    ? steps.value.findIndex(s => s.id === data.id)
+    : data.stepOrder !== undefined
+    ? steps.value.findIndex(s => s.stepOrder === data.stepOrder)
+    : -1
+
+  if (stepIndex >= 0) {
+    // 鏇存柊姝ラ - 淇濈暀鍘熸湁瀛楁锛屽彧鏇存柊SSE浼犳潵鐨勫瓧娈�
+    const existingStep = steps.value[stepIndex]
+    steps.value[stepIndex] = {
+      ...existingStep,
+      status: data.status || existingStep.status,
+      startTime: data.startTime ? new Date(data.startTime) : existingStep.startTime,
+      endTime: data.endTime ? new Date(data.endTime) : existingStep.endTime,
+      durationMs: data.durationMs !== undefined ? data.durationMs : existingStep.durationMs,
+      retryCount: data.retryCount !== undefined ? data.retryCount : existingStep.retryCount,
+      errorMessage: data.errorMessage || existingStep.errorMessage
+    }
+  } else if (data.stepOrder !== undefined) {
+    // 鏂版楠わ紝娣诲姞鍒板垪琛紙闇�瑕佽浆鎹㈡椂闂存埑涓篋ate瀵硅薄锛�
+    const newStep = {
+      ...data,
+      startTime: data.startTime ? new Date(data.startTime) : null,
+      endTime: data.endTime ? new Date(data.endTime) : null
+    }
+    steps.value.push(newStep)
+    // 鎸� stepOrder 鎺掑簭
+    steps.value.sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
+  }
+}
+
+const handleCancelTask = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+      `纭畾瑕佸彇娑堜换鍔� ${row.taskId} 鍚楋紵`,
+      '纭鍙栨秷',
+      {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
+      }
+    )
+    await multiDeviceTaskApi.cancelTask(row.taskId)
+    ElMessage.success('浠诲姟宸插彇娑�')
+    fetchTasks()
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error(error?.message || '鍙栨秷浠诲姟澶辫触')
+    }
+  }
+}
+
+const emit = defineEmits(['task-selected'])
+
 const handleRowClick = async (row) => {
   currentTaskId.value = row.taskId
+  currentTask.value = row
+  emit('task-selected', row)
   drawerVisible.value = true
   stepsLoading.value = true
   try {
@@ -120,9 +429,67 @@
       return 'danger'
     case 'RUNNING':
       return 'warning'
+    case 'PENDING':
+      return 'info'
+    case 'CANCELLED':
+      return 'info'
     default:
       return 'info'
   }
+}
+
+const getStatusLabel = (status) => {
+  const s = (status || '').toUpperCase()
+  const statusMap = {
+    'COMPLETED': '宸插畬鎴�',
+    'FAILED': '澶辫触',
+    'RUNNING': '鎵ц涓�',
+    'PENDING': '绛夊緟涓�',
+    'CANCELLED': '宸插彇娑�'
+  }
+  return statusMap[s] || s || '鏈煡'
+}
+
+const getProgressPercentage = (row) => {
+  if (!row.totalSteps || row.totalSteps === 0) return 0
+  return Math.round(((row.currentStep || 0) / row.totalSteps) * 100)
+}
+
+const getProgressStatus = (status) => {
+  const s = (status || '').toUpperCase()
+  if (s === 'COMPLETED') return 'success'
+  if (s === 'FAILED') return 'exception'
+  if (s === 'RUNNING') return 'active'
+  return null
+}
+
+const getStepTimelineType = (status) => {
+  const s = (status || '').toUpperCase()
+  if (s === 'COMPLETED') return 'success'
+  if (s === 'FAILED') return 'danger'
+  if (s === 'RUNNING') return 'primary'
+  return 'info'
+}
+
+const getStepStatusType = (status) => {
+  const s = (status || '').toUpperCase()
+  if (s === 'COMPLETED') return 'success'
+  if (s === 'FAILED') return 'danger'
+  if (s === 'RUNNING') return 'warning'
+  if (s === 'PENDING') return 'info'
+  return 'default'
+}
+
+const getStepStatusLabel = (status) => {
+  const s = (status || '').toUpperCase()
+  const statusMap = {
+    'COMPLETED': '宸插畬鎴�',
+    'FAILED': '澶辫触',
+    'RUNNING': '鎵ц涓�',
+    'PENDING': '绛夊緟涓�',
+    'SKIPPED': '宸茶烦杩�'
+  }
+  return statusMap[s] || s || '鏈煡'
 }
 
 const formatDuration = (ms) => {
@@ -157,14 +524,45 @@
   () => props.groupId,
   () => {
     fetchTasks()
+    // 濡傛灉SSE宸茶繛鎺ワ紝閲嶆柊杩炴帴锛堝洜涓虹洃鍚墍鏈変换鍔★紝鍓嶇浼氳繃婊わ級
+    if (sseConnected.value) {
+      disconnectSSE()
+      // 寤惰繜閲嶈繛锛岄伩鍏嶉绻佽繛鎺�
+      setTimeout(() => {
+        connectSSE()
+      }, 500)
+    }
   },
   { immediate: true }
 )
 
-onMounted(fetchTasks)
+watch(
+  () => props.taskId,
+  () => {
+    // 濡傛灉鎸囧畾浜� taskId锛岄噸鏂拌繛鎺ヤ互鐩戝惉鐗瑰畾浠诲姟
+    if (sseConnected.value) {
+      disconnectSSE()
+      setTimeout(() => {
+        connectSSE()
+      }, 500)
+    }
+  }
+)
+
+onMounted(() => {
+  fetchTasks()
+  // 鑷姩杩炴帴SSE
+  connectSSE()
+})
+
+onUnmounted(() => {
+  disconnectSSE()
+})
 
 defineExpose({
-  fetchTasks
+  fetchTasks,
+  connectSSE,
+  disconnectSSE
 })
 </script>
 
@@ -193,6 +591,56 @@
   font-size: 13px;
 }
 
+.sse-status {
+  margin-top: 4px;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.sse-status.connected {
+  color: #67c23a;
+}
+
+.sse-status.disconnected {
+  color: #f56c6c;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 12px;
+}
+
+.progress-cell {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.drawer-header {
+  margin-bottom: 20px;
+}
+
+.step-title {
+  font-weight: 600;
+  margin-bottom: 4px;
+}
+
+.step-desc {
+  font-size: 13px;
+  color: #606266;
+  margin-top: 4px;
+}
+
+.step-desc.error-message {
+  color: #f56c6c;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-top: 8px;
+}
+
 .step-title {
   font-weight: 600;
   margin-bottom: 4px;

--
Gitblit v1.8.0