重构工作流绘制

This commit is contained in:
2026-05-28 17:55:49 +08:00
parent 9bd4a44ab6
commit 032f258912
9 changed files with 2202 additions and 25 deletions

View File

@@ -0,0 +1,279 @@
<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 {
label: string;
nodeCode: string;
inputSource: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
formConfig?: any[];
modelConfig?: any;
skillName?: string;
}
interface ParentNode {
id: string;
name: string;
}
interface ParamOption {
label: string;
value: string;
}
const props = defineProps<{
selectedNode: Node<NodeData> | null;
nodes: Node<NodeData>[];
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(() => {
if (!props.selectedNode?.data.inputSource) return [];
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);
return node?.data.label || nodeId;
};
// 获取所有上级节点(用于显示输出引用选项)
const availableParentNodes = computed(() => {
if (!props.selectedNode) return [];
// 获取已经引用了字段的节点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);
if (!parentNode) return null;
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,
name: parentNode.data.label,
};
})
.filter(Boolean);
return parentNodes as ParentNode[];
});
// 检查节点输出是否被引用
const isNodeOutputQuoted = (nodeId: string): boolean => {
if (!props.selectedNode) return false;
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);
if (parent && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') {
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>