forked from SmartBench/CMVR-IOT-UI
218 lines
6.5 KiB
Vue
218 lines
6.5 KiB
Vue
|
|
<template>
|
||
|
|
<div class="loop-results">
|
||
|
|
<div class="loop-results__summary">
|
||
|
|
<span>{{ history.length }} 次循环</span>
|
||
|
|
<el-tag v-if="analysisSummary" :type="analysisSummary.type" size="small">
|
||
|
|
{{ analysisSummary.label }}
|
||
|
|
</el-tag>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<el-empty v-if="!history.length" description="暂无循环结果" :image-size="56" />
|
||
|
|
<el-collapse v-else v-model="expandedIterations" class="loop-results__iterations">
|
||
|
|
<el-collapse-item v-for="iteration in history" :key="iteration.iteration" :name="iteration.iteration">
|
||
|
|
<template #title>
|
||
|
|
<div class="loop-results__iteration-title">
|
||
|
|
<strong>第 {{ iteration.iteration }} 次</strong>
|
||
|
|
<el-tag :type="statusMeta(iteration.status).type" size="small">
|
||
|
|
{{ statusMeta(iteration.status).label }}
|
||
|
|
</el-tag>
|
||
|
|
<el-tag v-if="analysisMeta(iteration.analysisStatus)" :type="analysisMeta(iteration.analysisStatus).type" size="small">
|
||
|
|
{{ analysisMeta(iteration.analysisStatus).label }}
|
||
|
|
</el-tag>
|
||
|
|
<span>{{ formatDuration(iteration.durationMs) }}</span>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<div class="loop-results__nodes">
|
||
|
|
<details v-for="node in iteration.nodes || []" :key="`${iteration.iteration}:${node.nodeId}`" class="loop-results__node">
|
||
|
|
<summary>
|
||
|
|
<span class="loop-results__node-name">{{ node.nodeName }}</span>
|
||
|
|
<el-tag :type="statusMeta(node.status).type" size="small">
|
||
|
|
{{ statusMeta(node.status).label }}
|
||
|
|
</el-tag>
|
||
|
|
<el-tag v-if="node.passed !== null && node.passed !== undefined" :type="node.passed ? 'success' : 'danger'" size="small">
|
||
|
|
{{ node.passed ? '通过' : '未通过' }}
|
||
|
|
</el-tag>
|
||
|
|
<span class="loop-results__duration">{{ formatDuration(node.durationMs) }}</span>
|
||
|
|
</summary>
|
||
|
|
<div v-if="node.error" class="loop-results__error">{{ node.error }}</div>
|
||
|
|
<div v-if="hasValue(node.input)" class="loop-results__data">
|
||
|
|
<div class="loop-results__label">输入</div>
|
||
|
|
<JsonViewer :value="node.input" copyable boxed sort theme="light" />
|
||
|
|
</div>
|
||
|
|
<div v-if="hasValue(node.output)" class="loop-results__data">
|
||
|
|
<div class="loop-results__label">输出</div>
|
||
|
|
<JsonViewer :value="node.output" copyable boxed sort theme="light" />
|
||
|
|
</div>
|
||
|
|
</details>
|
||
|
|
</div>
|
||
|
|
</el-collapse-item>
|
||
|
|
</el-collapse>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed, ref, watch } from 'vue'
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
history: { type: Array, default: () => [] },
|
||
|
|
})
|
||
|
|
|
||
|
|
const expandedIterations = ref([])
|
||
|
|
let initialized = false
|
||
|
|
|
||
|
|
watch(() => props.history, (history) => {
|
||
|
|
if (initialized || !history?.length) return
|
||
|
|
const active = history.find(item => String(item.status || '').toUpperCase() === 'RUNNING')
|
||
|
|
expandedIterations.value = [active?.iteration || history[history.length - 1].iteration]
|
||
|
|
initialized = true
|
||
|
|
}, { immediate: true, deep: true })
|
||
|
|
|
||
|
|
const statusMeta = (status) => {
|
||
|
|
const value = String(status || '').toUpperCase()
|
||
|
|
if (value === 'SUCCESS') return { label: '执行完成', type: 'success' }
|
||
|
|
if (value === 'FAILED') return { label: '执行失败', type: 'danger' }
|
||
|
|
if (value === 'STOPPED') return { label: '已停止', type: 'info' }
|
||
|
|
if (value === 'PAUSED') return { label: '已暂停', type: 'warning' }
|
||
|
|
return { label: '执行中', type: 'warning' }
|
||
|
|
}
|
||
|
|
|
||
|
|
const analysisMeta = (status) => {
|
||
|
|
if (status === 'PASSED') return { label: '分析通过', type: 'success' }
|
||
|
|
if (status === 'FAILED') return { label: '分析未通过', type: 'danger' }
|
||
|
|
if (status === 'RUNNING') return { label: '分析中', type: 'warning' }
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
const analysisSummary = computed(() => {
|
||
|
|
const statuses = props.history.map(item => item.analysisStatus).filter(Boolean)
|
||
|
|
if (!statuses.length) return null
|
||
|
|
if (statuses.includes('FAILED')) return { label: '存在未通过', type: 'danger' }
|
||
|
|
if (statuses.includes('RUNNING')) return { label: '分析中', type: 'warning' }
|
||
|
|
return { label: '全部通过', type: 'success' }
|
||
|
|
})
|
||
|
|
|
||
|
|
const formatDuration = (value) => {
|
||
|
|
const duration = Number(value)
|
||
|
|
if (!Number.isFinite(duration) || duration < 0) return '-'
|
||
|
|
if (duration < 1000) return `${duration} ms`
|
||
|
|
return `${(duration / 1000).toFixed(2)} s`
|
||
|
|
}
|
||
|
|
|
||
|
|
const hasValue = (value) => {
|
||
|
|
if (value === null || value === undefined || value === '') return false
|
||
|
|
if (Array.isArray(value)) return value.length > 0
|
||
|
|
if (typeof value === 'object') return Object.keys(value).length > 0
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.loop-results__summary {
|
||
|
|
display: flex;
|
||
|
|
min-height: 32px;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 0 2px 8px;
|
||
|
|
color: #344054;
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__iterations {
|
||
|
|
border-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__iteration-title {
|
||
|
|
display: flex;
|
||
|
|
min-width: 0;
|
||
|
|
flex: 1;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding-right: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__iteration-title > span:last-child {
|
||
|
|
margin-left: auto;
|
||
|
|
color: #909399;
|
||
|
|
font-size: 12px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__nodes {
|
||
|
|
display: grid;
|
||
|
|
gap: 6px;
|
||
|
|
padding: 4px 0 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node {
|
||
|
|
border-bottom: 1px solid #ebeef5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node summary {
|
||
|
|
display: flex;
|
||
|
|
min-height: 34px;
|
||
|
|
align-items: center;
|
||
|
|
gap: 7px;
|
||
|
|
padding: 3px 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
list-style: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node summary::-webkit-details-marker {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node summary::before {
|
||
|
|
width: 0;
|
||
|
|
height: 0;
|
||
|
|
border-top: 4px solid transparent;
|
||
|
|
border-bottom: 4px solid transparent;
|
||
|
|
border-left: 6px solid #909399;
|
||
|
|
content: '';
|
||
|
|
transition: transform 0.16s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node[open] summary::before {
|
||
|
|
transform: rotate(90deg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__node-name {
|
||
|
|
min-width: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
color: #303133;
|
||
|
|
font-weight: 600;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__duration {
|
||
|
|
margin-left: auto;
|
||
|
|
color: #909399;
|
||
|
|
font-size: 12px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__error {
|
||
|
|
margin: 4px 4px 8px 18px;
|
||
|
|
padding: 7px 9px;
|
||
|
|
border-left: 3px solid var(--el-color-danger);
|
||
|
|
background: var(--el-color-danger-light-9);
|
||
|
|
color: var(--el-color-danger-dark-2);
|
||
|
|
line-height: 18px;
|
||
|
|
overflow-wrap: anywhere;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__data {
|
||
|
|
margin: 6px 4px 10px 18px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loop-results__label {
|
||
|
|
margin-bottom: 5px;
|
||
|
|
color: #667085;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
</style>
|