节点面板
设计画布
清空
-
-
-
-
-
-
-
-
-
+ @node-delete="removeNode"
+ />
@@ -209,26 +176,16 @@
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
-import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
-import { Background } from '@vue-flow/background'
-import { Controls } from '@vue-flow/controls'
-import { MiniMap } from '@vue-flow/minimap'
-import '@vue-flow/core/dist/style.css'
-import '@vue-flow/core/dist/theme-default.css'
-import '@vue-flow/controls/dist/style.css'
-import '@vue-flow/minimap/dist/style.css'
-import { flowApi, mcpApi, agentApi } from '@/api'
-import {
- Promotion, ChatDotRound, Tools, Connection, Bell,
- DataAnalysis, Search,
-} from '@element-plus/icons-vue'
-import FlowNode from './FlowNode.vue'
+import { Promotion, ChatDotRound, Tools, Connection, Bell, DataAnalysis, Search } from '@element-plus/icons-vue'
+import FlowCanvas from './FlowCanvas.vue'
const route = useRoute()
const router = useRouter()
const flowId = computed(() => route.params.id as string)
const isEdit = computed(() => !!flowId.value)
+const initError = ref('')
+
const flowName = ref('新工作流')
const flowDesc = ref('')
const flowStatus = ref('')
@@ -238,6 +195,12 @@ const selectedNodeData = ref
({})
const mcpServers = ref([])
const agentList = ref([])
+const nodes = ref([])
+const edges = ref([])
+const canvasRef = ref(null)
+
+let nodeCounter = 0
+
const nodeTypes = [
{ type: 'trigger', label: '触发节点', icon: Promotion, typeDesc: '企微触发' },
{ type: 'llm', label: 'LLM处理', icon: ChatDotRound, typeDesc: 'AI处理' },
@@ -260,24 +223,18 @@ const colorMap: Record = {
output: '#722ed1',
}
-const vueFlowStore = useVueFlow()
-const { nodes, edges, addNodes, addEdges, removeNodes, removeEdges, toObject } = vueFlowStore
-const storeAny = vueFlowStore as any
-const undo = () => storeAny.undo?.()
-const redo = () => storeAny.redo?.()
-const canUndo = computed(() => !!(storeAny.canUndo ?? false))
-const canRedo = computed(() => !!(storeAny.canRedo ?? false))
-let nodeCounter = 0
+function undo() { canvasRef.value?.undo() }
+function redo() { canvasRef.value?.redo() }
+const canUndo = computed(() => canvasRef.value?.canUndo ?? false)
+const canRedo = computed(() => canvasRef.value?.canRedo ?? false)
-const selectedNode = computed(() => {
- return nodes.value.find((n: any) => n.id === selectedNodeId.value) || null
-})
+const selectedNode = computed(() => nodes.value.find((n: any) => n.id === selectedNodeId.value) || null)
function getMiniMapColor(node: any) {
return colorMap[node?.data?.type || node?.type] || '#409EFF'
}
-function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) {
+function onDragStart(event: DragEvent, node: any) {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', JSON.stringify(node))
event.dataTransfer.effectAllowed = 'move'
@@ -287,74 +244,31 @@ function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) {
function onDrop(event: DragEvent) {
const dataStr = event.dataTransfer?.getData('application/vueflow')
if (!dataStr) return
-
const nodeData = JSON.parse(dataStr)
-
- let position = { x: 100, y: 100 }
- try {
- const bounds = (event.currentTarget as HTMLElement)?.getBoundingClientRect()
- if (bounds) {
- position = {
- x: event.clientX - bounds.left - 80,
- y: event.clientY - bounds.top - 20,
- }
- }
- } catch { /* fallback */ }
-
const id = `node_${++nodeCounter}`
- const newNode = {
+ canvasRef.value?.addNodes?.([{
id,
type: 'custom',
- position,
+ position: { x: 100 + nodeCounter * 50, y: 100 + nodeCounter * 30 },
data: {
label: nodeData.label,
type: nodeData.type,
typeDesc: nodeData.typeDesc,
color: colorMap[nodeData.type] || '#409EFF',
- config: nodeData.type === 'llm'
- ? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' }
- : {},
+ config: nodeData.type === 'llm' ? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' } : {},
},
draggable: true,
connectable: true,
- }
- addNodes([newNode])
-}
-
-function onConnect(connection: any) {
- const sourceHandle = connection.sourceHandle || 'source'
- const edgeId = `edge_${connection.source}_${connection.target}_${sourceHandle}`
-
- const isConditionFalse = sourceHandle === 'false'
- const newEdge = {
- id: edgeId,
- source: connection.source,
- target: connection.target,
- sourceHandle: connection.sourceHandle,
- targetHandle: connection.targetHandle,
- type: 'smoothstep',
- animated: true,
- markerEnd: MarkerType.ArrowClosed,
- style: isConditionFalse
- ? { stroke: '#F56C6C', strokeWidth: 2 }
- : { stroke: '#409EFF', strokeWidth: 2 },
- }
- addEdges([newEdge])
+ }])
}
function onNodeClick({ node }: any) {
selectedNodeId.value = node.id
const d = node.data || {}
- selectedNodeData.value = {
- label: d.label || '',
- typeDesc: d.typeDesc || '',
- ...(d.config || {}),
- }
+ selectedNodeData.value = { label: d.label || '', typeDesc: d.typeDesc || '', ...(d.config || {}) }
}
-function onPaneClick() {
- selectedNodeId.value = ''
-}
+function onPaneClick() { selectedNodeId.value = '' }
function onConfigLabelChange() {
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value)
@@ -379,288 +293,121 @@ function onAgentSelect(val: string) {
function onConfigChange() {
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value)
if (idx === -1) return
-
const found = nodes.value[idx]
const nodeType = found.data.type
const cfg: any = {}
-
- if (nodeType === 'trigger') {
- cfg.event_type = selectedNodeData.value.event_type
- } else if (nodeType === 'llm') {
- cfg.system_prompt = selectedNodeData.value.system_prompt
- cfg.model = selectedNodeData.value.model
- cfg.temperature = selectedNodeData.value.temperature
- cfg.agent_id = selectedNodeData.value.agent_id
- } else if (nodeType === 'tool') {
- cfg.tool_name = selectedNodeData.value.tool_name
- } else if (nodeType === 'mcp') {
- cfg.mcp_server = selectedNodeData.value.mcp_server
- cfg.tool_name = selectedNodeData.value.tool_name
- } else if (nodeType === 'wecom_notify') {
- cfg.message_template = selectedNodeData.value.message_template
- cfg.target = selectedNodeData.value.target
- } else if (nodeType === 'condition') {
- cfg.condition = selectedNodeData.value.condition
- } else if (nodeType === 'rag') {
- cfg.knowledge_base = selectedNodeData.value.knowledge_base
- cfg.top_k = selectedNodeData.value.top_k
- } else if (nodeType === 'output') {
- cfg.format = selectedNodeData.value.format
- }
-
+ if (nodeType === 'trigger') cfg.event_type = selectedNodeData.value.event_type
+ else if (nodeType === 'llm') { cfg.system_prompt = selectedNodeData.value.system_prompt; cfg.model = selectedNodeData.value.model; cfg.temperature = selectedNodeData.value.temperature; cfg.agent_id = selectedNodeData.value.agent_id }
+ else if (nodeType === 'tool') cfg.tool_name = selectedNodeData.value.tool_name
+ else if (nodeType === 'mcp') { cfg.mcp_server = selectedNodeData.value.mcp_server; cfg.tool_name = selectedNodeData.value.tool_name }
+ else if (nodeType === 'wecom_notify') { cfg.message_template = selectedNodeData.value.message_template; cfg.target = selectedNodeData.value.target }
+ else if (nodeType === 'condition') cfg.condition = selectedNodeData.value.condition
+ else if (nodeType === 'rag') { cfg.knowledge_base = selectedNodeData.value.knowledge_base; cfg.top_k = selectedNodeData.value.top_k }
+ else if (nodeType === 'output') cfg.format = selectedNodeData.value.format
const updated = { ...found }
updated.data = { ...found.data, config: cfg }
nodes.value[idx] = updated
}
function removeNode(id: string) {
- removeNodes([id])
- edges.value = edges.value.filter((e: any) => e.source !== id && e.target !== id)
+ canvasRef.value?.removeNodes?.([id])
if (selectedNodeId.value === id) selectedNodeId.value = ''
}
-function clearCanvas() {
- nodes.value = []
- edges.value = []
- nodeCounter = 0
- selectedNodeId.value = ''
-}
-
-function onNodesChange() {}
-function onEdgesChange() {}
-
-async function saveFlow() {
- if (!flowName.value) { ElMessage.warning('请输入流名称'); return }
- saving.value = true
- try {
- const serializedNodes = nodes.value.map((n: any) => ({
- id: n.id,
- type: n.data?.type || n.type,
- label: n.data?.label || n.id,
- config: n.data?.config || {},
- position: n.position,
- }))
- const serializedEdges = edges.value.map((e: any) => ({
- source: e.source,
- target: e.target,
- sourceHandle: e.sourceHandle || 'source',
- condition: e.sourceHandle === 'false' ? 'false' : (e.sourceHandle === 'true' ? 'true' : undefined),
- }))
-
- const payload = { name: flowName.value, description: flowDesc.value, nodes: serializedNodes, edges: serializedEdges, trigger: {} }
-
- if (isEdit.value) {
- await flowApi.updateFlow(flowId.value, payload)
- ElMessage.success('保存成功')
- } else {
- const res: any = await flowApi.createFlow(payload)
- const data = res?.data || res || {}
- if (data.id) {
- router.replace(`/admin/flow/editor/${data.id}`)
- ElMessage.success('创建成功,请继续编辑')
- }
- }
- } finally {
- saving.value = false
- }
-}
-
-async function testFlow() {
- if (!isEdit.value) { ElMessage.info('请先保存再进行验证'); return }
- try {
- const res: any = await flowApi.testFlow(flowId.value)
- const data = res?.data || res || {}
- if (data.valid) {
- ElMessage.success(`验证通过: ${data.node_count}个节点, ${data.edge_count}条边`)
- } else {
- ElMessage.warning(`验证问题: ${(data.issues || []).join(', ')}`)
- }
- } catch { /**/ }
-}
-
-async function publishFlow() {
- if (!isEdit.value) { ElMessage.warning('请先保存'); return }
- await flowApi.publishFlow(flowId.value)
- ElMessage.success('流已上架到企微')
- await loadFlow()
-}
+function clearCanvas() { nodes.value = []; edges.value = []; nodeCounter = 0; selectedNodeId.value = '' }
async function loadFlow() {
if (!isEdit.value) return
try {
+ const { flowApi } = await import('@/api')
const res: any = await flowApi.getFlow(flowId.value)
const flow = res?.data || res || {}
flowName.value = flow.name || ''
flowDesc.value = flow.description || ''
flowStatus.value = flow.status || ''
-
const definition = flow.definition_json || {}
const loadedNodes: any[] = []
const loadedEdges: any[] = []
-
const defNodes = definition.nodes || []
for (let i = 0; i < defNodes.length; i++) {
const n = defNodes[i]
const nt = nodeTypes.find(t => t.type === n.type)
loadedNodes.push({
- id: n.id || `node_${i}`,
- type: 'custom',
+ id: n.id || `node_${i}`, type: 'custom',
position: n.position || { x: 100 + (i % 4) * 250, y: 60 + Math.floor(i / 4) * 150 },
- data: {
- label: n.label || n.id || `节点${i}`,
- type: n.type,
- typeDesc: nt?.typeDesc || n.type,
- color: colorMap[n.type] || '#409EFF',
- config: n.config || {},
- },
- draggable: true,
- connectable: true,
+ data: { label: n.label || n.id || `节点${i}`, type: n.type, typeDesc: nt?.typeDesc || n.type, color: colorMap[n.type] || '#409EFF', config: n.config || {} },
+ draggable: true, connectable: true,
})
}
-
const defEdges = definition.edges || []
for (const e of defEdges) {
const source = e.source || e.from
const target = e.target || e.to
const cond = e.condition || e.sourceHandle
- const isFalse = cond === 'false'
- loadedEdges.push({
- id: e.id || `edge_${source}_${target}`,
- source,
- target,
- sourceHandle: cond || 'source',
- type: 'smoothstep',
- animated: true,
- markerEnd: MarkerType.ArrowClosed,
- style: isFalse
- ? { stroke: '#F56C6C', strokeWidth: 2 }
- : { stroke: '#409EFF', strokeWidth: 2 },
- })
+ loadedEdges.push({ id: e.id || `edge_${source}_${target}`, source, target, sourceHandle: cond || 'source', type: 'smoothstep', animated: true, style: cond === 'false' ? { stroke: '#F56C6C', strokeWidth: 2 } : { stroke: '#409EFF', strokeWidth: 2 } })
}
-
nodes.value = loadedNodes
edges.value = loadedEdges
nodeCounter = defNodes.length
- } catch { /**/ }
+ } catch (e: any) {
+ console.error('loadFlow error:', e)
+ }
}
async function loadMcpServers() {
- try {
- const res: any = await mcpApi.getServers()
- mcpServers.value = Array.isArray(res) ? res : (res?.data || [])
- } catch { /**/ }
+ try { const { mcpApi } = await import('@/api'); const res: any = await mcpApi.getServers(); mcpServers.value = Array.isArray(res) ? res : (res?.data || []) } catch {}
}
async function loadAgents() {
+ try { const { agentApi } = await import('@/api'); const res: any = await agentApi.getList(); agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) } catch {}
+}
+
+async function saveFlow() {
+ if (!flowName.value) { ElMessage.warning('请输入流名称'); return }
+ saving.value = true
try {
- const res: any = await agentApi.getList()
- agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
- } catch { /**/ }
+ const { flowApi } = await import('@/api')
+ const serializedNodes = nodes.value.map((n: any) => ({ id: n.id, type: n.data?.type || n.type, label: n.data?.label || n.id, config: n.data?.config || {}, position: n.position }))
+ const serializedEdges = edges.value.map((e: any) => ({ source: e.source, target: e.target, sourceHandle: e.sourceHandle || 'source', condition: e.sourceHandle === 'false' ? 'false' : (e.sourceHandle === 'true' ? 'true' : undefined) }))
+ const payload = { name: flowName.value, description: flowDesc.value, nodes: serializedNodes, edges: serializedEdges, trigger: {} }
+ if (isEdit.value) { await flowApi.updateFlow(flowId.value, payload); ElMessage.success('保存成功') }
+ else { const res: any = await flowApi.createFlow(payload); const data = res?.data || res || {}; if (data.id) { router.replace(`/admin/flow/editor/${data.id}`); ElMessage.success('创建成功') } }
+ } finally { saving.value = false }
+}
+
+async function testFlow() {
+ if (!isEdit.value) { ElMessage.info('请先保存再进行验证'); return }
+ try { const { flowApi } = await import('@/api'); const res: any = await flowApi.testFlow(flowId.value); const data = res?.data || res || {}; if (data.valid) { ElMessage.success(`验证通过: ${data.node_count}个节点, ${data.edge_count}条边`) } else { ElMessage.warning(`验证问题: ${(data.issues || []).join(', ')}`) } } catch {}
+}
+
+async function publishFlow() {
+ if (!isEdit.value) { ElMessage.warning('请先保存'); return }
+ try { const { flowApi } = await import('@/api'); await flowApi.publishFlow(flowId.value); ElMessage.success('流已上架到企微'); await loadFlow() } catch {}
}
onMounted(async () => {
- if (isEdit.value) {
- await loadFlow()
+ try {
+ if (isEdit.value) { await loadFlow() }
+ await Promise.allSettled([loadMcpServers(), loadAgents()])
+ } catch (e: any) {
+ console.error('FlowEditor init error:', e)
+ initError.value = '初始化失败: ' + (e?.message || '未知错误')
}
- await loadMcpServers()
- await loadAgents()
})
\ No newline at end of file
+.flow-editor-page { height: 100%; display: flex; flex-direction: column; }
+.editor-toolbar { display: flex; align-items: center; gap: 8px; }
+.editor-body { display: flex; gap: 12px; margin-top: 12px; flex: 1; min-height: 0; }
+.node-panel { width: 160px; background: #fff; border-radius: 4px; padding: 12px; border: 1px solid #ebeef5; overflow-y: auto; flex-shrink: 0; }
+.panel-title { font-weight: bold; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; }
+.node-item { display: flex; align-items: center; gap: 8px; padding: 8px; margin-bottom: 8px; border: 1px solid #e4e7ed; border-radius: 4px; cursor: grab; font-size: 13px; background: #f5f7fa; transition: all 0.2s; user-select: none; }
+.node-item:hover { border-color: #409EFF; background: #ecf5ff; }
+.panel-hint { font-size: 12px; color: #909399; line-height: 1.8; }
+.panel-hint p::before { content: '• '; color: #409EFF; }
+.canvas-wrapper { flex: 1; background: #fff; border-radius: 4px; border: 1px solid #ebeef5; display: flex; flex-direction: column; min-width: 0; }
+.canvas-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; border-bottom: 1px solid #ebeef5; font-weight: bold; flex-shrink: 0; }
+.config-panel { width: 280px; background: #fff; border-radius: 4px; padding: 12px; border: 1px solid #ebeef5; overflow-y: auto; flex-shrink: 0; }
+.config-actions { margin-top: 8px; padding-top: 8px; border-top: 1px solid #ebeef5; }
+