- 增加了检测流资源中引用循环的功能,以防止循环依赖。 - 改进了流引用的验证,以确保节点输出中不存在循环依赖。 - 增强了资源定义清理功能,以便在执行前移除过时的设备ID。 - 已实现复制资源角色的合并,以优化现有资源的优先级并避免重复。 - 更新了LoopIterationResults组件,以管理运行中迭代的自动扩展。 - 增强了NodeState和NodeTitle组件,以支持循环和子流结果的更宽弹出框显示。 - 优化了ParamsDrawer和SubFlowExecutionResults组件,以处理动态参数更新和执行结果。 - 改进了TaskExecutionLog,以统一输出处理并增强用户对日志的可见性。 - 更新了各种参数组件,以简化输入处理并提升用户体验。 - 在代码中添加了全面的注释和文档,以提高可维护性和易理解性。
225 lines
7.2 KiB
Vue
225 lines
7.2 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 })
|
||
|
||
// 后端状态枚举统一映射为 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>
|