174 lines
7.0 KiB
Vue
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>
|