- 增加了检测流资源中引用循环的功能,以防止循环依赖。 - 改进了流引用的验证,以确保节点输出中不存在循环依赖。 - 增强了资源定义清理功能,以便在执行前移除过时的设备ID。 - 已实现复制资源角色的合并,以优化现有资源的优先级并避免重复。 - 更新了LoopIterationResults组件,以管理运行中迭代的自动扩展。 - 增强了NodeState和NodeTitle组件,以支持循环和子流结果的更宽弹出框显示。 - 优化了ParamsDrawer和SubFlowExecutionResults组件,以处理动态参数更新和执行结果。 - 改进了TaskExecutionLog,以统一输出处理并增强用户对日志的可见性。 - 更新了各种参数组件,以简化输入处理并提升用户体验。 - 在代码中添加了全面的注释和文档,以提高可维护性和易理解性。
204 lines
4.9 KiB
Vue
204 lines
4.9 KiB
Vue
<!--
|
||
* 文件说明:节点运行状态面板,展示耗时、输入、输出和错误。
|
||
* 作用范围:仅服务于流程设计器模块。
|
||
-->
|
||
<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>
|