CMVR-BID/src/views/flow/components/LoopIterationResults.vue

218 lines
6.5 KiB
Vue
Raw Normal View History

<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>