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

172 lines
6.1 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.

<!-- End node output contract and value references. -->
<template>
<div class="end-params">
<div class="section-heading">
<div>
<div class="section-title">流程输出</div>
<div class="section-note">输出值来自开始参数或前面节点的输出。</div>
</div>
<el-button type="primary" plain :icon="Plus" @click="addOutput">添加输出</el-button>
</div>
<el-empty v-if="!outputs.length" description="当前流程没有输出" :image-size="72" />
<div v-for="(item, index) in outputs" :key="item.key" class="output-row">
<el-form-item label="输出名称" required>
<el-input v-model="item.name" maxlength="40" placeholder="例如:result" />
</el-form-item>
<el-form-item label="引用来源" required>
<el-cascader
v-model="item.quote"
:options="quoteOptions"
:props="{ checkStrictly: true }"
placeholder="选择开始参数或节点输出"
style="width: 100%"
@visible-change="refreshQuoteOptions"
@change="(value) => changeQuote(item, value)"
/>
</el-form-item>
<div class="output-row__meta">
<el-tag effect="plain" type="info">{{ item.valueType || '自动识别' }}</el-tag>
<el-input v-model="item.desc" placeholder="输出说明(可选)" />
<el-button circle :icon="Delete" aria-label="删除输出" @click="outputs.splice(index, 1)" />
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { Delete, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getUpstreamInput } from '@/utils/flow'
const props = defineProps({ data: Object })
const emit = defineEmits(['save-success', 'save-error'])
const outputs = ref([])
const quoteOptions = ref([])
const clone = (value) => JSON.parse(JSON.stringify(value ?? null))
const key = () => `output_${Date.now()}_${Math.random().toString(16).slice(2)}`
// 结束节点的输出只能来自已经执行过的开始参数或上游节点输出,面板展开时实时刷新引用树。
const refreshQuoteOptions = (visible = true) => {
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
}
/**
* 根据级联引用路径找到源字段定义,用于继承类型、子字段和默认说明。
* 路径格式为 [节点ID, input/output, 字段名, 子字段名...]。
*/
const findDefinition = (quote) => {
if (!Array.isArray(quote) || quote.length < 3) return null
const graph = globalThis.lf.getGraphData()
const source = graph.nodes.find((node) => node.id === quote[0])
let definitions = quote[1] === 'input'
? [...(source?.properties?.inputParams || []), ...(source?.properties?.nodeParams || [])]
: source?.properties?.outputParams || []
let definition = null
quote.slice(2).forEach((name) => {
definition = definitions.find((item) => item.name === name)
definitions = definition?.children || []
})
return definition
}
/**
* 用户切换引用来源后同步输出契约的类型和层级结构。
* 未手动填写名称/说明时,优先继承被引用字段的元信息。
*/
const changeQuote = (item, value) => {
item.quoteType = value?.[1] || ''
const definition = findDefinition(value)
if (!definition) return
item.valueType = definition.type || 'string'
item.children = clone(definition.children || [])
if (!item.name) item.name = definition.name
if (!item.desc) item.desc = definition.desc || ''
}
const addOutput = () => outputs.value.push({
key: key(),
name: '',
valueType: 'string',
quote: [],
quoteType: '',
desc: '',
children: [],
})
const init = () => {
const paramsByName = new Map((props.data.properties?.nodeParams || []).map((item) => [item.name, item]))
outputs.value = (props.data.properties?.outputParams || []).map((output) => {
const param = paramsByName.get(output.name) || {}
return {
key: key(),
name: output.name || '',
valueType: output.type || 'string',
quote: Array.isArray(param.quote) ? [...param.quote] : [],
quoteType: param.quoteType || param.quote?.[1] || '',
desc: output.desc || '',
children: clone(output.children || []),
}
})
refreshQuoteOptions()
}
/**
* 校验流程输出契约并写回结束节点。
* nodeParams 保存运行时取值引用,outputParams 保存对外暴露的输出字段说明。
*/
const validateAndSave = () => {
const names = outputs.value.map((item) => item.name.trim())
if (names.some((name) => !name)) {
ElMessage.warning('请填写流程输出名称')
emit('save-error')
return
}
if (new Set(names).size !== names.length) {
ElMessage.warning('流程输出名称不能重复')
emit('save-error')
return
}
if (outputs.value.some((item) => !Array.isArray(item.quote) || item.quote.length < 3)) {
ElMessage.warning('每个流程输出都必须选择引用来源')
emit('save-error')
return
}
const nodeParams = outputs.value.map((item) => ({
name: item.name.trim(),
type: 'quote',
input: '',
quote: [...item.quote],
quoteType: item.quoteType || item.quote[1],
required: true,
}))
const outputParams = outputs.value.map((item) => ({
name: item.name.trim(),
type: item.valueType || 'string',
desc: item.desc || '',
children: clone(item.children || []),
}))
globalThis.lf.setProperties(props.data.id, {
...props.data.properties,
nodeParams,
outputParams,
outputType: 'json',
})
emit('save-success')
}
onMounted(init)
defineExpose({ validateAndSave })
</script>
<style scoped lang="scss">
.end-params { padding-top: 14px; }
.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.section-title { color: #1f2937; font-size: 14px; font-weight: 650; }
.section-note { margin-top: 4px; color: #7a8494; font-size: 12px; }
.output-row { margin-bottom: 12px; padding: 14px; border: 1px solid #e4e8ef; border-radius: 6px; background: #fff; }
.output-row__meta { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; }
</style>