From 9dcde5b27b70a4b0c0885347af5405eb2d1ef089 Mon Sep 17 00:00:00 2001
From: huang <1532065656@qq.com>
Date: 星期五, 12 十二月 2025 17:00:54 +0800
Subject: [PATCH] 修改前端状态显示变更,保持前端实时更新
---
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 525 insertions(+), 20 deletions(-)
diff --git a/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue b/mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
index 65133b4..b4dfb0c 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,35 +40,99 @@
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" prop="startTime" />
- <el-table-column label="缁撴潫鏃堕棿" min-width="160" prop="endTime" />
+ <el-table-column label="寮�濮嬫椂闂�" min-width="160">
+ <template #default="{ row }">
+ {{ formatDateTime(row.startTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃堕棿" min-width="160">
+ <template #default="{ row }">
+ {{ 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' || row.status === 'FAILED'"
+ 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="step.startTime || '-'"
- :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
+ :timestamp="formatDateTime(step.startTime) || '-'"
+ :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" v-if="step.successMessage">
+ 鎻愮ず锛歿{ step.successMessage }}
+ </div>
+ <div class="step-desc error-message" v-if="step.errorMessage">
+ <el-icon><Warning /></el-icon>
閿欒锛歿{ step.errorMessage }}
</div>
</el-timeline-item>
@@ -55,14 +142,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
}
})
@@ -73,6 +171,19 @@
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 || ''
+
+// 缁熶竴鐨処D姣旇緝锛岄伩鍏嶆暟瀛�/瀛楃涓蹭笉涓�鑷村鑷碨SE鏇存柊琚拷鐣�
+const isSameId = (a, b) => {
+ if (a == null || b == null) return false
+ return String(a) === String(b)
+}
const fetchTasks = async () => {
try {
@@ -90,8 +201,226 @@
}
}
+// 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 raw = JSON.parse(event.data)
+ // 鍚庣鍏ㄩ噺鐩戝惉鏃剁殑鏁版嵁缁撴瀯涓� { taskId, step: { ... } }锛岄渶瑕佸睍寮� step
+ const data = raw?.step ? { ...raw.step, taskId: raw.taskId || raw.step.taskId } : raw
+ // 濡傛灉鏁版嵁涓寘鍚� taskId锛屾鏌ユ槸鍚﹀尮閰嶅綋鍓嶆煡鐪嬬殑浠诲姟
+ if (data.taskId && isSameId(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 => isSameId(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 && !isSameId(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,
+ successMessage: data.successMessage !== undefined ? data.successMessage : existingStep.successMessage,
+ 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 {
@@ -104,6 +433,20 @@
}
}
+// 鏍规嵁taskId鎵撳紑浠诲姟璇︽儏鎶藉眽锛堜緵鐖剁粍浠惰皟鐢級
+const openTaskDrawer = async (taskId) => {
+ if (!taskId) return
+ // 濡傛灉浠诲姟鍒楄〃涓虹┖锛屽厛鍔犺浇涓�娆�
+ if (!tasks.value || tasks.value.length === 0) {
+ await fetchTasks()
+ }
+ const task = tasks.value.find(t => t.taskId === taskId)
+ if (!task) {
+ return
+ }
+ await handleRowClick(task)
+}
+
const statusType = (status) => {
switch ((status || '').toUpperCase()) {
case 'COMPLETED':
@@ -112,9 +455,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) => {
@@ -123,18 +524,72 @@
return `${(ms / 1000).toFixed(1)} s`
}
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return '-'
+ try {
+ const date = new Date(dateTime)
+ // 妫�鏌ユ棩鏈熸槸鍚︽湁鏁�
+ if (isNaN(date.getTime())) {
+ return dateTime // 濡傛灉鏃犳硶瑙f瀽锛岃繑鍥炲師濮嬪��
+ }
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ const seconds = String(date.getSeconds()).padStart(2, '0')
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+ } catch (error) {
+ console.warn('鏍煎紡鍖栨椂闂村け璐�:', dateTime, error)
+ return dateTime
+ }
+}
+
watch(
() => 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,
+ openTaskDrawer
})
</script>
@@ -163,6 +618,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