fix(canvas): 连接点外置、拖线平滑跟手、图片失败显示状态码

This commit is contained in:
2026-10-12 13:42:26 +08:00
parent c429209e50
commit c559e712b6
2 changed files with 40 additions and 14 deletions
@@ -16,6 +16,7 @@ const portLabels = { top: '上', right: '右', bottom: '下', left: '左' }
const imgLoaded = ref(false) const imgLoaded = ref(false)
const imgBroken = ref(false) const imgBroken = ref(false)
const imgStatus = ref('')
const imgThumb = computed(() => thumbUrl(props.node.src)) const imgThumb = computed(() => thumbUrl(props.node.src))
const imgOriginal = computed(() => generatedUrl(props.node.src)) const imgOriginal = computed(() => generatedUrl(props.node.src))
const imgOriginalAbs = computed(() => { const imgOriginalAbs = computed(() => {
@@ -70,6 +71,10 @@ function onImgError(e) {
} }
imgLoaded.value = true imgLoaded.value = true
imgBroken.value = true imgBroken.value = true
imgStatus.value = ''
fetch(imgOriginal.value, { credentials: 'include' })
.then((r) => { imgStatus.value = String(r.status) })
.catch(() => { imgStatus.value = '网络错误' })
} }
function startEdit() { function startEdit() {
@@ -140,6 +145,7 @@ defineExpose({ startEdit, commitNote })
<button title="置顶" @click.stop="emit('node-action', { type: 'front' })">⤒</button> <button title="置顶" @click.stop="emit('node-action', { type: 'front' })">⤒</button>
<button class="cv-tools__del" title="删除" @click.stop="emit('node-action', { type: 'delete' })">✕</button> <button class="cv-tools__del" title="删除" @click.stop="emit('node-action', { type: 'delete' })">✕</button>
</div> </div>
<div class="cv-node__clip">
<!-- 图片节点 --> <!-- 图片节点 -->
<template v-if="node.type === 'image'"> <template v-if="node.type === 'image'">
<img <img
@@ -153,7 +159,7 @@ defineExpose({ startEdit, commitNote })
@load="onImgLoad" @load="onImgLoad"
@error="onImgError" @error="onImgError"
> >
<div v-else class="cv-node__missing">图片不可用</div> <div v-else class="cv-node__missing">图片不可用<small v-if="imgStatus">HTTP {{ imgStatus }}</small></div>
<div v-if="node.prompt" class="cv-node__cap" :title="node.prompt">{{ node.prompt }}</div> <div v-if="node.prompt" class="cv-node__cap" :title="node.prompt">{{ node.prompt }}</div>
</template> </template>
@@ -180,6 +186,7 @@ defineExpose({ startEdit, commitNote })
<span class="cv-node__err" :title="node.error || ''">{{ node.error || '请检查提示词后重试' }}</span> <span class="cv-node__err" :title="node.error || ''">{{ node.error || '请检查提示词后重试' }}</span>
</template> </template>
</div> </div>
</div>
</div> </div>
</template> </template>
@@ -188,8 +195,7 @@ defineExpose({ startEdit, commitNote })
position: absolute; position: absolute;
border-radius: 14px; border-radius: 14px;
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
overflow: hidden; contain: layout style;
contain: layout style paint;
user-select: none; user-select: none;
touch-action: none; touch-action: none;
cursor: grab; cursor: grab;
@@ -197,6 +203,12 @@ defineExpose({ startEdit, commitNote })
box-shadow: var(--glass-shadow); box-shadow: var(--glass-shadow);
transition: box-shadow 0.15s ease, border-color 0.15s ease; transition: box-shadow 0.15s ease, border-color 0.15s ease;
} }
.cv-node__clip {
position: absolute;
inset: 0;
border-radius: 13px;
overflow: hidden;
}
.cv-node.is-selected { .cv-node.is-selected {
border-color: rgb(124 92 252 / 0.7); border-color: rgb(124 92 252 / 0.7);
box-shadow: var(--glass-shadow-lg), 0 0 0 3px rgb(124 92 252 / 0.25), 0 10px 30px var(--accent-glow); box-shadow: var(--glass-shadow-lg), 0 0 0 3px rgb(124 92 252 / 0.25), 0 10px 30px var(--accent-glow);
@@ -212,9 +224,12 @@ defineExpose({ startEdit, commitNote })
display: block; display: block;
background: var(--surface-2); background: var(--surface-2);
} }
.cv-node__missing small { display: block; margin-top: 4px; font-weight: 500; opacity: 0.7; }
.cv-node__missing { .cv-node__missing {
width: 100%; width: 100%;
height: 100%; height: 100%;
align-content: center;
text-align: center;
display: grid; display: grid;
place-items: center; place-items: center;
color: var(--fg-3); color: var(--fg-3);
@@ -341,10 +356,10 @@ html.dark .cv-node__note { color: rgb(255 255 255 / 0.88); }
} }
.cv-port.is-active::after { background: var(--m-orange); } .cv-port.is-active::after { background: var(--m-orange); }
.cv-port:focus-visible { outline: 2px solid var(--m-orange); outline-offset: -2px; border-radius: 50%; } .cv-port:focus-visible { outline: 2px solid var(--m-orange); outline-offset: -2px; border-radius: 50%; }
.cv-port--top { top: 0; left: 50%; transform: translateX(-50%); } .cv-port--top { top: -17px; left: 50%; transform: translateX(-50%); }
.cv-port--right { right: 0; top: 50%; transform: translateY(-50%); } .cv-port--right { right: -17px; top: 50%; transform: translateY(-50%); }
.cv-port--bottom { bottom: 0; left: 50%; transform: translateX(-50%); } .cv-port--bottom { bottom: -17px; left: 50%; transform: translateX(-50%); }
.cv-port--left { left: 0; top: 50%; transform: translateY(-50%); } .cv-port--left { left: -17px; top: 50%; transform: translateY(-50%); }
/* 右下角缩放方块(节点 contain:paint,只能放内侧) */ /* 右下角缩放方块(节点 contain:paint,只能放内侧) */
.cv-size { .cv-size {
+18 -7
View File
@@ -55,10 +55,10 @@ const nodeById = computed(() => {
}) })
function portPoint(n, side) { function portPoint(n, side) {
if (side === 'top') return { x: n.x + n.w / 2, y: n.y + 16 } if (side === 'top') return { x: n.x + n.w / 2, y: n.y }
if (side === 'right') return { x: n.x + n.w - 16, y: n.y + n.h / 2 } if (side === 'right') return { x: n.x + n.w, y: n.y + n.h / 2 }
if (side === 'bottom') return { x: n.x + n.w / 2, y: n.y + n.h - 16 } if (side === 'bottom') return { x: n.x + n.w / 2, y: n.y + n.h }
if (side === 'left') return { x: n.x + 16, y: n.y + n.h / 2 } if (side === 'left') return { x: n.x, y: n.y + n.h / 2 }
return { x: n.x + n.w / 2, y: n.y + n.h / 2 } return { x: n.x + n.w / 2, y: n.y + n.h / 2 }
} }
function portDirection(side, fallback) { function portDirection(side, fallback) {
@@ -186,7 +186,13 @@ const edgePaths = computed(() => {
const c2 = shifted(p2, v2, d) const c2 = shifted(p2, v2, d)
let path let path
if (manualBend) { if (manualBend) {
path = roundedPath([p1, shifted(p1, v1, 30), manualBend, shifted(p2, v2, 30), p2]) const L = Math.hypot(p2.x - p1.x, p2.y - p1.y) || 1
const tx = (p2.x - p1.x) / L
const ty = (p2.y - p1.y) / L
const d1 = Math.max(40, Math.hypot(manualBend.x - p1.x, manualBend.y - p1.y) * 0.5)
const d2 = Math.max(40, Math.hypot(p2.x - manualBend.x, p2.y - manualBend.y) * 0.5)
path = `M ${p1.x} ${p1.y} C ${p1.x + v1.x * d1} ${p1.y + v1.y * d1}, ${manualBend.x - tx * d1} ${manualBend.y - ty * d1}, ${manualBend.x} ${manualBend.y} `
+ `C ${manualBend.x + tx * d2} ${manualBend.y + ty * d2}, ${p2.x + v2.x * d2} ${p2.y + v2.y * d2}, ${p2.x} ${p2.y}`
} else { } else {
path = `M ${p1.x} ${p1.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${p2.x} ${p2.y}` path = `M ${p1.x} ${p1.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${p2.x} ${p2.y}`
const obstacles = props.nodes const obstacles = props.nodes
@@ -441,8 +447,13 @@ function applyMove(e) {
clearSelection() clearSelection()
} }
const cur = screenToWorld(p.x, p.y, vp) const cur = screenToWorld(p.x, p.y, vp)
act.edge.bendX = Math.round(act.bendX + cur.x - act.start.x) const a = nodeById.value.get(act.edge.from)
act.edge.bendY = Math.round(act.bendY + cur.y - act.start.y) const b = nodeById.value.get(act.edge.to)
if (!a || !b) return
const p1 = portPoint(a, act.edge.fromSide)
const p2 = portPoint(b, act.edge.toSide)
act.edge.bendX = Math.round(cur.x - (p1.x + p2.x) / 2)
act.edge.bendY = Math.round(cur.y - (p1.y + p2.y) / 2)
return return
} }
if (act.mode === 'resize') { if (act.mode === 'resize') {