From 3ab9da094a61402988a158175f71b06b813a82c3 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 02:53:06 +0800 Subject: [PATCH] auto-save 2026-05-14 02:52 (~4) --- .memory/worklog.json | 13 +++ docs/source-analysis.html | 12 +++ web/components/nodes/hover-preview.tsx | 131 ++++++++++++++++++++++--- web/components/nodes/index.tsx | 70 ++++++------- 4 files changed, 177 insertions(+), 49 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 60852f9..d1bb45e 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index e041afa..645b04e 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -815,6 +815,18 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-14 · 大图预览改为尺寸感知 Fit / 1:1

+ Canvas + HoverPreview +
+
+

问题:有些源视频和图片分辨率很大,完全按原尺寸展示会撑爆画布;但统一固定预览大小又会丢失素材真实尺寸感知。

+

改动:HoverPreview 默认按原比例适应可视区域,角标显示原始分辨率和当前 Fit 百分比;点击钉住后可切换到 1:1,超大素材在预览框内滚动查看局部。

+

影响:web/components/nodes/hover-preview.tsx。视觉缩略图高度同步放大,图片缩略图的复制 / 删除按钮改为常驻大号 icon。

+
+

2026-05-14 · 缩略图滑动条改为大号可拖轨道

diff --git a/web/components/nodes/hover-preview.tsx b/web/components/nodes/hover-preview.tsx index b3d8179..7ca0976 100644 --- a/web/components/nodes/hover-preview.tsx +++ b/web/components/nodes/hover-preview.tsx @@ -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(null) + const videoRef = useRef(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 (
+ style={previewFrameStyle}> + {shown && ( +
+ {metadataText} +
+ )} {videoSrc ? (
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 4fc4a4d..f4be8c9 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -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 }} > @@ -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" > - 📋 + )} @@ -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" > - 📋 + )} @@ -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" > - + )} @@ -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" > - + )} @@ -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" > - + )} @@ -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="删除这个视频任务" > - + )}
@@ -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" > - 📋 + )} - {/* 删除按钮:hover 时右上角浮出 */} + {/* 删除按钮:常驻可见 */} {d.onDeleteFrame && ( )} @@ -1544,7 +1546,7 @@ export function StoryboardNode({ data, selected }: any) {
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" > - 📋 + )} - {/* 右上:删除(hover 浮出) */} + {/* 右上:删除 */} {d.onDeleteCutout && ( )}
@@ -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" > - + )})}