CMVR-BID/src/views/flow/components/LoopIterationResults.vue
zhanghao d733ea8525 feat: 增强流资源管理和节点执行组件
- 增加了检测流资源中引用循环的功能,以防止循环依赖。
- 改进了流引用的验证,以确保节点输出中不存在循环依赖。
- 增强了资源定义清理功能,以便在执行前移除过时的设备ID。
- 已实现复制资源角色的合并,以优化现有资源的优先级并避免重复。
- 更新了LoopIterationResults组件,以管理运行中迭代的自动扩展。
- 增强了NodeState和NodeTitle组件,以支持循环和子流结果的更宽弹出框显示。
- 优化了ParamsDrawer和SubFlowExecutionResults组件,以处理动态参数更新和执行结果。
- 改进了TaskExecutionLog,以统一输出处理并增强用户对日志的可见性。
- 更新了各种参数组件,以简化输入处理并提升用户体验。
- 在代码中添加了全面的注释和文档,以提高可维护性和易理解性。
2026-08-21 15:01:16 +08:00

225 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 })
// 后端状态枚举统一映射为 Element Plus tag 的文案和类型。
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' }
})
// 后端以毫秒返回耗时,短耗时保留 ms,较长耗时转秒,避免标题区域过长。
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`
}
// JSON Viewer 只在确实有内容时展示,避免空对象/空数组占据节点详情空间。
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>