From 910651ddba1173952df0a6bd54fe1bae47edf49a Mon Sep 17 00:00:00 2001 From: dokploy-bot Date: Sun, 20 Sep 2026 00:06:08 +0800 Subject: [PATCH] feat(canvas): manual node linking + no-model hint + render-hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drag from a node's side port onto another node to create an edge (dashed live preview, dedupe, self-link rejected); click an edge to select (highlighted), Delete removes it, Ctrl+Z restores — all in undo history and autosave - generate panel shows an explicit hint when the site has no image models instead of an empty dropdown - render hardening: viewport-culling size now has a hard floor (max(stageSize, window, 1600x900)) so a delayed/degenerate ResizeObserver reading can never cull nodes out of the DOM; stage listeners attach with retry + rAF self-healing measure; pointer capture wrapped in try/catch for synthetic pointers --- .../src/components/canvas/CanvasNodeView.vue | 22 ++ .../src/components/canvas/CanvasStage.vue | 189 ++++++++++++++++-- frontend/src/views/CanvasView.vue | 39 +++- 3 files changed, 224 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/canvas/CanvasNodeView.vue b/frontend/src/components/canvas/CanvasNodeView.vue index a59d6fa..4c1b7f1 100644 --- a/frontend/src/components/canvas/CanvasNodeView.vue +++ b/frontend/src/components/canvas/CanvasNodeView.vue @@ -99,6 +99,8 @@ defineExpose({ startEdit, commitNote }) :data-node-id="node.id" @dblclick="onSelfDblClick" > + + @@ -375,7 +506,6 @@ defineExpose({ width: 1px; height: 1px; overflow: visible; - pointer-events: none; } .cv-edge { fill: none; @@ -386,6 +516,25 @@ defineExpose({ stroke: #7c6cff; stroke-width: 2; } +.cv-edge--sel { + stroke: #ff7b8f; + stroke-width: 2.5; + filter: drop-shadow(0 0 3px rgb(255 123 143 / 0.6)); +} +.cv-edge-hit { + fill: none; + stroke: transparent; + stroke-width: 16; + pointer-events: stroke; + cursor: pointer; +} +.cv-linkline { + fill: none; + stroke: #a99dff; + stroke-width: 2; + stroke-dasharray: 7 6; + pointer-events: none; +} .cv-marquee { position: absolute; border: 1px solid #7c6cff; diff --git a/frontend/src/views/CanvasView.vue b/frontend/src/views/CanvasView.vue index 069bca0..fec013f 100644 --- a/frontend/src/views/CanvasView.vue +++ b/frontend/src/views/CanvasView.vue @@ -323,14 +323,28 @@ function viewportCenterWorld() { } function deleteSelection() { - if (!selectionIds.value.length) return const dead = new Set(selectionIds.value) - doc.nodes = doc.nodes.filter((n) => !dead.has(n.id)) - doc.edges = doc.edges.filter((e) => !dead.has(e.from) && !dead.has(e.to)) + const deadEdges = stage.value?.consumeSelectedEdges() || [] + if (!dead.size && !deadEdges.length) return + if (dead.size) { + doc.nodes = doc.nodes.filter((n) => !dead.has(n.id)) + doc.edges = doc.edges.filter((e) => !dead.has(e.from) && !dead.has(e.to)) + } + if (deadEdges.length) { + const gone = new Set(deadEdges) + doc.edges = doc.edges.filter((e) => !gone.has(e.id)) + } selectionIds.value = [] pushHistory() } +// ---- 手动连线 ---- +function onLinkCreate({ from, to }) { + if (doc.edges.some((e) => e.from === from && e.to === to)) return // 去重 + doc.edges.push({ id: newId('e'), from, to }) + pushHistory() +} + // ---- 画布管理 ---- async function switchCanvas(id) { @@ -557,6 +571,7 @@ function onKeydown(e) { e.preventDefault() deleteSelection() } else if (e.key === 'Escape') { + stage.value?.cancelLink() stage.value?.clearSelection() previewSrc.value = '' pickerOpen.value = false @@ -660,6 +675,7 @@ const saveLabel = computed(() => ({ :edges="doc.edges" @selection="onSelection" @move-end="onMoveEnd" + @link-create="onLinkCreate" @note-commit="onNoteCommit" @img-size="onImgSize" @viewport-rest="onViewportRest" @@ -672,11 +688,14 @@ const saveLabel = computed(() => ({ @@ -934,6 +953,14 @@ const saveLabel = computed(() => ({ color: #9aa1b2; word-break: break-all; } +.cv-nomodels { + font-size: 11px; + color: #d8a657; + line-height: 1.6; + border: 1px dashed rgb(216 166 87 / 0.4); + border-radius: 8px; + padding: 8px 10px; +} .cv-tips { font-size: 11px; color: #5f6675;