CMVR-IOT-UI/src/views/flow/components/SubFlowExecutionResults.vue
lixiaolong b3dae8db39 feat(flow): 添加子流程功能并优化参数引用逻辑
- 新增子流程节点类型,支持在同一项目中执行已发布工作流
- 添加子流程节点UI组件和相关配置选项
- 重构参数引用逻辑,使用getUpstreamInput替代getInput方法
- 优化cascader组件属性设置,统一使用props配置checkStrictly
- 修正多个参数组件中的handleTypeChange和cascaderChange函数参数传递
- 添加流程输出参数管理功能,支持引用来源选择和类型识别
- 实现子流程运行时状态管理和迭代输出处理
- 增加循环引用检测和资源验证功能
- 更新API接口,添加子流程选项获取方法
- 修复流程引用和依赖关系处理逻辑
2026-08-21 11:48:10 +08:00

174 lines
7.0 KiB
Vue

<template>
<div class="subflow-results">
<div class="subflow-results__summary">
<div>
<strong>{{ data.name || '子流程' }}</strong>
<span v-if="data.versionNo">V{{ data.versionNo }}</span>
</div>
<div class="subflow-results__summary-status">
<el-tag v-if="analysisMeta" :type="analysisMeta.type" size="small">{{ analysisMeta.label }}</el-tag>
<el-tag :type="statusMeta(data.status).type" size="small">{{ statusMeta(data.status).label }}</el-tag>
<span>{{ formatDuration(data.durationMs) }}</span>
</div>
</div>
<el-empty v-if="!executions.length" description="暂无内部节点日志" :image-size="56" />
<el-collapse v-else v-model="expandedExecutions" class="subflow-results__groups">
<el-collapse-item v-for="execution in executions" :key="execution.key" :name="execution.key">
<template #title>
<div class="subflow-results__group-title">
<strong>{{ execution.label }}</strong>
<el-tag :type="statusMeta(execution.status).type" size="small">
{{ statusMeta(execution.status).label }}
</el-tag>
<span>{{ execution.nodes?.length || 0 }} 个节点</span>
</div>
</template>
<div class="subflow-results__nodes">
<details v-for="(node, index) in execution.nodes || []" :key="`${execution.key}:${node.nodeId}:${index}`">
<summary>
<span class="subflow-results__order">{{ index + 1 }}</span>
<span class="subflow-results__node-name">{{ node.nodeName }}</span>
<span v-if="node.actionLabel && node.actionLabel !== node.nodeName" class="subflow-results__action">
{{ node.actionLabel }}
</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="subflow-results__duration">{{ formatDuration(node.durationMs) }}</span>
</summary>
<div v-if="node.error" class="subflow-results__error">{{ node.error }}</div>
<div v-if="hasValue(node.input)" class="subflow-results__data">
<div class="subflow-results__label">输入</div>
<JsonViewer :value="node.input" copyable boxed sort theme="light" />
</div>
<div v-if="hasValue(node.output)" class="subflow-results__data">
<div class="subflow-results__label">输出</div>
<JsonViewer :value="node.output" copyable boxed sort theme="light" />
</div>
</details>
</div>
</el-collapse-item>
</el-collapse>
<details v-if="hasValue(data.output)" class="subflow-results__final-output">
<summary>子流程最终输出</summary>
<JsonViewer :value="data.output" copyable boxed sort theme="light" />
</details>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({ data: { type: Object, default: () => ({}) } })
const executions = computed(() => Array.isArray(props.data.executions) ? props.data.executions : [])
const expandedExecutions = ref([])
let initialized = false
watch(executions, (groups) => {
if (initialized || !groups.length) return
const active = groups.find((group) => String(group.status || '').toUpperCase() === 'RUNNING')
expandedExecutions.value = [active?.key || groups[groups.length - 1].key]
initialized = true
}, { immediate: 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 = computed(() => {
if (props.data.analysisStatus === 'PASSED') return { label: '分析通过', type: 'success' }
if (props.data.analysisStatus === 'FAILED') return { label: '分析未通过', type: 'danger' }
if (props.data.analysisStatus === 'RUNNING') return { label: '分析中', type: 'warning' }
return null
})
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">
.subflow-results__summary,
.subflow-results__summary-status,
.subflow-results__group-title,
.subflow-results__nodes summary {
display: flex;
align-items: center;
}
.subflow-results__summary {
justify-content: space-between;
gap: 12px;
padding: 2px 2px 12px;
color: #344054;
}
.subflow-results__summary > div,
.subflow-results__summary-status { display: flex; align-items: center; gap: 8px; }
.subflow-results__summary span { color: #667085; font-size: 12px; }
.subflow-results__groups { border-top: 0; }
.subflow-results__group-title {
min-width: 0;
flex: 1;
gap: 8px;
padding-right: 8px;
}
.subflow-results__group-title > span:last-child {
margin-left: auto;
color: #909399;
font-size: 12px;
}
.subflow-results__nodes { display: grid; gap: 5px; padding-bottom: 10px; }
.subflow-results__nodes details { border-bottom: 1px solid #ebeef5; }
.subflow-results__nodes summary {
min-height: 36px;
gap: 7px;
padding: 3px 4px;
cursor: pointer;
list-style: none;
}
.subflow-results__nodes summary::-webkit-details-marker { display: none; }
.subflow-results__order { width: 20px; color: #98a2b3; font-size: 11px; text-align: center; }
.subflow-results__node-name { min-width: 0; overflow: hidden; color: #303133; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.subflow-results__action { color: #98a2b3; font-size: 11px; white-space: nowrap; }
.subflow-results__duration { margin-left: auto; color: #909399; font-size: 12px; white-space: nowrap; }
.subflow-results__error {
margin: 4px 4px 8px 31px;
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);
overflow-wrap: anywhere;
}
.subflow-results__data { margin: 6px 4px 10px 31px; }
.subflow-results__label { margin-bottom: 5px; color: #667085; font-size: 12px; font-weight: 600; }
.subflow-results__final-output { margin-top: 10px; padding-top: 10px; border-top: 1px solid #ebeef5; }
.subflow-results__final-output summary { margin-bottom: 8px; color: #344054; cursor: pointer; font-size: 12px; font-weight: 600; }
</style>