|
|
@@ -1,5 +1,5 @@
|
|
|
<script setup>
|
|
|
-import { ref, computed, reactive, onMounted, markRaw } from 'vue'
|
|
|
+import { ref, computed, reactive, onMounted, markRaw, nextTick } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
|
|
import { Background } from '@vue-flow/background'
|
|
|
@@ -9,17 +9,19 @@ import {
|
|
|
import {
|
|
|
ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
|
|
|
SparklesOutline, RocketOutline, FlaskOutline, GitBranchOutline, ExitOutline,
|
|
|
- PlayOutline, CloseOutline
|
|
|
+ PlayOutline, CloseOutline, ColorWandOutline
|
|
|
} from '@vicons/ionicons5'
|
|
|
import { useWorkflowStore } from '../../stores/workflow'
|
|
|
import { useSkillStore } from '../../stores/skill'
|
|
|
import { runWorkflow } from '../../api/workflow'
|
|
|
+import { getModelList } from '../../api/model'
|
|
|
import InputNode from '../../components/workflow/nodes/InputNode.vue'
|
|
|
import LLMNode from '../../components/workflow/nodes/LLMNode.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 ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
|
|
|
+import SmartActionNode from '../../components/workflow/nodes/SmartActionNode.vue'
|
|
|
import { inferNewEdge, refreshMappingsForNode, getEdgeStyle, IO_TYPES, extractTemplateVariables } from '../../utils/ioInference'
|
|
|
|
|
|
import '@vue-flow/core/dist/style.css'
|
|
|
@@ -64,9 +66,9 @@ const canRun = computed(() => !!workflowId && !running.value && getNodes.value.l
|
|
|
|
|
|
// ========== Vue Flow ==========
|
|
|
const {
|
|
|
- onConnect, addEdges, addNodes, removeEdges,
|
|
|
+ onConnect, addEdges, addNodes, removeNodes, removeEdges,
|
|
|
updateNodeData: vfUpdateNode,
|
|
|
- project, vueFlowRef, getNodes, getEdges
|
|
|
+ project, vueFlowRef, getNodes, getEdges, fitView, onInit
|
|
|
} = useVueFlow()
|
|
|
|
|
|
const selectedNode = ref(null)
|
|
|
@@ -104,25 +106,28 @@ const nodeTypes = [
|
|
|
{ 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: 'smartAction', label: '智能操作', icon: markRaw(ColorWandOutline), color: '#EC4899' },
|
|
|
{ type: 'condition', label: '条件分支', icon: markRaw(GitBranchOutline), color: '#F97316' },
|
|
|
{ type: 'output', label: '输出', icon: markRaw(ExitOutline), color: '#EF4444' }
|
|
|
]
|
|
|
|
|
|
const nodeTypeInfo = {
|
|
|
- userInput: { color: '#10B981', typeLabel: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline) },
|
|
|
- llm: { color: '#8B5CF6', typeLabel: '大模型', icon: markRaw(SparklesOutline) },
|
|
|
- skill: { color: '#06B6D4', typeLabel: '技能', icon: markRaw(FlaskOutline) },
|
|
|
- agent: { color: '#F59E0B', typeLabel: '智能体', icon: markRaw(RocketOutline) },
|
|
|
- condition: { color: '#F97316', typeLabel: '条件', icon: markRaw(GitBranchOutline) },
|
|
|
- output: { color: '#EF4444', typeLabel: '输出', icon: markRaw(ExitOutline) }
|
|
|
+ userInput: { color: '#10B981', typeLabel: '用户输入', icon: markRaw(ChatbubbleEllipsesOutline) },
|
|
|
+ llm: { color: '#8B5CF6', typeLabel: '大模型', icon: markRaw(SparklesOutline) },
|
|
|
+ skill: { color: '#06B6D4', typeLabel: '技能', icon: markRaw(FlaskOutline) },
|
|
|
+ agent: { color: '#F59E0B', typeLabel: '智能体', icon: markRaw(RocketOutline) },
|
|
|
+ smartAction: { color: '#EC4899', typeLabel: '智能操作', icon: markRaw(ColorWandOutline) },
|
|
|
+ condition: { color: '#F97316', typeLabel: '条件', icon: markRaw(GitBranchOutline) },
|
|
|
+ output: { color: '#EF4444', typeLabel: '输出', icon: markRaw(ExitOutline) }
|
|
|
}
|
|
|
|
|
|
function getDefaultData(type) {
|
|
|
switch (type) {
|
|
|
case 'userInput': return { label: '用户输入', variables: [] }
|
|
|
- case 'llm': return { label: '大模型处理', model: '', systemPrompt: '', userPrompt: '', inputs: [], outputs: [{ name: 'result', label: 'LLM 输出', type: 'string', description: '' }] }
|
|
|
- case 'agent': return { label: '智能体', agentId: '', agentName: '' }
|
|
|
- case 'skill': return { label: '技能', skillId: '', skillName: '' }
|
|
|
+ case 'llm': return { label: '大模型处理', modelId: null, systemPrompt: '', userPrompt: '', inputs: [], outputs: [{ name: 'result', label: 'LLM 输出', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
+ case 'agent': return { label: '智能体', agentId: '', agentName: '', modelId: null, failStrategy: 'abort' }
|
|
|
+ case 'skill': return { label: '技能', skillId: '', skillName: '', modelId: null, failStrategy: 'abort' }
|
|
|
+ case 'smartAction': return { label: '智能操作', actionPrompt: '', modelId: null, inputs: [], outputs: [{ name: 'result', label: '操作结果', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
case 'condition': return { label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
|
|
|
case 'output': return { label: '输出', fields: [{ name: 'result', label: '输出结果', type: 'string', description: '' }] }
|
|
|
default: return { label: '节点' }
|
|
|
@@ -170,6 +175,42 @@ function onNodeClick({ node }) {
|
|
|
function onPaneClick() {
|
|
|
selectedNode.value = null
|
|
|
selectedEdgeId.value = null
|
|
|
+ contextMenu.value = null
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 节点右键菜单 ==========
|
|
|
+const contextMenu = ref(null) // { x, y, nodeId }
|
|
|
+
|
|
|
+function onNodeContextMenu({ event, node }) {
|
|
|
+ event.preventDefault()
|
|
|
+ contextMenu.value = { x: event.clientX, y: event.clientY, nodeId: node.id }
|
|
|
+}
|
|
|
+
|
|
|
+function closeContextMenu() {
|
|
|
+ contextMenu.value = null
|
|
|
+}
|
|
|
+
|
|
|
+function deleteNodeById(nodeId) {
|
|
|
+ // 收集关联边的上下游节点,删除后刷新映射
|
|
|
+ const affectedNodeIds = new Set()
|
|
|
+ for (const edge of getEdges.value) {
|
|
|
+ if (edge.source === nodeId || edge.target === nodeId) {
|
|
|
+ affectedNodeIds.add(edge.source)
|
|
|
+ affectedNodeIds.add(edge.target)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ removeNodes(nodeId)
|
|
|
+ // 清除选中状态
|
|
|
+ if (selectedNode.value?.id === nodeId) {
|
|
|
+ selectedNode.value = null
|
|
|
+ }
|
|
|
+ closeContextMenu()
|
|
|
+ // 刷新受影响节点的边映射
|
|
|
+ for (const id of affectedNodeIds) {
|
|
|
+ if (id !== nodeId) {
|
|
|
+ applyEdgeMappingUpdates(refreshMappingsForNode(id, getNodes.value, getEdges.value))
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// ========== 连接线选中与删除 ==========
|
|
|
@@ -196,7 +237,11 @@ function onKeyDown(e) {
|
|
|
// 避免在输入框内触发删除
|
|
|
const tag = e.target?.tagName
|
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
|
|
- deleteSelectedEdge()
|
|
|
+ if (selectedNode.value) {
|
|
|
+ deleteNodeById(selectedNode.value.id)
|
|
|
+ } else {
|
|
|
+ deleteSelectedEdge()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -215,13 +260,32 @@ const agentOptions = computed(() =>
|
|
|
}))
|
|
|
)
|
|
|
|
|
|
-const modelOptions = [
|
|
|
- { label: 'GPT-4o', value: 'gpt-4o' },
|
|
|
- { label: 'GPT-4o-mini', value: 'gpt-4o-mini' },
|
|
|
- { label: 'Claude 3.5 Sonnet', value: 'claude-3.5-sonnet' },
|
|
|
- { label: 'Claude 3 Opus', value: 'claude-3-opus' },
|
|
|
- { label: 'DeepSeek V3', value: 'deepseek-v3' },
|
|
|
- { label: 'GLM-4', value: 'glm-4' }
|
|
|
+// ========== 数据库模型选项 ==========
|
|
|
+const dbModels = ref([])
|
|
|
+const defaultModelId = ref(null)
|
|
|
+
|
|
|
+const modelOptions = computed(() =>
|
|
|
+ dbModels.value.map(m => ({
|
|
|
+ label: `${m.name} (${m.modelName})`,
|
|
|
+ value: m.id
|
|
|
+ }))
|
|
|
+)
|
|
|
+
|
|
|
+async function fetchDbModels() {
|
|
|
+ try {
|
|
|
+ const res = await getModelList()
|
|
|
+ dbModels.value = res.data || []
|
|
|
+ const defaultModel = dbModels.value.find(m => m.isDefault)
|
|
|
+ defaultModelId.value = defaultModel?.id || null
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('加载模型列表失败', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 失败策略选项
|
|
|
+const failStrategyOptions = [
|
|
|
+ { label: '终止工作流', value: 'abort' },
|
|
|
+ { label: '跳过继续', value: 'skip' }
|
|
|
]
|
|
|
|
|
|
// ========== 条件分支操作 ==========
|
|
|
@@ -361,7 +425,7 @@ function removeAgentField(field, index) {
|
|
|
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: '' }]
|
|
|
@@ -530,12 +594,13 @@ function closeResult() {
|
|
|
}
|
|
|
|
|
|
function statusText(s) {
|
|
|
- return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' }[s] || s
|
|
|
+ return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过', no_run: '未运行' }[s] || s
|
|
|
}
|
|
|
|
|
|
// ========== 初始化 ==========
|
|
|
onMounted(async () => {
|
|
|
skillStore.fetchSkills()
|
|
|
+ fetchDbModels()
|
|
|
if (workflowId) {
|
|
|
try {
|
|
|
const wf = await wfStore.fetchOne(workflowId)
|
|
|
@@ -567,6 +632,9 @@ onMounted(async () => {
|
|
|
goBack()
|
|
|
}
|
|
|
}
|
|
|
+ // 初始视图:fitView 后缩放到 0.5,让整体更小
|
|
|
+ await nextTick()
|
|
|
+ fitView({ padding: 0.5 })
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
@@ -624,11 +692,11 @@ onMounted(async () => {
|
|
|
<div class="canvas-area" @dragover="onDragOver" @drop="onDrop">
|
|
|
<VueFlow
|
|
|
:default-viewport="{ zoom: 1, x: 0, y: 0 }"
|
|
|
- :min-zoom="0.3"
|
|
|
+ :min-zoom="0.1"
|
|
|
:max-zoom="2"
|
|
|
- fit-view-on-init
|
|
|
:elements-selectable="true"
|
|
|
@node-click="onNodeClick"
|
|
|
+ @node-contextmenu="onNodeContextMenu"
|
|
|
@pane-click="onPaneClick"
|
|
|
@edge-click="onEdgeClick"
|
|
|
:snap-to-grid="true"
|
|
|
@@ -640,6 +708,7 @@ onMounted(async () => {
|
|
|
<template #node-skill="nodeProps"><SkillNode :data="nodeProps.data" /></template>
|
|
|
<template #node-output="nodeProps"><OutputNode :data="nodeProps.data" /></template>
|
|
|
<template #node-condition="nodeProps"><ConditionNode :data="nodeProps.data" /></template>
|
|
|
+ <template #node-smartAction="nodeProps"><SmartActionNode :data="nodeProps.data" /></template>
|
|
|
<Background :gap="20" :size="1" pattern-color="rgba(255,255,255,0.05)" />
|
|
|
</VueFlow>
|
|
|
</div>
|
|
|
@@ -685,11 +754,12 @@ onMounted(async () => {
|
|
|
<div class="prop-section">
|
|
|
<label class="prop-label">模型</label>
|
|
|
<n-select
|
|
|
- :value="selectedData?.model"
|
|
|
+ :value="selectedData?.modelId"
|
|
|
:options="modelOptions"
|
|
|
- placeholder="选择模型"
|
|
|
+ placeholder="选择模型(默认使用默认模型)"
|
|
|
size="small"
|
|
|
- @update:value="v => onFieldChange('model', v)"
|
|
|
+ clearable
|
|
|
+ @update:value="v => onFieldChange('modelId', v)"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="prop-section">
|
|
|
@@ -712,10 +782,10 @@ onMounted(async () => {
|
|
|
@update:value="v => onFieldChange('userPrompt', v)"
|
|
|
/>
|
|
|
</div>
|
|
|
- <!-- 输入变量 -->
|
|
|
+ <!-- 前置数据 -->
|
|
|
<div class="prop-section">
|
|
|
<div class="prop-header">
|
|
|
- <span class="prop-label">输入变量</span>
|
|
|
+ <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>
|
|
|
@@ -751,6 +821,16 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.outputs?.length" class="empty-hint">默认输出 result 变量</div>
|
|
|
</div>
|
|
|
+ <!-- 失败策略 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">失败策略</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.failStrategy || 'abort'"
|
|
|
+ :options="failStrategyOptions"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('failStrategy', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<!-- 智能体节点 -->
|
|
|
@@ -766,10 +846,21 @@ onMounted(async () => {
|
|
|
@update:value="v => onFieldChange('agentId', v)"
|
|
|
/>
|
|
|
</div>
|
|
|
- <!-- 输入变量 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">模型</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.modelId"
|
|
|
+ :options="modelOptions"
|
|
|
+ placeholder="默认使用默认模型"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ @update:value="v => onFieldChange('modelId', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <!-- 前置数据 -->
|
|
|
<div class="prop-section">
|
|
|
<div class="prop-header">
|
|
|
- <span class="prop-label">输入变量</span>
|
|
|
+ <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">
|
|
|
@@ -802,6 +893,16 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.outputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
</div>
|
|
|
+ <!-- 失败策略 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">失败策略</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.failStrategy || 'abort'"
|
|
|
+ :options="failStrategyOptions"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('failStrategy', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<!-- 技能节点 -->
|
|
|
@@ -817,10 +918,21 @@ onMounted(async () => {
|
|
|
@update:value="v => onFieldChange('skillId', v)"
|
|
|
/>
|
|
|
</div>
|
|
|
- <!-- 输入变量 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">模型</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.modelId"
|
|
|
+ :options="modelOptions"
|
|
|
+ placeholder="默认使用默认模型"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ @update:value="v => onFieldChange('modelId', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <!-- 前置数据 -->
|
|
|
<div class="prop-section">
|
|
|
<div class="prop-header">
|
|
|
- <span class="prop-label">输入变量</span>
|
|
|
+ <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">
|
|
|
@@ -853,6 +965,87 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.outputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
</div>
|
|
|
+ <!-- 失败策略 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">失败策略</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.failStrategy || 'abort'"
|
|
|
+ :options="failStrategyOptions"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('failStrategy', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 智能操作节点 -->
|
|
|
+ <template v-if="selectedNode?.type === 'smartAction'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">操作要求</label>
|
|
|
+ <n-input
|
|
|
+ :value="selectedData?.actionPrompt"
|
|
|
+ type="textarea" :rows="5"
|
|
|
+ placeholder="描述你希望 AI 执行的操作..."
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('actionPrompt', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">模型</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.modelId"
|
|
|
+ :options="modelOptions"
|
|
|
+ placeholder="默认使用默认模型"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ @update:value="v => onFieldChange('modelId', 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">默认输出 result 变量</div>
|
|
|
+ </div>
|
|
|
+ <!-- 失败策略 -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">失败策略</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.failStrategy || 'abort'"
|
|
|
+ :options="failStrategyOptions"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('failStrategy', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<!-- 条件分支节点 -->
|
|
|
@@ -877,7 +1070,7 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
<div class="prop-section">
|
|
|
<div class="prop-header">
|
|
|
- <span class="prop-label">接收变量</span>
|
|
|
+ <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">
|
|
|
@@ -891,7 +1084,7 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
|
|
|
- <div class="passthrough-hint">所有接收变量将透传至下游节点</div>
|
|
|
+ <div class="passthrough-hint">所有前置数据将透传至下游节点</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -943,9 +1136,9 @@ onMounted(async () => {
|
|
|
<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 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>
|
|
|
@@ -1030,6 +1223,22 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 右键菜单 -->
|
|
|
+ <Teleport to="body">
|
|
|
+ <div
|
|
|
+ v-if="contextMenu"
|
|
|
+ class="context-menu-overlay"
|
|
|
+ @click="closeContextMenu"
|
|
|
+ @contextmenu.prevent="closeContextMenu"
|
|
|
+ >
|
|
|
+ <div class="context-menu" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }">
|
|
|
+ <button class="ctx-item ctx-danger" @click="deleteNodeById(contextMenu.nodeId)">
|
|
|
+ 删除节点
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Teleport>
|
|
|
+
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -1291,6 +1500,7 @@ onMounted(async () => {
|
|
|
.nrc-status.status-success { background: rgba(16,185,129,0.15); color: #34d399; }
|
|
|
.nrc-status.status-failed { background: rgba(239,68,68,0.15); color: #f87171; }
|
|
|
.nrc-status.status-skipped { background: rgba(107,114,128,0.15); color: #9ca3af; }
|
|
|
+.nrc-status.status-no_run { background: rgba(107,114,128,0.10); color: #6b7280; }
|
|
|
|
|
|
.nrc-vars {
|
|
|
padding: 6px 10px;
|
|
|
@@ -1591,4 +1801,49 @@ onMounted(async () => {
|
|
|
color: var(--text-tertiary, #666);
|
|
|
font-size: 10px;
|
|
|
}
|
|
|
+
|
|
|
+/* 右键菜单 */
|
|
|
+.context-menu-overlay {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 200;
|
|
|
+}
|
|
|
+
|
|
|
+.context-menu {
|
|
|
+ position: fixed;
|
|
|
+ background: #1e1b2e;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 4px;
|
|
|
+ min-width: 140px;
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.ctx-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: transparent;
|
|
|
+ color: var(--text-primary, #e0e0e0);
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.12s;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.ctx-item:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.ctx-item.ctx-danger {
|
|
|
+ color: #f87171;
|
|
|
+}
|
|
|
+
|
|
|
+.ctx-item.ctx-danger:hover {
|
|
|
+ background: rgba(239, 68, 68, 0.15);
|
|
|
+}
|
|
|
</style>
|