2026-08-21 11:48:10 +08:00
|
|
|
|
<!-- 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)}`
|
|
|
|
|
|
|
2026-08-21 15:01:16 +08:00
|
|
|
|
// 结束节点的输出只能来自已经执行过的开始参数或上游节点输出,面板展开时实时刷新引用树。
|
2026-08-21 11:48:10 +08:00
|
|
|
|
const refreshQuoteOptions = (visible = true) => {
|
|
|
|
|
|
if (visible) quoteOptions.value = getUpstreamInput(props.data.id) || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 15:01:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据级联引用路径找到源字段定义,用于继承类型、子字段和默认说明。
|
|
|
|
|
|
* 路径格式为 [节点ID, input/output, 字段名, 子字段名...]。
|
|
|
|
|
|
*/
|
2026-08-21 11:48:10 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 15:01:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 用户切换引用来源后同步输出契约的类型和层级结构。
|
|
|
|
|
|
* 未手动填写名称/说明时,优先继承被引用字段的元信息。
|
|
|
|
|
|
*/
|
2026-08-21 11:48:10 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 15:01:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 校验流程输出契约并写回结束节点。
|
|
|
|
|
|
* nodeParams 保存运行时取值引用,outputParams 保存对外暴露的输出字段说明。
|
|
|
|
|
|
*/
|
2026-08-21 11:48:10 +08:00
|
|
|
|
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>
|