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

204 lines
4.9 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 v-if="showState !== 'NORMAL'" class="node-state" :class="`node-state--${showState.toLowerCase()}`">
<div class="node-state__summary">
<el-icon class="node-state__icon">
<SuccessFilled v-if="props.state === 'SUCCESS'" />
<CircleCloseFilled v-else-if="['FAILED', 'STOPPED'].includes(props.state)" />
<VideoPause v-else-if="props.state === 'PAUSED'" />
<Loading v-else />
</el-icon>
<span class="node-state__text">{{ stateText }}</span>
<span class="node-state__runtime">{{ props.runtimes }} ms</span>
</div>
<el-popover
ref="popoverRef"
trigger="click"
placement="right-start"
:width="props.popoverWidth"
:teleported="true"
:popper-class="props.popperClass"
@show="popoverVisible = true"
@hide="popoverVisible = false"
>
<template #reference>
<button type="button" class="node-state__result nodrag nopan" @mousedown.stop @click.stop>
<el-icon><View /></el-icon>
<span>{{ popoverVisible ? "收起结果" : "查看结果" }}</span>
</button>
</template>
<div class="result-section">
<div class="result-section__title">输入</div>
<div class="result-section__content"><slot name="input" /></div>
</div>
<div v-if="slots.output" class="result-section result-section--output">
<div class="result-section__title">输出</div>
<div class="result-section__content"><slot name="output" /></div>
</div>
</el-popover>
</div>
</template>
<script setup>
import { computed, ref, useSlots, watch } from "vue";
import { CircleCloseFilled, Loading, SuccessFilled, VideoPause, View } from "@element-plus/icons-vue";
const props = defineProps({
state: { type: String, default: "NORMAL" },
runtimes: { type: Number, default: 0 },
// 循环和子流程的运行结果更宽,允许调用方覆盖弹窗尺寸和样式类。
popoverWidth: { type: Number, default: 480 },
popperClass: { type: String, default: "flow-result-popover" },
});
const slots = useSlots();
const popoverRef = ref();
const popoverVisible = ref(false);
const showState = ref("NORMAL");
const stateText = computed(() => ({
SUCCESS: "执行成功",
RUNNING: "执行中",
FAILED: "执行失败",
PAUSED: "已暂停",
STOPPED: "已停止",
NORMAL: "",
}[props.state] || props.state));
watch(() => props.state, (value) => {
showState.value = value;
}, { immediate: true });
/**
* 关闭 closePopover 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const closePopover = () => {
showState.value = "NORMAL";
popoverVisible.value = false;
popoverRef.value?.hide?.();
};
defineExpose({ closePopover });
</script>
<style lang="scss" scoped>
.node-state {
display: flex;
min-height: 34px;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: -4px -4px 10px;
padding: 5px 7px 5px 9px;
border: 1px solid #dbe3ee;
border-radius: 6px;
background: #f7f9fc;
color: #526071;
font-size: 12px;
}
.node-state__summary {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.node-state__icon { font-size: 16px; }
.node-state__text { font-weight: 600; white-space: nowrap; }
.node-state__runtime {
padding-left: 6px;
border-left: 1px solid currentColor;
opacity: 0.72;
white-space: nowrap;
}
.node-state__result {
display: flex;
height: 26px;
flex: 0 0 auto;
align-items: center;
gap: 4px;
padding: 0 7px;
border: 0;
border-radius: 4px;
background: rgba(255, 255, 255, 0.72);
color: inherit;
cursor: pointer;
font-size: 11px;
font-weight: 600;
}
.node-state--success {
border-color: #c8ead5;
background: #effaf3;
color: #1e7d49;
}
.node-state--failed,
.node-state--stopped {
border-color: #f1cecb;
background: #fff3f2;
color: #c33b32;
}
.node-state--paused {
border-color: #d9dee7;
background: #f3f5f8;
color: #657084;
}
.node-state--running {
border-color: #cbdcf8;
background: #f0f6ff;
color: #2f6fca;
.node-state__icon { animation: rotate-animation 1.2s linear infinite; }
}
@keyframes rotate-animation {
to { transform: rotate(360deg); }
}
</style>
<style lang="scss">
.flow-result-popover {
max-width: min(480px, calc(100vw - 32px));
padding: 0 !important;
overflow: hidden;
.result-section {
padding: 14px 16px;
}
.result-section--output { border-top: 1px solid #e8ebf0; }
.result-section__title {
margin-bottom: 8px;
color: #344054;
font-size: 13px;
font-weight: 650;
}
.result-section__content {
max-height: 280px;
overflow: auto;
color: #475467;
font-size: 12px;
}
}
.flow-loop-result-popover {
max-width: min(720px, calc(100vw - 32px));
.result-section__content {
max-height: 520px;
}
}
</style>