|
@@ -8,7 +8,7 @@ import {
|
|
|
} from 'naive-ui'
|
|
} from 'naive-ui'
|
|
|
import {
|
|
import {
|
|
|
ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
|
|
ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
|
|
|
- SparklesOutline, RocketOutline, GitBranchOutline, ExitOutline,
|
|
|
|
|
|
|
+ SparklesOutline, RocketOutline, FlaskOutline, GitBranchOutline, ExitOutline,
|
|
|
PlayOutline, CloseOutline
|
|
PlayOutline, CloseOutline
|
|
|
} from '@vicons/ionicons5'
|
|
} from '@vicons/ionicons5'
|
|
|
import { useWorkflowStore } from '../../stores/workflow'
|
|
import { useWorkflowStore } from '../../stores/workflow'
|
|
@@ -17,8 +17,10 @@ import { runWorkflow } from '../../api/workflow'
|
|
|
import InputNode from '../../components/workflow/nodes/InputNode.vue'
|
|
import InputNode from '../../components/workflow/nodes/InputNode.vue'
|
|
|
import LLMNode from '../../components/workflow/nodes/LLMNode.vue'
|
|
import LLMNode from '../../components/workflow/nodes/LLMNode.vue'
|
|
|
import AgentNode from '../../components/workflow/nodes/AgentNode.vue'
|
|
import AgentNode from '../../components/workflow/nodes/AgentNode.vue'
|
|
|
|
|
+import SkillNode from '../../components/workflow/nodes/SkillNode.vue'
|
|
|
import OutputNode from '../../components/workflow/nodes/OutputNode.vue'
|
|
import OutputNode from '../../components/workflow/nodes/OutputNode.vue'
|
|
|
import ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
|
|
import ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
|
|
|
|
|
+import { inferNewEdge, refreshMappingsForNode, getEdgeStyle, IO_TYPES, extractTemplateVariables } from '../../utils/ioInference'
|
|
|
|
|
|
|
|
import '@vue-flow/core/dist/style.css'
|
|
import '@vue-flow/core/dist/style.css'
|
|
|
import '@vue-flow/core/dist/theme-default.css'
|
|
import '@vue-flow/core/dist/theme-default.css'
|
|
@@ -73,18 +75,34 @@ const defaultEdgeStyle = { stroke: '#666', strokeWidth: 2 }
|
|
|
const selectedEdgeStyle = { stroke: '#facc15', strokeWidth: 3 }
|
|
const selectedEdgeStyle = { stroke: '#facc15', strokeWidth: 3 }
|
|
|
|
|
|
|
|
onConnect((params) => {
|
|
onConnect((params) => {
|
|
|
|
|
+ const sourceNode = getNodes.value.find(n => n.id === params.source)
|
|
|
|
|
+ const targetNode = getNodes.value.find(n => n.id === params.target)
|
|
|
|
|
+ const inferred = sourceNode && targetNode ? inferNewEdge(sourceNode, targetNode) : null
|
|
|
addEdges([{
|
|
addEdges([{
|
|
|
...params,
|
|
...params,
|
|
|
type: 'bezier',
|
|
type: 'bezier',
|
|
|
animated: true,
|
|
animated: true,
|
|
|
- style: defaultEdgeStyle
|
|
|
|
|
|
|
+ style: inferred?.style || defaultEdgeStyle,
|
|
|
|
|
+ data: inferred?.data || {}
|
|
|
}])
|
|
}])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 批量更新边的映射数据和样式
|
|
|
|
|
+function applyEdgeMappingUpdates(updates) {
|
|
|
|
|
+ for (const upd of updates) {
|
|
|
|
|
+ const edge = getEdges.value.find(e => e.id === upd.id)
|
|
|
|
|
+ if (edge) {
|
|
|
|
|
+ edge.data = { ...edge.data, ...upd.data }
|
|
|
|
|
+ edge.style = upd.style
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ========== 节点类型定义 ==========
|
|
// ========== 节点类型定义 ==========
|
|
|
const nodeTypes = [
|
|
const nodeTypes = [
|
|
|
{ type: 'userInput', label: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline), color: '#10B981' },
|
|
{ type: 'userInput', label: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline), color: '#10B981' },
|
|
|
{ type: 'llm', label: '大模型处理', icon: markRaw(SparklesOutline), color: '#8B5CF6' },
|
|
{ type: 'llm', label: '大模型处理', icon: markRaw(SparklesOutline), color: '#8B5CF6' },
|
|
|
|
|
+ { type: 'skill', label: '技能', icon: markRaw(FlaskOutline), color: '#06B6D4' },
|
|
|
{ type: 'agent', label: '智能体', icon: markRaw(RocketOutline), color: '#F59E0B' },
|
|
{ type: 'agent', label: '智能体', icon: markRaw(RocketOutline), color: '#F59E0B' },
|
|
|
{ type: 'condition', label: '条件分支', icon: markRaw(GitBranchOutline), color: '#F97316' },
|
|
{ type: 'condition', label: '条件分支', icon: markRaw(GitBranchOutline), color: '#F97316' },
|
|
|
{ type: 'output', label: '输出', icon: markRaw(ExitOutline), color: '#EF4444' }
|
|
{ type: 'output', label: '输出', icon: markRaw(ExitOutline), color: '#EF4444' }
|
|
@@ -93,6 +111,7 @@ const nodeTypes = [
|
|
|
const nodeTypeInfo = {
|
|
const nodeTypeInfo = {
|
|
|
userInput: { color: '#10B981', typeLabel: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline) },
|
|
userInput: { color: '#10B981', typeLabel: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline) },
|
|
|
llm: { color: '#8B5CF6', typeLabel: '大模型', icon: markRaw(SparklesOutline) },
|
|
llm: { color: '#8B5CF6', typeLabel: '大模型', icon: markRaw(SparklesOutline) },
|
|
|
|
|
+ skill: { color: '#06B6D4', typeLabel: '技能', icon: markRaw(FlaskOutline) },
|
|
|
agent: { color: '#F59E0B', typeLabel: '智能体', icon: markRaw(RocketOutline) },
|
|
agent: { color: '#F59E0B', typeLabel: '智能体', icon: markRaw(RocketOutline) },
|
|
|
condition: { color: '#F97316', typeLabel: '条件', icon: markRaw(GitBranchOutline) },
|
|
condition: { color: '#F97316', typeLabel: '条件', icon: markRaw(GitBranchOutline) },
|
|
|
output: { color: '#EF4444', typeLabel: '输出', icon: markRaw(ExitOutline) }
|
|
output: { color: '#EF4444', typeLabel: '输出', icon: markRaw(ExitOutline) }
|
|
@@ -101,10 +120,11 @@ const nodeTypeInfo = {
|
|
|
function getDefaultData(type) {
|
|
function getDefaultData(type) {
|
|
|
switch (type) {
|
|
switch (type) {
|
|
|
case 'userInput': return { label: '用户输入', variables: [] }
|
|
case 'userInput': return { label: '用户输入', variables: [] }
|
|
|
- case 'llm': return { label: '大模型处理', model: '', systemPrompt: '', userPrompt: '' }
|
|
|
|
|
|
|
+ case 'llm': return { label: '大模型处理', model: '', systemPrompt: '', userPrompt: '', inputs: [], outputs: [{ name: 'result', label: 'LLM 输出', type: 'string', description: '' }] }
|
|
|
case 'agent': return { label: '智能体', agentId: '', agentName: '' }
|
|
case 'agent': return { label: '智能体', agentId: '', agentName: '' }
|
|
|
|
|
+ case 'skill': return { label: '技能', skillId: '', skillName: '' }
|
|
|
case 'condition': return { label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
|
|
case 'condition': return { label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
|
|
|
- case 'output': return { label: '输出', outputType: 'text' }
|
|
|
|
|
|
|
+ case 'output': return { label: '输出', fields: [{ name: 'result', label: '输出结果', type: 'string', description: '' }] }
|
|
|
default: return { label: '节点' }
|
|
default: return { label: '节点' }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -161,10 +181,14 @@ function onEdgeClick({ edge }) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function deleteSelectedEdge() {
|
|
function deleteSelectedEdge() {
|
|
|
- if (selectedEdgeId.value) {
|
|
|
|
|
- removeEdges(selectedEdgeId.value)
|
|
|
|
|
- selectedEdgeId.value = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!selectedEdgeId.value) return
|
|
|
|
|
+ const edge = getEdges.value.find(e => e.id === selectedEdgeId.value)
|
|
|
|
|
+ const sourceId = edge?.source
|
|
|
|
|
+ const targetId = edge?.target
|
|
|
|
|
+ removeEdges(selectedEdgeId.value)
|
|
|
|
|
+ selectedEdgeId.value = null
|
|
|
|
|
+ if (sourceId) applyEdgeMappingUpdates(refreshMappingsForNode(sourceId, getNodes.value, getEdges.value))
|
|
|
|
|
+ if (targetId) applyEdgeMappingUpdates(refreshMappingsForNode(targetId, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onKeyDown(e) {
|
|
function onKeyDown(e) {
|
|
@@ -178,6 +202,11 @@ function onKeyDown(e) {
|
|
|
|
|
|
|
|
const selectedData = computed(() => selectedNode.value?.data || null)
|
|
const selectedData = computed(() => selectedNode.value?.data || null)
|
|
|
|
|
|
|
|
|
|
+const selectedEdge = computed(() => {
|
|
|
|
|
+ if (!selectedEdgeId.value) return null
|
|
|
|
|
+ return getEdges.value.find(e => e.id === selectedEdgeId.value) || null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
// ========== 智能体选项 ==========
|
|
// ========== 智能体选项 ==========
|
|
|
const agentOptions = computed(() =>
|
|
const agentOptions = computed(() =>
|
|
|
(skillStore.skills || []).map(s => ({
|
|
(skillStore.skills || []).map(s => ({
|
|
@@ -195,11 +224,6 @@ const modelOptions = [
|
|
|
{ label: 'GLM-4', value: 'glm-4' }
|
|
{ label: 'GLM-4', value: 'glm-4' }
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
-const outputTypeOptions = [
|
|
|
|
|
- { label: '文本', value: 'text' },
|
|
|
|
|
- { label: 'JSON 对象', value: 'json' }
|
|
|
|
|
-]
|
|
|
|
|
-
|
|
|
|
|
// ========== 条件分支操作 ==========
|
|
// ========== 条件分支操作 ==========
|
|
|
function addElif() {
|
|
function addElif() {
|
|
|
if (!selectedData.value?.conditions) return
|
|
if (!selectedData.value?.conditions) return
|
|
@@ -207,6 +231,7 @@ function addElif() {
|
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
|
// 同步 selectedNode 引用
|
|
// 同步 selectedNode 引用
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function removeElif(index) {
|
|
function removeElif(index) {
|
|
@@ -214,6 +239,7 @@ function removeElif(index) {
|
|
|
const newConditions = selectedData.value.conditions.filter((_, i) => i !== index)
|
|
const newConditions = selectedData.value.conditions.filter((_, i) => i !== index)
|
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ========== 变量操作 ==========
|
|
// ========== 变量操作 ==========
|
|
@@ -222,25 +248,39 @@ function addVariable() {
|
|
|
const newVars = [...(selectedData.value.variables || []), { name: '', label: '', type: 'string', description: '' }]
|
|
const newVars = [...(selectedData.value.variables || []), { name: '', label: '', type: 'string', description: '' }]
|
|
|
vfUpdateNode(selectedNode.value.id, { variables: newVars })
|
|
vfUpdateNode(selectedNode.value.id, { variables: newVars })
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function removeVariable(index) {
|
|
function removeVariable(index) {
|
|
|
const newVars = (selectedData.value?.variables || []).filter((_, i) => i !== index)
|
|
const newVars = (selectedData.value?.variables || []).filter((_, i) => i !== index)
|
|
|
vfUpdateNode(selectedNode.value.id, { variables: newVars })
|
|
vfUpdateNode(selectedNode.value.id, { variables: newVars })
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ========== 通用数据更新 ==========
|
|
// ========== 通用数据更新 ==========
|
|
|
function onFieldChange(field, value) {
|
|
function onFieldChange(field, value) {
|
|
|
if (!selectedNode.value) return
|
|
if (!selectedNode.value) return
|
|
|
const update = { [field]: value }
|
|
const update = { [field]: value }
|
|
|
- // 特殊处理:智能体选择时同步名称
|
|
|
|
|
|
|
+ // 特殊处理:智能体选择时同步名称和 IO 字段
|
|
|
if (field === 'agentId') {
|
|
if (field === 'agentId') {
|
|
|
const opt = agentOptions.value.find(o => o.value === value)
|
|
const opt = agentOptions.value.find(o => o.value === value)
|
|
|
update.agentName = opt ? opt.label : ''
|
|
update.agentName = opt ? opt.label : ''
|
|
|
|
|
+ const skill = skillStore.skills?.find(s => s.folderName === value)
|
|
|
|
|
+ update.inputs = skill?.inputs || []
|
|
|
|
|
+ update.outputs = skill?.outputs || []
|
|
|
|
|
+ }
|
|
|
|
|
+ // 特殊处理:技能选择时同步名称和 IO 字段
|
|
|
|
|
+ if (field === 'skillId') {
|
|
|
|
|
+ const opt = agentOptions.value.find(o => o.value === value)
|
|
|
|
|
+ update.skillName = opt ? opt.label : ''
|
|
|
|
|
+ const skill = skillStore.skills?.find(s => s.folderName === value)
|
|
|
|
|
+ update.inputs = skill?.inputs || []
|
|
|
|
|
+ update.outputs = skill?.outputs || []
|
|
|
}
|
|
}
|
|
|
vfUpdateNode(selectedNode.value.id, update)
|
|
vfUpdateNode(selectedNode.value.id, update)
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onConditionChange(index, field, value) {
|
|
function onConditionChange(index, field, value) {
|
|
@@ -250,6 +290,96 @@ function onConditionChange(index, field, value) {
|
|
|
)
|
|
)
|
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
|
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 输出节点字段操作 ==========
|
|
|
|
|
+function addOutputField() {
|
|
|
|
|
+ if (!selectedData.value) return
|
|
|
|
|
+ const newFields = [...(selectedData.value.fields || []), { name: '', label: '', type: 'string', description: '' }]
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { fields: newFields })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeOutputField(index) {
|
|
|
|
|
+ const newFields = (selectedData.value?.fields || []).filter((_, i) => i !== index)
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { fields: newFields })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== LLM 节点 IO 字段操作 ==========
|
|
|
|
|
+function addLlmField(field) {
|
|
|
|
|
+ if (!selectedData.value) return
|
|
|
|
|
+ const list = [...(selectedData.value[field] || []), { name: '', label: '', type: 'string', description: '' }]
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { [field]: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeLlmField(field, index) {
|
|
|
|
|
+ const list = (selectedData.value?.[field] || []).filter((_, i) => i !== index)
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { [field]: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function syncLlmFieldsFromTemplate(field) {
|
|
|
|
|
+ if (!selectedData.value) return
|
|
|
|
|
+ const extracted = extractTemplateVariables(
|
|
|
|
|
+ (selectedData.value.systemPrompt || '') + '\n' + (selectedData.value.userPrompt || '')
|
|
|
|
|
+ )
|
|
|
|
|
+ const existing = selectedData.value[field] || []
|
|
|
|
|
+ const existingNames = new Set(existing.map(f => f.name))
|
|
|
|
|
+ const merged = [...existing]
|
|
|
|
|
+ for (const name of extracted) {
|
|
|
|
|
+ if (!existingNames.has(name)) {
|
|
|
|
|
+ merged.push({ name, label: name, type: 'string', description: '' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 移除不在模板中的字段(仅移除无 label 描述的自动提取字段)
|
|
|
|
|
+ const final = merged.filter(f => extracted.includes(f.name) || f.label !== f.name || existingNames.has(f.name))
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { [field]: final })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 智能体节点 IO 字段操作 ==========
|
|
|
|
|
+function addAgentField(field) {
|
|
|
|
|
+ if (!selectedData.value) return
|
|
|
|
|
+ const list = [...(selectedData.value[field] || []), { name: '', label: '', type: 'string', description: '' }]
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { [field]: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeAgentField(field, index) {
|
|
|
|
|
+ const list = (selectedData.value?.[field] || []).filter((_, i) => i !== index)
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { [field]: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 条件分支节点输入变量操作 ==========
|
|
|
|
|
+function addConditionInput() {
|
|
|
|
|
+ if (!selectedData.value) return
|
|
|
|
|
+ const list = [...(selectedData.value.inputs || []), { name: '', label: '', type: 'string', description: '' }]
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { inputs: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeConditionInput(index) {
|
|
|
|
|
+ const list = (selectedData.value?.inputs || []).filter((_, i) => i !== index)
|
|
|
|
|
+ vfUpdateNode(selectedNode.value.id, { inputs: list })
|
|
|
|
|
+ selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 边状态文案 ==========
|
|
|
|
|
+function edgeStatusText(status) {
|
|
|
|
|
+ return { ok: '匹配完成', unmapped: '未映射', partial: '部分匹配', mismatch: '类型不匹配' }[status] || '未知'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ========== 保存 ==========
|
|
// ========== 保存 ==========
|
|
@@ -262,7 +392,7 @@ async function handleSave() {
|
|
|
try {
|
|
try {
|
|
|
const graphData = JSON.stringify({
|
|
const graphData = JSON.stringify({
|
|
|
nodes: getNodes.value.map(n => ({ id: n.id, type: n.type, position: n.position, data: n.data })),
|
|
nodes: getNodes.value.map(n => ({ id: n.id, type: n.type, position: n.position, data: n.data })),
|
|
|
- edges: getEdges.value.map(e => ({ id: e.id, source: e.source, target: e.target, sourceHandle: e.sourceHandle }))
|
|
|
|
|
|
|
+ edges: getEdges.value.map(e => ({ id: e.id, source: e.source, target: e.target, sourceHandle: e.sourceHandle, data: e.data }))
|
|
|
})
|
|
})
|
|
|
if (workflowId) {
|
|
if (workflowId) {
|
|
|
await wfStore.save(workflowId, {
|
|
await wfStore.save(workflowId, {
|
|
@@ -422,9 +552,14 @@ onMounted(async () => {
|
|
|
...e,
|
|
...e,
|
|
|
type: 'bezier',
|
|
type: 'bezier',
|
|
|
animated: true,
|
|
animated: true,
|
|
|
- style: defaultEdgeStyle
|
|
|
|
|
|
|
+ style: defaultEdgeStyle,
|
|
|
|
|
+ data: e.data || {}
|
|
|
}))
|
|
}))
|
|
|
if (edgeData.length) addEdges(edgeData)
|
|
if (edgeData.length) addEdges(edgeData)
|
|
|
|
|
+ // 重新推断所有边的 IO 映射(保留已有的手动映射)
|
|
|
|
|
+ for (const node of getNodes.value) {
|
|
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(node.id, getNodes.value, getEdges.value))
|
|
|
|
|
+ }
|
|
|
} catch { /* ignore */ }
|
|
} catch { /* ignore */ }
|
|
|
}
|
|
}
|
|
|
} catch {
|
|
} catch {
|
|
@@ -502,6 +637,7 @@ onMounted(async () => {
|
|
|
<template #node-userInput="nodeProps"><InputNode :data="nodeProps.data" /></template>
|
|
<template #node-userInput="nodeProps"><InputNode :data="nodeProps.data" /></template>
|
|
|
<template #node-llm="nodeProps"><LLMNode :data="nodeProps.data" /></template>
|
|
<template #node-llm="nodeProps"><LLMNode :data="nodeProps.data" /></template>
|
|
|
<template #node-agent="nodeProps"><AgentNode :data="nodeProps.data" /></template>
|
|
<template #node-agent="nodeProps"><AgentNode :data="nodeProps.data" /></template>
|
|
|
|
|
+ <template #node-skill="nodeProps"><SkillNode :data="nodeProps.data" /></template>
|
|
|
<template #node-output="nodeProps"><OutputNode :data="nodeProps.data" /></template>
|
|
<template #node-output="nodeProps"><OutputNode :data="nodeProps.data" /></template>
|
|
|
<template #node-condition="nodeProps"><ConditionNode :data="nodeProps.data" /></template>
|
|
<template #node-condition="nodeProps"><ConditionNode :data="nodeProps.data" /></template>
|
|
|
<Background :gap="20" :size="1" pattern-color="rgba(255,255,255,0.05)" />
|
|
<Background :gap="20" :size="1" pattern-color="rgba(255,255,255,0.05)" />
|
|
@@ -535,7 +671,7 @@ onMounted(async () => {
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
<n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="var-form-row">
|
|
<div class="var-form-row">
|
|
|
- <n-select :value="v.type" :options="[{label:'字符串',value:'string'},{label:'数字',value:'number'},{label:'布尔',value:'boolean'}]" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
<n-input :value="v.description" placeholder="描述" size="small" @update:value="nv => { v.description = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
<n-input :value="v.description" placeholder="描述" size="small" @update:value="nv => { v.description = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
</div>
|
|
</div>
|
|
|
<button class="text-btn danger" @click="removeVariable(i)">删除</button>
|
|
<button class="text-btn danger" @click="removeVariable(i)">删除</button>
|
|
@@ -576,6 +712,45 @@ onMounted(async () => {
|
|
|
@update:value="v => onFieldChange('userPrompt', v)"
|
|
@update:value="v => onFieldChange('userPrompt', v)"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <!-- 输入变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输入变量</span>
|
|
|
|
|
+ <div style="display:flex;gap:4px">
|
|
|
|
|
+ <button class="text-btn" @click="syncLlmFieldsFromTemplate('inputs')">从模板同步</button>
|
|
|
|
|
+ <button class="text-btn" @click="addLlmField('inputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeLlmField('inputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{变量名}} 引用,或手动添加</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 输出变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输出变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addLlmField('outputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeLlmField('outputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.outputs?.length" class="empty-hint">默认输出 result 变量</div>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<!-- 智能体节点 -->
|
|
<!-- 智能体节点 -->
|
|
@@ -585,12 +760,99 @@ onMounted(async () => {
|
|
|
<n-select
|
|
<n-select
|
|
|
:value="selectedData?.agentId"
|
|
:value="selectedData?.agentId"
|
|
|
:options="agentOptions"
|
|
:options="agentOptions"
|
|
|
- placeholder="选择 Skill 或智能体"
|
|
|
|
|
|
|
+ placeholder="选择技能"
|
|
|
size="small"
|
|
size="small"
|
|
|
filterable
|
|
filterable
|
|
|
@update:value="v => onFieldChange('agentId', v)"
|
|
@update:value="v => onFieldChange('agentId', v)"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <!-- 输入变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输入变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 输出变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输出变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeAgentField('outputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.outputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 技能节点 -->
|
|
|
|
|
+ <template v-if="selectedNode?.type === 'skill'">
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <label class="prop-label">选择技能</label>
|
|
|
|
|
+ <n-select
|
|
|
|
|
+ :value="selectedData?.skillId"
|
|
|
|
|
+ :options="agentOptions"
|
|
|
|
|
+ placeholder="选择技能"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ @update:value="v => onFieldChange('skillId', v)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 输入变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输入变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 输出变量 -->
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">输出变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-'+i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeAgentField('outputs', i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.outputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<!-- 条件分支节点 -->
|
|
<!-- 条件分支节点 -->
|
|
@@ -605,7 +867,7 @@ onMounted(async () => {
|
|
|
<span class="cond-tag">{{ c.type }}</span>
|
|
<span class="cond-tag">{{ c.type }}</span>
|
|
|
<n-input
|
|
<n-input
|
|
|
:value="c.expression"
|
|
:value="c.expression"
|
|
|
- placeholder="自然语言条件"
|
|
|
|
|
|
|
+ placeholder="自然语言条件,可用 {{变量名}}"
|
|
|
size="small"
|
|
size="small"
|
|
|
@update:value="v => onConditionChange(i, 'expression', v)"
|
|
@update:value="v => onConditionChange(i, 'expression', v)"
|
|
|
/>
|
|
/>
|
|
@@ -613,22 +875,96 @@ onMounted(async () => {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">接收变量</span>
|
|
|
|
|
+ <button class="text-btn" @click="addConditionInput">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(v, i) in selectedData?.inputs || []" :key="i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.name" placeholder="变量名" size="small" @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="v.label" placeholder="中文名" size="small" @update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeConditionInput(i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.inputs?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
|
|
|
|
|
+ <div class="passthrough-hint">所有接收变量将透传至下游节点</div>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<!-- 输出节点 -->
|
|
<!-- 输出节点 -->
|
|
|
<template v-if="selectedNode?.type === 'output'">
|
|
<template v-if="selectedNode?.type === 'output'">
|
|
|
<div class="prop-section">
|
|
<div class="prop-section">
|
|
|
- <label class="prop-label">输出格式</label>
|
|
|
|
|
- <n-select
|
|
|
|
|
- :value="selectedData?.outputType"
|
|
|
|
|
- :options="outputTypeOptions"
|
|
|
|
|
- size="small"
|
|
|
|
|
- @update:value="v => onFieldChange('outputType', v)"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">接收字段</span>
|
|
|
|
|
+ <button class="text-btn" @click="addOutputField">+ 添加</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(f, i) in selectedData?.fields || []" :key="i" class="var-form">
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="f.name" placeholder="字段名" size="small" @update:value="nv => { f.name = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
|
|
+ <n-select :value="f.type" :options="IO_TYPES" size="small" style="width:100px" @update:value="nv => { f.type = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="var-form-row">
|
|
|
|
|
+ <n-input :value="f.label" placeholder="中文名" size="small" @update:value="nv => { f.label = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
|
|
+ <button class="text-btn danger" @click="removeOutputField(i)">删除</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedData?.fields?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</template>
|
|
</template>
|
|
|
- <div v-else class="empty-props-hint">点击节点查看属性</div>
|
|
|
|
|
|
|
+ <!-- 边映射信息 -->
|
|
|
|
|
+ <template v-else-if="selectedEdge?.data">
|
|
|
|
|
+ <div class="prop-section">
|
|
|
|
|
+ <div class="prop-header">
|
|
|
|
|
+ <span class="prop-label">连接映射</span>
|
|
|
|
|
+ <span class="edge-status-badge" :class="'es-' + selectedEdge.data.status">
|
|
|
|
|
+ {{ edgeStatusText(selectedEdge.data.status) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 已映射字段 -->
|
|
|
|
|
+ <div v-if="selectedEdge.data.mapping?.length" class="mapping-list">
|
|
|
|
|
+ <div v-for="m in selectedEdge.data.mapping" :key="m.sourceField + '-' + m.targetField" class="mapping-item mapped">
|
|
|
|
|
+ <span class="mapping-field">{{ m.sourceField }}</span>
|
|
|
|
|
+ <span class="mapping-arrow">→</span>
|
|
|
|
|
+ <span class="mapping-field">{{ m.targetField }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 类型不匹配 -->
|
|
|
|
|
+ <div v-if="selectedEdge.data.typeMismatches?.length" class="mapping-list">
|
|
|
|
|
+ <div v-for="tm in selectedEdge.data.typeMismatches" :key="tm.source.name" class="mapping-item mismatch">
|
|
|
|
|
+ <span class="mapping-field">{{ tm.source.name }}</span>
|
|
|
|
|
+ <span class="mapping-type">({{ tm.source.type }})</span>
|
|
|
|
|
+ <span class="mapping-arrow">→</span>
|
|
|
|
|
+ <span class="mapping-field">{{ tm.target.name }}</span>
|
|
|
|
|
+ <span class="mapping-type">({{ tm.target.type }})</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 未匹配的目标输入 -->
|
|
|
|
|
+ <div v-if="selectedEdge.data.unmatchedTarget?.length">
|
|
|
|
|
+ <div class="prop-label" style="margin-top:8px">未匹配输入</div>
|
|
|
|
|
+ <div v-for="f in selectedEdge.data.unmatchedTarget" :key="f.name" class="mapping-item unmapped">
|
|
|
|
|
+ <span class="mapping-field">{{ f.name }}</span>
|
|
|
|
|
+ <span class="mapping-type">({{ f.type }})</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 多余的源输出 -->
|
|
|
|
|
+ <div v-if="selectedEdge.data.unmatchedSource?.length">
|
|
|
|
|
+ <div class="prop-label" style="margin-top:8px">多余输出</div>
|
|
|
|
|
+ <div v-for="f in selectedEdge.data.unmatchedSource" :key="f.name" class="mapping-item extra">
|
|
|
|
|
+ <span class="mapping-field">{{ f.name }}</span>
|
|
|
|
|
+ <span class="mapping-type">({{ f.type }})</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!selectedEdge.data.mapping?.length && !selectedEdge.data.typeMismatches?.length && !(selectedEdge.data.unmatchedTarget?.length) && !(selectedEdge.data.unmatchedSource?.length)" class="empty-hint">
|
|
|
|
|
+ 无需映射
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div v-else class="empty-props-hint">点击节点或连线查看详情</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 运行结果 Tab -->
|
|
<!-- 运行结果 Tab -->
|
|
@@ -1064,6 +1400,16 @@ onMounted(async () => {
|
|
|
font-style: italic;
|
|
font-style: italic;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.passthrough-hint {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: var(--text-tertiary, #555);
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ background: rgba(249,115,22,0.08);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ color: #fb923c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 通用按钮 */
|
|
/* 通用按钮 */
|
|
|
.icon-btn {
|
|
.icon-btn {
|
|
|
width: 32px;
|
|
width: 32px;
|
|
@@ -1199,4 +1545,50 @@ onMounted(async () => {
|
|
|
color: var(--text-secondary, #999);
|
|
color: var(--text-secondary, #999);
|
|
|
margin-bottom: 4px;
|
|
margin-bottom: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 边映射状态 */
|
|
|
|
|
+.edge-status-badge {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.edge-status-badge.es-ok { background: rgba(34,197,94,0.15); color: #4ade80; }
|
|
|
|
|
+.edge-status-badge.es-unmapped { background: rgba(107,114,128,0.15); color: #9ca3af; }
|
|
|
|
|
+.edge-status-badge.es-partial { background: rgba(245,158,11,0.15); color: #fbbf24; }
|
|
|
|
|
+.edge-status-badge.es-mismatch { background: rgba(239,68,68,0.15); color: #f87171; }
|
|
|
|
|
+
|
|
|
|
|
+.mapping-list {
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mapping-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ margin-bottom: 3px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mapping-item.mapped { background: rgba(34,197,94,0.08); }
|
|
|
|
|
+.mapping-item.mismatch { background: rgba(239,68,68,0.08); }
|
|
|
|
|
+.mapping-item.unmapped { background: rgba(245,158,11,0.08); }
|
|
|
|
|
+.mapping-item.extra { background: rgba(107,114,128,0.06); }
|
|
|
|
|
+
|
|
|
|
|
+.mapping-field {
|
|
|
|
|
+ color: var(--text-primary, #e0e0e0);
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mapping-arrow {
|
|
|
|
|
+ color: var(--text-tertiary, #666);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mapping-type {
|
|
|
|
|
+ color: var(--text-tertiary, #666);
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|