fix(canvas): 连线改用主题色并修复图片回退
This commit is contained in:
+13
-3
@@ -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`
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
<!-- 图片节点 -->
|
||||
<template v-if="node.type === 'image'">
|
||||
<img
|
||||
v-if="!imgBroken"
|
||||
class="cv-node__img"
|
||||
:src="thumbUrl(node.src)"
|
||||
:src="imgThumb"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
draggable="false"
|
||||
alt=""
|
||||
@load="onImgLoad"
|
||||
@error="imgLoaded = true"
|
||||
@error="onImgError"
|
||||
>
|
||||
<div v-else class="cv-node__missing">图片不可用</div>
|
||||
<div v-if="node.prompt" class="cv-node__cap" :title="node.prompt">{{ node.prompt }}</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user