refactor: rewrite editor with immutable state, async-safe Vue Flow, and loading guard
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { watch, onMounted, nextTick } from 'vue'
|
||||
import { watch, nextTick } from 'vue'
|
||||
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
import '@vue-flow/core/dist/style.css'
|
||||
import '@vue-flow/controls/dist/style.css'
|
||||
import '@vue-flow/core/dist/theme-default.css'
|
||||
import type { Node, Edge, Connection } from '@vue-flow/core'
|
||||
import type { Connection } from '@vue-flow/core'
|
||||
|
||||
const props = defineProps<{
|
||||
sceneNodes: { id: string; label: string }[]
|
||||
@@ -17,82 +17,82 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectNode: [id: string]
|
||||
addNode: []
|
||||
addEdge: [source: string, target: string]
|
||||
}>()
|
||||
|
||||
const { nodes, edges, onNodeClick, onConnect, fitView } = useVueFlow()
|
||||
|
||||
function buildNodes() {
|
||||
function makeNodes() {
|
||||
return props.sceneNodes.map((n, i) => ({
|
||||
id: n.id,
|
||||
type: 'default',
|
||||
position: { x: (i % 4) * 220 + 50, y: Math.floor(i / 4) * 120 + 50 },
|
||||
data: { label: n.id === props.startScene ? `▶ ${n.label}` : n.label },
|
||||
data: { label: n.id === props.startScene ? `\u25b6 ${n.label}` : n.label },
|
||||
style: n.id === props.startScene
|
||||
? 'background:#1b5e20; color:#fff; border-color:#388e3c'
|
||||
? { background: '#1b5e20', color: '#fff', borderColor: '#388e3c' }
|
||||
: n.id === props.selectedNodeId
|
||||
? 'background:#1565c0; color:#fff; border-color:#1976d2'
|
||||
: '',
|
||||
? { background: '#1565c0', color: '#fff', borderColor: '#1976d2' }
|
||||
: {},
|
||||
sourcePosition: 'right' as const,
|
||||
targetPosition: 'left' as const,
|
||||
connectable: true,
|
||||
}))
|
||||
}
|
||||
|
||||
function buildEdges() {
|
||||
function makeEdges() {
|
||||
return props.sceneEdges.map((e) => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
label: e.label || '',
|
||||
label: e.label ?? '',
|
||||
animated: true,
|
||||
style: { stroke: '#4fc3f7' },
|
||||
labelStyle: { fill: '#aaa', fontWeight: 400, fontSize: 11 },
|
||||
labelBgStyle: { fill: 'rgba(0,0,0,0.7)' },
|
||||
}))
|
||||
}
|
||||
|
||||
let lastNodesLen = 0
|
||||
let lastEdgesLen = 0
|
||||
let prevNodeCount = -1
|
||||
let prevEdgeCount = -1
|
||||
let didFitView = false
|
||||
let rebuildSeq = 0
|
||||
|
||||
async function structuralRebuild() {
|
||||
const seq = ++rebuildSeq
|
||||
edges.value = []
|
||||
await nextTick()
|
||||
nodes.value = buildNodes()
|
||||
if (seq !== rebuildSeq) return
|
||||
nodes.value = makeNodes()
|
||||
await nextTick()
|
||||
edges.value = buildEdges()
|
||||
lastNodesLen = props.sceneNodes.length
|
||||
lastEdgesLen = props.sceneEdges.length
|
||||
if (seq !== rebuildSeq) return
|
||||
edges.value = makeEdges()
|
||||
prevNodeCount = props.sceneNodes.length
|
||||
prevEdgeCount = props.sceneEdges.length
|
||||
if (!didFitView && nodes.value.length > 0) {
|
||||
didFitView = true
|
||||
setTimeout(() => fitView({ padding: 0.2 }), 80)
|
||||
}
|
||||
}
|
||||
|
||||
function styleOnlyRebuild() {
|
||||
nodes.value = buildNodes()
|
||||
edges.value = buildEdges()
|
||||
function inlineRebuild() {
|
||||
nodes.value = makeNodes()
|
||||
edges.value = makeEdges()
|
||||
}
|
||||
|
||||
watch(() => [props.sceneNodes, props.sceneEdges, props.startScene, props.selectedNodeId], () => {
|
||||
const nodesLen = props.sceneNodes.length
|
||||
const edgesLen = props.sceneEdges.length
|
||||
if (nodesLen !== lastNodesLen || edgesLen !== lastEdgesLen) {
|
||||
structuralRebuild()
|
||||
} else {
|
||||
styleOnlyRebuild()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => [props.sceneNodes, props.sceneEdges, props.startScene, props.selectedNodeId] as const,
|
||||
() => {
|
||||
const nc = props.sceneNodes.length
|
||||
const ec = props.sceneEdges.length
|
||||
if (nc !== prevNodeCount || ec !== prevEdgeCount) {
|
||||
structuralRebuild()
|
||||
} else {
|
||||
inlineRebuild()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onNodeClick((event) => {
|
||||
emit('selectNode', event.node.id)
|
||||
})
|
||||
onNodeClick((ev) => emit('selectNode', ev.node.id))
|
||||
|
||||
onConnect((connection: Connection) => {
|
||||
if (connection.source && connection.target) {
|
||||
emit('addEdge', connection.source, connection.target)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => fitView({ padding: 0.2 }), 100)
|
||||
onConnect((conn: Connection) => {
|
||||
if (conn.source && conn.target) emit('addEdge', conn.source, conn.target)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user