<template>
|
<div class="execution-monitor">
|
<div class="panel-header">
|
<div>
|
<h3>任务执行监控</h3>
|
<p>实时查看最新的多设备任务</p>
|
</div>
|
<el-button :loading="loading" @click="fetchTasks">
|
<el-icon><Refresh /></el-icon>
|
刷新
|
</el-button>
|
</div>
|
|
<el-table
|
v-loading="loading"
|
:data="tasks"
|
height="300"
|
stripe
|
@row-click="handleRowClick"
|
>
|
<el-table-column prop="taskId" label="任务编号" min-width="160" />
|
<el-table-column prop="groupId" label="设备组ID" width="120" />
|
<el-table-column prop="status" label="状态" width="120">
|
<template #default="{ row }">
|
<el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="currentStep" label="进度" width="120">
|
<template #default="{ row }">
|
{{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}
|
</template>
|
</el-table-column>
|
<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>
|
|
<el-drawer v-model="drawerVisible" size="40%" title="任务步骤详情">
|
<el-timeline v-loading="stepsLoading" :reverse="false">
|
<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'"
|
>
|
<div class="step-title">{{ step.stepName }}</div>
|
<div class="step-desc">状态:{{ step.status }}</div>
|
<div class="step-desc">耗时:{{ formatDuration(step.durationMs) }}</div>
|
<div class="step-desc" v-if="step.errorMessage">
|
错误:{{ step.errorMessage }}
|
</div>
|
</el-timeline-item>
|
</el-timeline>
|
</el-drawer>
|
</div>
|
</template>
|
|
<script setup>
|
import { onMounted, ref, watch } from 'vue'
|
import { ElMessage } from 'element-plus'
|
import { Refresh } from '@element-plus/icons-vue'
|
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
|
|
const props = defineProps({
|
groupId: {
|
type: [String, Number],
|
default: null
|
}
|
})
|
|
const loading = ref(false)
|
const tasks = ref([])
|
const drawerVisible = ref(false)
|
const stepsLoading = ref(false)
|
const steps = ref([])
|
const currentTaskId = ref(null)
|
|
const fetchTasks = async () => {
|
try {
|
loading.value = true
|
const { data } = await multiDeviceTaskApi.getTaskList({
|
groupId: props.groupId,
|
page: 1,
|
size: 10
|
})
|
tasks.value = data?.records || data?.data || data || []
|
} catch (error) {
|
ElMessage.error(error?.message || '加载任务列表失败')
|
} finally {
|
loading.value = false
|
}
|
}
|
|
const handleRowClick = async (row) => {
|
currentTaskId.value = row.taskId
|
drawerVisible.value = true
|
stepsLoading.value = true
|
try {
|
const { data } = await multiDeviceTaskApi.getTaskSteps(row.taskId)
|
steps.value = Array.isArray(data) ? data : (data?.data || [])
|
} catch (error) {
|
ElMessage.error(error?.message || '加载任务步骤失败')
|
} finally {
|
stepsLoading.value = false
|
}
|
}
|
|
const statusType = (status) => {
|
switch ((status || '').toUpperCase()) {
|
case 'COMPLETED':
|
return 'success'
|
case 'FAILED':
|
return 'danger'
|
case 'RUNNING':
|
return 'warning'
|
default:
|
return 'info'
|
}
|
}
|
|
const formatDuration = (ms) => {
|
if (!ms) return '-'
|
if (ms < 1000) return `${ms} ms`
|
return `${(ms / 1000).toFixed(1)} s`
|
}
|
|
// 格式化日期时间
|
const formatDateTime = (dateTime) => {
|
if (!dateTime) return '-'
|
try {
|
const date = new Date(dateTime)
|
// 检查日期是否有效
|
if (isNaN(date.getTime())) {
|
return dateTime // 如果无法解析,返回原始值
|
}
|
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()
|
},
|
{ immediate: true }
|
)
|
|
onMounted(fetchTasks)
|
|
defineExpose({
|
fetchTasks
|
})
|
</script>
|
|
<style scoped>
|
.execution-monitor {
|
background: #fff;
|
border-radius: 12px;
|
padding: 20px;
|
box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
|
}
|
|
.panel-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
}
|
|
.panel-header h3 {
|
margin: 0;
|
}
|
|
.panel-header p {
|
margin: 4px 0 0;
|
color: #909399;
|
font-size: 13px;
|
}
|
|
.step-title {
|
font-weight: 600;
|
margin-bottom: 4px;
|
}
|
|
.step-desc {
|
font-size: 13px;
|
color: #606266;
|
}
|
</style>
|