From 5a0cbeb4292b045fdb376c67a181b263b0ea9ddf Mon Sep 17 00:00:00 2001 From: chiyi Date: Mon, 12 Oct 2026 13:30:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E8=BF=9E=E7=BA=BF=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E4=B8=BB=E9=A2=98=E8=89=B2=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api.js | 16 +++++-- .../src/components/canvas/CanvasNodeView.vue | 44 +++++++++++++++++-- .../src/components/canvas/CanvasStage.vue | 9 ++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/frontend/src/api.js b/frontend/src/api.js index 1aef72d..8e8628f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -47,13 +47,23 @@ export async function fetchHealth() { return r.json() } -/** Absolute URL for a generated artifact (works in dev via proxy too). */ +/** Absolute URL for a generated artifact (works in dev via proxy too). + * Older canvas documents may contain "/images/..." or a full URL instead of + * the storage key; normalize all three forms instead of double-prefixing. */ export function generatedUrl(name) { - return `${BASE}/images/${name}` + const s = String(name || '').trim() + if (!s) return '' + const marker = '/images/' + const i = s.indexOf(marker) + if (i >= 0) return /^https?:\/\//i.test(s) ? s : `${BASE}${s.slice(i)}` + if (/^https?:\/\//i.test(s)) return s + return `${BASE}/images/${s.replace(/^\/?images\//, '')}` } /** Small thumbnail URL for list views. The server falls back to the original * when no thumbnail object exists (old images), so this is always safe. */ export function thumbUrl(name) { - return `${BASE}/images/${name}.thumb.jpg` + const url = generatedUrl(name) + if (!url || url.endsWith('.thumb.jpg') || !url.includes('/images/')) return url + return `${url}.thumb.jpg` } diff --git a/frontend/src/components/canvas/CanvasNodeView.vue b/frontend/src/components/canvas/CanvasNodeView.vue index 0745eff..cfc2ab0 100644 --- a/frontend/src/components/canvas/CanvasNodeView.vue +++ b/frontend/src/components/canvas/CanvasNodeView.vue @@ -2,8 +2,8 @@ // 单个画布节点。位置/尺寸由 stage 的世界层 transform 统一缩放, // 这里只负责节点自身内容:image / note / gen(生成中占位)。 // 性能:contain 隔离布局;图片 lazy + async 解码 + 缩略图。 -import { ref, onMounted, watch } from 'vue' -import { thumbUrl } from '../../api' +import { computed, ref, onMounted, watch } from 'vue' +import { generatedUrl, thumbUrl } from '../../api' const props = defineProps({ node: { type: Object, required: true }, @@ -15,6 +15,12 @@ const portSides = ['top', 'right', 'bottom', 'left'] const portLabels = { top: '上', right: '右', bottom: '下', left: '左' } const imgLoaded = ref(false) +const imgBroken = ref(false) +const imgThumb = computed(() => thumbUrl(props.node.src)) +const imgOriginal = computed(() => generatedUrl(props.node.src)) +const imgOriginalAbs = computed(() => { + try { return new URL(imgOriginal.value, window.location.href).href } catch { return imgOriginal.value } +}) const noteEl = ref(null) const editing = ref(false) @@ -34,8 +40,17 @@ watch( }, ) +watch( + () => props.node.src, + () => { + imgLoaded.value = false + imgBroken.value = false + }, +) + function onImgLoad(e) { imgLoaded.value = true + imgBroken.value = false const img = e.target // 按真实长宽比校正节点尺寸(以长边 512 归一,保持画布视觉密度一致)。 if (img.naturalWidth && img.naturalHeight) { @@ -48,6 +63,15 @@ function onImgLoad(e) { } } +function onImgError(e) { + if (e.target.src !== imgOriginalAbs.value) { + e.target.src = imgOriginal.value + return + } + imgLoaded.value = true + imgBroken.value = true +} + function startEdit() { if (props.node.type !== 'note') return editing.value = true @@ -119,15 +143,17 @@ defineExpose({ startEdit, commitNote }) @@ -186,6 +212,16 @@ defineExpose({ startEdit, commitNote }) display: block; background: var(--surface-2); } +.cv-node__missing { + width: 100%; + height: 100%; + display: grid; + place-items: center; + color: var(--fg-3); + font-size: 13px; + font-weight: 600; + background: var(--surface-2); +} .cv-node__cap { position: absolute; diff --git a/frontend/src/components/canvas/CanvasStage.vue b/frontend/src/components/canvas/CanvasStage.vue index 25d00b7..e1759c9 100644 --- a/frontend/src/components/canvas/CanvasStage.vue +++ b/frontend/src/components/canvas/CanvasStage.vue @@ -610,17 +610,20 @@ defineExpose({ } .cv-edge { fill: none; - stroke: var(--ink-soft); - stroke-width: 2; + stroke: var(--m-orange-2); + stroke-width: 2.4; + filter: drop-shadow(0 0 5px rgb(34 211 238 / 0.35)); } .cv-edge--hot { stroke: var(--m-orange); - stroke-width: 2.5; + stroke-width: 2.8; + filter: drop-shadow(0 0 7px var(--accent-glow)); } .cv-edge--sel { stroke: var(--m-orange); stroke-width: 3.5; stroke-dasharray: 10 6; + filter: drop-shadow(0 0 8px var(--accent-glow)); } .cv-edge-hit { fill: none;