auto-save 2026-05-14 02:52 (~4)
This commit is contained in:
@@ -2953,6 +2953,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 02:41 (~4)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T02:47:36+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 02:47 (~1)",
|
||||
"hash": "b5d8eb1",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T18:48:48Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 02:47 (~1)",
|
||||
"files_changed": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -815,6 +815,18 @@ api/main.py
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 大图预览改为尺寸感知 Fit / 1:1</h3>
|
||||
<span class="tag violet">Canvas</span>
|
||||
<span class="tag blue">HoverPreview</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>有些源视频和图片分辨率很大,完全按原尺寸展示会撑爆画布;但统一固定预览大小又会丢失素材真实尺寸感知。</p>
|
||||
<p><strong>改动:</strong><code>HoverPreview</code> 默认按原比例适应可视区域,角标显示原始分辨率和当前 Fit 百分比;点击钉住后可切换到 <code>1:1</code>,超大素材在预览框内滚动查看局部。</p>
|
||||
<p><strong>影响:</strong><code>web/components/nodes/hover-preview.tsx</code>。视觉缩略图高度同步放大,图片缩略图的复制 / 删除按钮改为常驻大号 icon。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 缩略图滑动条改为大号可拖轨道</h3>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
import { X } from "lucide-react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Maximize2, Minimize2, X } from "lucide-react"
|
||||
|
||||
/**
|
||||
* 视觉类节点统一大预览:
|
||||
* - **在 ReactFlow 节点 DOM 内**作为 absolute 元素,底边贴当前缩略图上方边缘
|
||||
* - 跟随 ReactFlow 画布 pan/zoom 一起变化(属于"无限画布"的一部分)
|
||||
* - 媒体按"自然像素分辨率"渲染,不做 max 尺寸限制
|
||||
* - 默认按原比例 fit 到可视区域,并标注原始分辨率 / 当前缩放比例
|
||||
* - pinned 后可切换到 1:1;大图在预览框内滚动查看局部
|
||||
* - 不 pinned 时:pointer-events-none,依赖调用方传入 visible
|
||||
* - pinned=true:强制 visible,pointer-events 开启,可点 × 关闭
|
||||
* - 用法:渲染在节点根层,不要放进 overflow-x-auto 缩略图滚动条里
|
||||
@@ -36,9 +38,78 @@ export function HoverPreview({
|
||||
onClose,
|
||||
}: Props) {
|
||||
const shown = pinned || visible
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [naturalSize, setNaturalSize] = useState<{ width: number; height: number } | null>(null)
|
||||
const [actualSize, setActualSize] = useState(false)
|
||||
const [viewport, setViewport] = useState({ width: 1200, height: 800 })
|
||||
|
||||
useEffect(() => {
|
||||
setNaturalSize(null)
|
||||
setActualSize(false)
|
||||
}, [imgSrc, videoSrc])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pinned) setActualSize(false)
|
||||
}, [pinned])
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => setViewport({
|
||||
width: window.innerWidth || 1200,
|
||||
height: window.innerHeight || 800,
|
||||
})
|
||||
update()
|
||||
window.addEventListener("resize", update)
|
||||
return () => window.removeEventListener("resize", update)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shown) return
|
||||
const raf = window.requestAnimationFrame(() => {
|
||||
const img = imgRef.current
|
||||
if (img?.complete && img.naturalWidth && img.naturalHeight) {
|
||||
setNaturalSize({ width: img.naturalWidth, height: img.naturalHeight })
|
||||
return
|
||||
}
|
||||
const video = videoRef.current
|
||||
if (video?.videoWidth && video.videoHeight) {
|
||||
setNaturalSize({ width: video.videoWidth, height: video.videoHeight })
|
||||
}
|
||||
})
|
||||
return () => window.cancelAnimationFrame(raf)
|
||||
}, [imgSrc, shown, videoSrc])
|
||||
|
||||
const fitScale = useMemo(() => {
|
||||
if (!naturalSize) return null
|
||||
const maxWidth = Math.min(920, viewport.width * 0.7)
|
||||
const maxHeight = Math.min(820, viewport.height * 0.72)
|
||||
return Math.min(1, maxWidth / naturalSize.width, maxHeight / naturalSize.height)
|
||||
}, [naturalSize, viewport.height, viewport.width])
|
||||
|
||||
const metadataText = naturalSize
|
||||
? `原始 ${naturalSize.width}×${naturalSize.height} · ${actualSize ? "1:1" : `Fit ${Math.round((fitScale ?? 1) * 100)}%`}`
|
||||
: actualSize ? "1:1" : "Fit"
|
||||
|
||||
const visibilityCls = shown
|
||||
? pinned ? "opacity-100 pointer-events-auto" : "opacity-100 pointer-events-none"
|
||||
: "pointer-events-none opacity-0"
|
||||
const previewFrameStyle = actualSize
|
||||
? {
|
||||
width: "max-content",
|
||||
maxWidth: "min(78vw, 1040px)",
|
||||
maxHeight: "min(76vh, 860px)",
|
||||
overflow: "auto",
|
||||
}
|
||||
: {
|
||||
width: "max-content",
|
||||
maxWidth: "min(70vw, 920px)",
|
||||
maxHeight: "min(72vh, 820px)",
|
||||
overflow: "hidden",
|
||||
}
|
||||
const mediaStyle = actualSize
|
||||
? { width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }
|
||||
: { width: "auto", height: "auto", maxWidth: "min(70vw, 920px)", maxHeight: "min(72vh, 820px)" }
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute transition-all duration-150 z-[120] ${visibilityCls}`}
|
||||
@@ -51,9 +122,15 @@ export function HoverPreview({
|
||||
}}
|
||||
>
|
||||
<div className={`relative rounded-lg overflow-hidden border-2 bg-black shadow-2xl ${pinned ? "ring-2 ring-violet-400/70" : ""} ${borderClass}`}
|
||||
style={{ width: "max-content" }}>
|
||||
style={previewFrameStyle}>
|
||||
{shown && (
|
||||
<div className="absolute left-1.5 top-1.5 z-10 rounded-md bg-black/75 px-2 py-1 text-[10px] font-mono leading-none text-white shadow-lg backdrop-blur">
|
||||
{metadataText}
|
||||
</div>
|
||||
)}
|
||||
{videoSrc ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoSrc}
|
||||
poster={poster}
|
||||
muted
|
||||
@@ -62,16 +139,29 @@ export function HoverPreview({
|
||||
autoPlay
|
||||
preload="auto"
|
||||
className="block"
|
||||
style={{ width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }}
|
||||
onLoadedMetadata={(e) => { e.currentTarget.play().catch(() => {}) }}
|
||||
style={mediaStyle}
|
||||
onLoadedMetadata={(e) => {
|
||||
const video = e.currentTarget
|
||||
if (video.videoWidth && video.videoHeight) {
|
||||
setNaturalSize({ width: video.videoWidth, height: video.videoHeight })
|
||||
}
|
||||
video.play().catch(() => {})
|
||||
}}
|
||||
onCanPlay={(e) => { e.currentTarget.play().catch(() => {}) }}
|
||||
/>
|
||||
) : imgSrc ? (
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
className="block"
|
||||
style={{ width: "auto", height: "auto", maxWidth: "none", maxHeight: "none" }}
|
||||
style={mediaStyle}
|
||||
onLoad={(e) => {
|
||||
const img = e.currentTarget
|
||||
if (img.naturalWidth && img.naturalHeight) {
|
||||
setNaturalSize({ width: img.naturalWidth, height: img.naturalHeight })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-40 bg-black/40" style={{ aspectRatio: aspect ?? "1/1" }} />
|
||||
@@ -83,15 +173,26 @@ export function HoverPreview({
|
||||
</div>
|
||||
)}
|
||||
{pinned && onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onClose() }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title="取消固定预览"
|
||||
className="absolute right-1 top-1 h-7 w-7 rounded-full bg-black/75 text-white shadow-lg hover:bg-rose-500 inline-flex items-center justify-center z-10"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="absolute right-1 top-1 z-10 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setActualSize((x) => !x) }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title={actualSize ? "适应预览" : "1:1 查看"}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/75 text-white shadow-lg backdrop-blur hover:bg-violet-500"
|
||||
>
|
||||
{actualSize ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onClose() }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title="取消固定预览"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/75 text-white shadow-lg backdrop-blur hover:bg-rose-500"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,6 +104,8 @@ function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
const THUMBNAIL_HEIGHT = 176
|
||||
|
||||
function canvasThumbnailAnchor(root: HTMLDivElement | null, target: HTMLElement) {
|
||||
if (!root) return { x: 160, y: 0 }
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
@@ -356,7 +358,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
onClick={(e) => { e.stopPropagation(); fileRef.current?.click() }}
|
||||
title="再上传一个视频"
|
||||
className="shrink-0 rounded-md border border-dashed border-white/30 hover:border-white/50 bg-white/[0.04] hover:bg-white/[0.08] inline-flex items-center justify-center text-white/60 hover:text-white transition"
|
||||
style={{ width: 88, height: 160 }}
|
||||
style={{ width: 96, height: THUMBNAIL_HEIGHT }}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -370,7 +372,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
className={`group relative shrink-0 rounded-md overflow-visible border shadow-lg transition hover:-translate-y-0.5 ${
|
||||
isActive ? "border-violet-400 ring-2 ring-violet-400/60" : "border-white/25"
|
||||
}`}
|
||||
style={{ height: 160, aspectRatio: aspectStr }}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: aspectStr }}
|
||||
onMouseEnter={(e) => setHoverPreviewJob({ id: j.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreviewJob(null)}
|
||||
>
|
||||
@@ -765,7 +767,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
? isSelected ? "border-emerald-400 ring-2 ring-emerald-400/60" : "border-white/30 dark:border-white/20"
|
||||
: p.borderClass
|
||||
} ${p.kind === "cutout" ? "bg-white" : "bg-black"}`}
|
||||
style={{ height: 150, aspectRatio: aspect }}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: aspect }}
|
||||
onMouseEnter={(e) => setHoverPreview({ id: p.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreview(null)}
|
||||
>
|
||||
@@ -819,9 +821,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
d.onCopyImage?.({ kind: "keyframe", frame_idx: p.frameIdx, label: `${p.label} 关键帧` })
|
||||
}}
|
||||
title="复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 z-[70] h-5 w-5 rounded-full bg-violet-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center text-[10px] leading-none transition hover:bg-violet-400 hover:scale-110"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
📋
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -839,9 +841,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
})
|
||||
}}
|
||||
title="复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 z-[70] h-5 w-5 rounded-full bg-violet-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center text-[10px] leading-none transition hover:bg-violet-400 hover:scale-110"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
📋
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -855,10 +857,10 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
void navigator.clipboard?.writeText(video.prompt).catch(() => {})
|
||||
toast.success("已复制视频 prompt")
|
||||
}}
|
||||
className="absolute left-1 top-1 z-[70] h-5 w-5 rounded-full bg-violet-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center transition hover:bg-violet-400 hover:scale-110"
|
||||
className="absolute left-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
title="复制视频 prompt"
|
||||
>
|
||||
<Copy className="h-2.5 w-2.5" />
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -870,9 +872,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
if (confirm(`删除${p.label}?相关清洗 / 抠图 / 生成图都会一并清除。`)) d.onDeleteFrame?.(p.frameIdx)
|
||||
}}
|
||||
title="删除该关键帧"
|
||||
className="absolute top-1 right-1 z-[70] h-5 w-5 rounded-full bg-black/70 text-white/80 backdrop-blur inline-flex items-center justify-center opacity-0 transition hover:bg-rose-500 hover:text-white group-hover:opacity-100"
|
||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -886,9 +888,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
}
|
||||
}}
|
||||
title="删除该提取图"
|
||||
className="absolute top-1 right-1 z-[70] h-5 w-5 rounded-full bg-black/70 text-white/85 backdrop-blur inline-flex items-center justify-center opacity-0 transition hover:bg-rose-500 hover:text-white group-hover:opacity-100"
|
||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -899,10 +901,10 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
e.stopPropagation()
|
||||
d.onDeleteVideo?.(p.videoId)
|
||||
}}
|
||||
className="absolute right-1 top-1 z-[70] h-5 w-5 rounded-full bg-rose-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center transition hover:bg-rose-400 hover:scale-110"
|
||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
title="删除这个视频任务"
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1022,7 +1024,7 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
: "border-white/30 dark:border-white/20"
|
||||
}`}
|
||||
style={{
|
||||
height: 160,
|
||||
height: THUMBNAIL_HEIGHT,
|
||||
aspectRatio: d.job && d.job.height > 0
|
||||
? `${d.job.width}/${d.job.height}`
|
||||
: "16/9",
|
||||
@@ -1078,13 +1080,13 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
label: `分镜 ${f.index + 1} 关键帧`,
|
||||
})
|
||||
}}
|
||||
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
|
||||
title="复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
📋
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{/* 删除按钮:hover 时右上角浮出 */}
|
||||
{/* 删除按钮:常驻可见 */}
|
||||
{d.onDeleteFrame && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -1094,9 +1096,9 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
}
|
||||
}}
|
||||
title="删除该关键帧"
|
||||
className="absolute top-1 right-1 h-5 w-5 rounded-full bg-black/70 backdrop-blur text-white/80 hover:bg-rose-500 hover:text-white inline-flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-[70]"
|
||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1544,7 +1546,7 @@ export function StoryboardNode({ data, selected }: any) {
|
||||
<div
|
||||
key={key}
|
||||
className="group relative shrink-0 rounded-md border border-violet-300/50 overflow-visible transition shadow-lg hover:-translate-y-0.5 bg-white"
|
||||
style={{ height: 160, aspectRatio: aspect }}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: aspect }}
|
||||
onMouseEnter={(e) => setHoverPreviewCutout({ id: key, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreviewCutout(null)}
|
||||
>
|
||||
@@ -1579,13 +1581,13 @@ export function StoryboardNode({ data, selected }: any) {
|
||||
label: p.name,
|
||||
})
|
||||
}}
|
||||
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
|
||||
title="复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
📋
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{/* 右上:删除(hover 浮出) */}
|
||||
{/* 右上:删除 */}
|
||||
{d.onDeleteCutout && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -1595,9 +1597,9 @@ export function StoryboardNode({ data, selected }: any) {
|
||||
}
|
||||
}}
|
||||
title="删除该提取图"
|
||||
className="absolute top-1 right-1 h-5 w-5 rounded-full bg-black/70 backdrop-blur text-white/85 hover:bg-rose-500 hover:text-white inline-flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-[70]"
|
||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1695,7 +1697,7 @@ export function VideoGenNode({ data, selected }: any) {
|
||||
className={`group relative shrink-0 rounded-md border overflow-visible transition shadow-lg hover:-translate-y-0.5 bg-black ${
|
||||
ready ? "border-emerald-300/60" : v.status === "failed" ? "border-rose-300/70" : "border-violet-300/55"
|
||||
}`}
|
||||
style={{ height: 160, aspectRatio: aspect }}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: aspect }}
|
||||
onMouseEnter={(e) => setHoverPreviewVideo({ id: v.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreviewVideo(null)}
|
||||
>
|
||||
@@ -1747,10 +1749,10 @@ export function VideoGenNode({ data, selected }: any) {
|
||||
void navigator.clipboard?.writeText(v.prompt).catch(() => {})
|
||||
toast.success("已复制视频 prompt")
|
||||
}}
|
||||
className="absolute left-1 top-1 z-[70] h-5 w-5 rounded-full bg-violet-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center transition hover:bg-violet-400 hover:scale-110"
|
||||
className="absolute left-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
title="复制视频 prompt"
|
||||
>
|
||||
<Copy className="h-2.5 w-2.5" />
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1758,10 +1760,10 @@ export function VideoGenNode({ data, selected }: any) {
|
||||
e.stopPropagation()
|
||||
d.onDeleteVideo?.(v.id)
|
||||
}}
|
||||
className="absolute right-1 top-1 z-[70] h-5 w-5 rounded-full bg-rose-500/90 text-white shadow-md backdrop-blur inline-flex items-center justify-center transition hover:bg-rose-400 hover:scale-110"
|
||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
title="删除这个视频任务"
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)})}
|
||||
|
||||
Reference in New Issue
Block a user