|
|
|
@ -1,42 +1,45 @@ |
|
|
|
<template> |
|
|
|
<VueFlow |
|
|
|
:nodes="localNodes" |
|
|
|
:edges="localEdges" |
|
|
|
@nodes-change="onNodesChange" |
|
|
|
@edges-change="onEdgesChange" |
|
|
|
@connect="handleConnect" |
|
|
|
@node-click="$emit('node-click', $event)" |
|
|
|
@pane-click="onPaneClickLocal" |
|
|
|
@drop="$emit('drop', $event)" |
|
|
|
@dragover.prevent |
|
|
|
@edge-click="onEdgeClick" |
|
|
|
@contextmenu="onContextMenu" |
|
|
|
> |
|
|
|
<Background :gap="20" :size="1" /> |
|
|
|
<Controls position="bottom-right" /> |
|
|
|
<MiniMap |
|
|
|
position="bottom-left" |
|
|
|
:node-stroke-color="'#409EFF'" |
|
|
|
:node-color="nodeColor" |
|
|
|
pannable |
|
|
|
zoomable |
|
|
|
/> |
|
|
|
|
|
|
|
<template #node-custom="nodeProps"> |
|
|
|
<FlowNode |
|
|
|
:id="nodeProps.id" |
|
|
|
:data="nodeProps.data" |
|
|
|
:selected="nodeProps.selected" |
|
|
|
@delete="$emit('node-delete', nodeProps.id)" |
|
|
|
<div style="width:100%;height:100%"> |
|
|
|
<VueFlow |
|
|
|
v-if="ready" |
|
|
|
:nodes="props.nodes" |
|
|
|
:edges="props.edges" |
|
|
|
@nodes-change="onNodesChange" |
|
|
|
@edges-change="onEdgesChange" |
|
|
|
@node-click="(e: any) => $emit('node-click', e)" |
|
|
|
@pane-click="onPaneClickLocal" |
|
|
|
@connect="handleConnect" |
|
|
|
@drop="(e: any) => $emit('drop', e)" |
|
|
|
@dragover.prevent |
|
|
|
@edge-click="onEdgeClick" |
|
|
|
@contextmenu="onContextMenu" |
|
|
|
> |
|
|
|
<Background :gap="20" :size="1" /> |
|
|
|
<Controls position="bottom-right" /> |
|
|
|
<MiniMap |
|
|
|
position="bottom-left" |
|
|
|
:node-stroke-color="'#409EFF'" |
|
|
|
:node-color="props.nodeColor" |
|
|
|
pannable |
|
|
|
zoomable |
|
|
|
/> |
|
|
|
</template> |
|
|
|
</VueFlow> |
|
|
|
|
|
|
|
<template #node-custom="nodeProps"> |
|
|
|
<FlowNode |
|
|
|
:id="nodeProps.id" |
|
|
|
:data="nodeProps.data" |
|
|
|
:selected="nodeProps.selected" |
|
|
|
@delete="$emit('node-delete', nodeProps.id)" |
|
|
|
/> |
|
|
|
</template> |
|
|
|
</VueFlow> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, watch, nextTick } from 'vue' |
|
|
|
import { ref, watch } from 'vue' |
|
|
|
import { MarkerType } from '@vue-flow/core' |
|
|
|
import { VueFlow, useVueFlow } from '@vue-flow/core' |
|
|
|
import { VueFlow } from '@vue-flow/core' |
|
|
|
import { Background } from '@vue-flow/background' |
|
|
|
import { Controls } from '@vue-flow/controls' |
|
|
|
import { MiniMap } from '@vue-flow/minimap' |
|
|
|
@ -57,113 +60,43 @@ const emit = defineEmits([ |
|
|
|
'update:nodes', 'update:edges', |
|
|
|
]) |
|
|
|
|
|
|
|
const localNodes = ref<any[]>([]) |
|
|
|
const localEdges = ref<any[]>([]) |
|
|
|
const ready = ref(true) |
|
|
|
const selectedEdgeId = ref<string | null>(null) |
|
|
|
let skipNextNodesChange = false |
|
|
|
|
|
|
|
let isExternalUpdate = false |
|
|
|
let isInitialized = false |
|
|
|
let pendingAdditions = new Map<string, any>() |
|
|
|
watch(() => props.nodes, () => { |
|
|
|
skipNextNodesChange = true |
|
|
|
}) |
|
|
|
|
|
|
|
watch(() => props.nodes, async (newNodes) => { |
|
|
|
if (isExternalUpdate) { |
|
|
|
isExternalUpdate = false |
|
|
|
function onNodesChange(changes: any[]) { |
|
|
|
if (skipNextNodesChange) { |
|
|
|
skipNextNodesChange = false |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if (!newNodes || newNodes.length === 0) return |
|
|
|
|
|
|
|
const existingNodeMap = new Map(localNodes.value.map(n => [n.id, { position: n.position, data: n.data }])) |
|
|
|
const positionChanges = changes.filter((c: any) => c.type === 'position') |
|
|
|
if (positionChanges.length === 0) return |
|
|
|
|
|
|
|
const mergedNodes = newNodes.map(newNode => { |
|
|
|
const pending = pendingAdditions.get(newNode.id) |
|
|
|
if (pending) { |
|
|
|
pendingAdditions.delete(newNode.id) |
|
|
|
return { |
|
|
|
...newNode, |
|
|
|
position: { ...pending.position }, |
|
|
|
data: { ...newNode.data }, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const existing = existingNodeMap.get(newNode.id) |
|
|
|
if (existing) { |
|
|
|
return { |
|
|
|
...newNode, |
|
|
|
position: { ...existing.position }, |
|
|
|
data: { ...newNode.data }, |
|
|
|
} |
|
|
|
const updatedNodes = props.nodes.map((node: any) => { |
|
|
|
const change = positionChanges.find((c: any) => c.id === node.id) |
|
|
|
if (change && change.position) { |
|
|
|
return { ...node, position: { x: change.position.x, y: change.position.y } } |
|
|
|
} |
|
|
|
|
|
|
|
return { ...newNode, data: { ...newNode.data } } |
|
|
|
return node |
|
|
|
}) |
|
|
|
|
|
|
|
localNodes.value = mergedNodes |
|
|
|
|
|
|
|
if (!isInitialized) { |
|
|
|
isInitialized = true |
|
|
|
} |
|
|
|
}, { immediate: true }) |
|
|
|
|
|
|
|
watch(() => props.edges, (newEdges) => { |
|
|
|
if (isExternalUpdate) { |
|
|
|
isExternalUpdate = false |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if (!newEdges || newEdges.length === 0) return |
|
|
|
|
|
|
|
localEdges.value = JSON.parse(JSON.stringify(newEdges)) |
|
|
|
}, { immediate: true }) |
|
|
|
|
|
|
|
function onNodesChange(changes: any[]) { |
|
|
|
if (isExternalUpdate) { |
|
|
|
isExternalUpdate = false |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
const positionChanges = changes.filter((c: any) => c.type === 'position') |
|
|
|
if (positionChanges.length > 0 && localNodes.value.length > 0) { |
|
|
|
const updatedNodes = localNodes.value.map((node: any) => { |
|
|
|
const change = positionChanges.find((c: any) => c.id === node.id) |
|
|
|
if (change) { |
|
|
|
return { |
|
|
|
...node, |
|
|
|
position: { |
|
|
|
x: change.position?.x ?? node.position?.x, |
|
|
|
y: change.position?.y ?? node.position?.y, |
|
|
|
}, |
|
|
|
} |
|
|
|
} |
|
|
|
return node |
|
|
|
}) |
|
|
|
|
|
|
|
localNodes.value = updatedNodes |
|
|
|
} |
|
|
|
emit('update:nodes', updatedNodes) |
|
|
|
} |
|
|
|
|
|
|
|
function onEdgesChange(changes: any[]) { |
|
|
|
if (isExternalUpdate) { |
|
|
|
isExternalUpdate = false |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
const removeChanges = changes.filter((c: any) => c.type === 'remove') |
|
|
|
if (removeChanges.length > 0) { |
|
|
|
const removeIds = new Set(removeChanges.map((c: any) => c.id)) |
|
|
|
localEdges.value = localEdges.value.filter((e: any) => !removeIds.has(e.id)) |
|
|
|
} |
|
|
|
|
|
|
|
const selectChanges = changes.filter((c: any) => c.type === 'select') |
|
|
|
if (selectChanges.length > 0) { |
|
|
|
const selected = selectChanges.find((c: any) => c.selected) |
|
|
|
selectedEdgeId.value = selected ? selected.id : null |
|
|
|
const updatedEdges = props.edges.filter((e: any) => !removeIds.has(e.id)) |
|
|
|
emit('update:edges', updatedEdges) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const store = useVueFlow() |
|
|
|
|
|
|
|
const selectedEdgeId = ref<string | null>(null) |
|
|
|
|
|
|
|
function onEdgeClick({ edge }: any) { |
|
|
|
selectedEdgeId.value = edge.id |
|
|
|
} |
|
|
|
@ -180,7 +113,8 @@ function onContextMenu(event: MouseEvent) { |
|
|
|
event.preventDefault() |
|
|
|
const edgeId = edgeEl.getAttribute('data-id') |
|
|
|
if (edgeId) { |
|
|
|
localEdges.value = localEdges.value.filter((e: any) => e.id !== edgeId) |
|
|
|
const updatedEdges = props.edges.filter((e: any) => e.id !== edgeId) |
|
|
|
emit('update:edges', updatedEdges) |
|
|
|
selectedEdgeId.value = null |
|
|
|
} |
|
|
|
return |
|
|
|
@ -189,7 +123,7 @@ function onContextMenu(event: MouseEvent) { |
|
|
|
|
|
|
|
function handleConnect(connection: any) { |
|
|
|
const sourceHandle = connection.sourceHandle || 'source' |
|
|
|
const edge: any = { |
|
|
|
const newEdge: any = { |
|
|
|
id: `edge_${connection.source}_${connection.target}_${sourceHandle}_${Date.now()}`, |
|
|
|
source: connection.source, |
|
|
|
target: connection.target, |
|
|
|
@ -202,34 +136,17 @@ function handleConnect(connection: any) { |
|
|
|
? { stroke: '#E53935', strokeWidth: 3 } |
|
|
|
: { stroke: '#1976D2', strokeWidth: 3 }, |
|
|
|
} |
|
|
|
localEdges.value.push(edge) |
|
|
|
emit('update:edges', [...props.edges, newEdge]) |
|
|
|
emit('connect', connection) |
|
|
|
} |
|
|
|
|
|
|
|
function getSnapshot() { |
|
|
|
return { nodes: JSON.parse(JSON.stringify(localNodes.value)), edges: JSON.parse(JSON.stringify(localEdges.value)) } |
|
|
|
return { nodes: JSON.parse(JSON.stringify(props.nodes)), edges: JSON.parse(JSON.stringify(props.edges)) } |
|
|
|
} |
|
|
|
|
|
|
|
defineExpose({ |
|
|
|
undo: () => (store as any).undo?.(), |
|
|
|
redo: () => (store as any).redo?.(), |
|
|
|
get canUndo() { return !!(store as any).canUndo }, |
|
|
|
get canRedo() { return !!(store as any).canRedo }, |
|
|
|
get canUndo() { return false }, |
|
|
|
get canRedo() { return false }, |
|
|
|
getSnapshot, |
|
|
|
addNodes: (newNodes: any[]) => { |
|
|
|
newNodes.forEach(n => { |
|
|
|
pendingAdditions.set(n.id, { position: { ...n.position } }) |
|
|
|
}) |
|
|
|
|
|
|
|
localNodes.value = [...localNodes.value, ...newNodes] |
|
|
|
|
|
|
|
nextTick(() => { |
|
|
|
pendingAdditions.clear() |
|
|
|
}) |
|
|
|
}, |
|
|
|
removeNodes: (ids: string[]) => { |
|
|
|
localNodes.value = localNodes.value.filter((n: any) => !ids.includes(n.id)) |
|
|
|
localEdges.value = localEdges.value.filter((e: any) => !ids.includes(e.source) && !ids.includes(e.target)) |
|
|
|
}, |
|
|
|
}) |
|
|
|
</script> |
|
|
|
</script> |