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