CMVR-IOT-UI/src/views/flow/components/params/EndParams.vue

172 lines
6.1 KiB
Vue
Raw Normal View History

<!-- 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>