2026-05-28 17:55:49 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="input-source-manager">
|
|
|
|
|
<el-divider content-position="left">上级参数引用</el-divider>
|
|
|
|
|
|
|
|
|
|
<!-- 已引用的参数列表 -->
|
|
|
|
|
<div v-if="currentInputSource && currentInputSource.length > 0" class="input-source-list">
|
|
|
|
|
<div v-for="(sourceNode, index) in currentInputSource" :key="index" class="input-source-item">
|
|
|
|
|
<div class="input-source-header">
|
|
|
|
|
<span class="input-source-node-name">{{ getNodeName(sourceNode.nodeId) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="sourceNode.field && sourceNode.field.length > 0" class="input-source-fields">
|
|
|
|
|
<div v-for="fieldName in sourceNode.field" :key="fieldName" class="field-tag">
|
|
|
|
|
<el-tag size="small">{{ fieldName }}</el-tag>
|
|
|
|
|
<el-button type="danger" link size="small" @click="emit('removeField', sourceNode.nodeId, fieldName)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="input-source-output">
|
|
|
|
|
<el-switch
|
|
|
|
|
:model-value="sourceNode.quoteOutput === true"
|
|
|
|
|
@change="(val: boolean) => emit('toggleOutput', sourceNode.nodeId, val)"
|
|
|
|
|
size="small"
|
|
|
|
|
active-text="引入输出"
|
|
|
|
|
inactive-text=""
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 显示所有上级节点的输出引用选项 -->
|
|
|
|
|
<div v-if="availableParentNodes.length > 0" class="parent-nodes-output">
|
|
|
|
|
<div class="parent-nodes-title">上级节点输出</div>
|
|
|
|
|
<div v-for="parentNode in availableParentNodes" :key="parentNode.id" class="parent-node-output-item">
|
|
|
|
|
<span class="parent-node-name">{{ parentNode.name }}</span>
|
|
|
|
|
<el-switch
|
|
|
|
|
:model-value="isNodeOutputQuoted(parentNode.id)"
|
|
|
|
|
@change="(val: boolean) => emit('toggleOutput', parentNode.id, val)"
|
|
|
|
|
size="small"
|
|
|
|
|
active-text="引入输出"
|
|
|
|
|
inactive-text=""
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 选择参数下拉框 -->
|
|
|
|
|
<el-form-item label="选择参数">
|
|
|
|
|
<el-select :model-value="selectedParam" @update:model-value="handleParamSelect" placeholder="选择上级节点的参数" class="w100">
|
|
|
|
|
<el-option v-for="param in availableParams" :key="param.value" :label="param.label" :value="param.value" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed } from 'vue';
|
|
|
|
|
import type { Node } from '@vue-flow/core';
|
|
|
|
|
|
|
|
|
|
interface NodeData {
|
2026-05-29 10:19:53 +08:00
|
|
|
label?: string;
|
|
|
|
|
nodeCode?: string;
|
|
|
|
|
inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
|
2026-05-28 17:55:49 +08:00
|
|
|
formConfig?: any[];
|
|
|
|
|
modelConfig?: any;
|
|
|
|
|
skillName?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ParentNode {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ParamOption {
|
|
|
|
|
label: string;
|
|
|
|
|
value: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
2026-05-29 10:19:53 +08:00
|
|
|
selectedNode: Node<NodeData, any, string> | null;
|
|
|
|
|
nodes: Node<NodeData, any, string>[];
|
2026-05-28 17:55:49 +08:00
|
|
|
edges: any[];
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
(e: 'removeField', nodeId: string, fieldName: string): void;
|
|
|
|
|
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
|
|
|
|
|
(e: 'addParam', paramValue: string): void;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const selectedParam = ref('');
|
|
|
|
|
|
|
|
|
|
// 当前节点的 inputSource
|
|
|
|
|
const currentInputSource = computed(() => {
|
2026-05-29 10:19:53 +08:00
|
|
|
if (!props.selectedNode?.data?.inputSource) return [];
|
2026-05-28 17:55:49 +08:00
|
|
|
return props.selectedNode.data.inputSource.filter((item) => item.field && item.field.length > 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 获取节点名称
|
|
|
|
|
const getNodeName = (nodeId: string) => {
|
|
|
|
|
const node = props.nodes.find((n) => n.id === nodeId);
|
2026-05-29 10:19:53 +08:00
|
|
|
return node?.data?.label || nodeId;
|
2026-05-28 17:55:49 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取所有上级节点(用于显示输出引用选项)
|
|
|
|
|
const availableParentNodes = computed(() => {
|
2026-05-29 10:19:53 +08:00
|
|
|
if (!props.selectedNode?.data) return [];
|
2026-05-28 17:55:49 +08:00
|
|
|
|
|
|
|
|
// 获取已经引用了字段的节点ID列表
|
|
|
|
|
const inputSource = props.selectedNode.data.inputSource;
|
|
|
|
|
const nodesWithFields = new Set<string>();
|
|
|
|
|
if (Array.isArray(inputSource)) {
|
|
|
|
|
inputSource.forEach((item) => {
|
|
|
|
|
if (item.field && item.field.length > 0) {
|
|
|
|
|
nodesWithFields.add(item.nodeId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 递归查找所有上级节点
|
|
|
|
|
const findAllParentNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
|
|
|
|
if (visited.has(nodeId)) return [];
|
|
|
|
|
visited.add(nodeId);
|
|
|
|
|
|
|
|
|
|
const incomingEdges = props.edges.filter((e) => e.target === nodeId);
|
|
|
|
|
const parentIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
incomingEdges.forEach((edge) => {
|
|
|
|
|
parentIds.push(edge.source);
|
|
|
|
|
parentIds.push(...findAllParentNodes(edge.source, visited));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return parentIds;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const allParentIds = findAllParentNodes(props.selectedNode.id);
|
|
|
|
|
const parentNodes = allParentIds
|
|
|
|
|
.map((parentId) => {
|
|
|
|
|
const parentNode = props.nodes.find((n) => n.id === parentId);
|
2026-05-29 10:19:53 +08:00
|
|
|
if (!parentNode?.data) return null;
|
2026-05-28 17:55:49 +08:00
|
|
|
|
|
|
|
|
const nodeCode = String(parentNode.data.nodeCode || '').toLowerCase();
|
|
|
|
|
const isJudge = ['判断', 'judge', 'condition', 'if', 'branch', 'gateway'].some((k) => nodeCode.includes(k));
|
|
|
|
|
const isStart = nodeCode === '__start__';
|
|
|
|
|
|
|
|
|
|
if (isJudge || isStart || nodesWithFields.has(parentId)) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: parentId,
|
2026-05-29 10:19:53 +08:00
|
|
|
name: parentNode.data.label || parentId,
|
2026-05-28 17:55:49 +08:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
return parentNodes as ParentNode[];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 检查节点输出是否被引用
|
|
|
|
|
const isNodeOutputQuoted = (nodeId: string): boolean => {
|
2026-05-29 10:19:53 +08:00
|
|
|
if (!props.selectedNode?.data) return false;
|
2026-05-28 17:55:49 +08:00
|
|
|
const inputSource = props.selectedNode.data.inputSource;
|
|
|
|
|
if (!Array.isArray(inputSource)) return false;
|
|
|
|
|
|
|
|
|
|
const node = inputSource.find((item) => item.nodeId === nodeId);
|
|
|
|
|
return node?.quoteOutput === true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取可用的参数选项
|
|
|
|
|
const availableParams = computed(() => {
|
|
|
|
|
if (!props.selectedNode) return [];
|
|
|
|
|
|
|
|
|
|
const params: ParamOption[] = [];
|
|
|
|
|
const visited = new Set<string>();
|
|
|
|
|
|
|
|
|
|
const findParents = (nodeId: string) => {
|
|
|
|
|
if (visited.has(nodeId)) return;
|
|
|
|
|
visited.add(nodeId);
|
|
|
|
|
|
|
|
|
|
props.edges
|
|
|
|
|
.filter((e) => e.target === nodeId)
|
|
|
|
|
.forEach((edge) => {
|
|
|
|
|
const parent = props.nodes.find((n) => n.id === edge.source);
|
2026-05-29 10:19:53 +08:00
|
|
|
if (parent?.data && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') {
|
2026-05-28 17:55:49 +08:00
|
|
|
params.push({
|
|
|
|
|
label: `${parent.data.label}.output`,
|
|
|
|
|
value: `\${${parent.id}.output}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
findParents(edge.source);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
findParents(props.selectedNode.id);
|
|
|
|
|
return params;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleParamSelect = (value: string) => {
|
|
|
|
|
if (!value) return;
|
|
|
|
|
emit('addParam', value);
|
|
|
|
|
selectedParam.value = '';
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.input-source-manager {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-list {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-item {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-header {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-node-name {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #334155;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-fields {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.field-tag {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-source-output {
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
border-top: 1px solid #e2e8f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.parent-nodes-output {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.parent-nodes-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.parent-node-output-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.parent-node-name {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #475569;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.w100 {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
</style>
|