fix(canvas): 连接点外置、拖线平滑跟手、图片失败显示状态码
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user