diff --git a/.memory/worklog.json b/.memory/worklog.json index 09201a4..adb88ef 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1649,6 +1649,19 @@ "message": "auto-save 2026-05-13 14:10 (~2)", "hash": "6d87afa", "files_changed": 2 + }, + { + "ts": "2026-05-13T14:16:19+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 14:16 (~1)", + "hash": "4983d9a", + "files_changed": 1 + }, + { + "ts": "2026-05-13T06:17:39Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 14:16 (~1)", + "files_changed": 2 } ] } diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index e72913c..b30fd98 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -1,5 +1,6 @@ "use client" -import { useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" +import { createPortal } from "react-dom" import { type NodeProps } from "@xyflow/react" import { Link2, Upload, Download, Scissors, Image as ImageIcon, @@ -345,6 +346,9 @@ export function KeyframeNode({ data, selected }: any) { const st = keyframeStatus(d.job) const frames = d.job?.frames ?? [] const jobId = d.job?.id + const [hover, setHover] = useState<{ idx: number; rect: DOMRect } | null>(null) + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) return (
@@ -372,7 +376,9 @@ export function KeyframeNode({ data, selected }: any) { > {/* 删除按钮:hover 时右上角浮出 */} {d.onDeleteFrame && ( @@ -486,6 +460,45 @@ export function KeyframeNode({ data, selected }: any) {
)} + + {/* Portal hover 大预览 — 浮在缩略图上方(不挡其他界面) */} + {mounted && hover && jobId && (() => { + const hf = frames.find((x) => x.index === hover.idx) + if (!hf) return null + // 大图最大尺寸(按视频比例算) + const vidAspect = d.job && d.job.height > 0 ? d.job.height / d.job.width : 16 / 9 + const maxH = Math.min(window.innerHeight * 0.7, hover.rect.top - 16) + const maxW = Math.min(window.innerWidth * 0.6, 600) + let h = maxH, w = h / vidAspect + if (w > maxW) { w = maxW; h = w * vidAspect } + // 水平居中到缩略图,clamp 在视口内 + const centerX = hover.rect.left + hover.rect.width / 2 + const left = Math.max(12, Math.min(window.innerWidth - w - 12, centerX - w / 2)) + return createPortal( +
+
+ {`preview +
+ 分镜 {hf.index + 1} · {hf.timestamp.toFixed(2)}s + 点击进入精细调整 +
+
+
, + document.body, + ) + })()} ) } @@ -589,6 +602,9 @@ const IMAGEGEN_WIDTH = 360 export function ImageGenNode({ data, selected }: any) { const d: NodeData = data const job = d?.job + const [hover, setHover] = useState<{ key: string; rect: DOMRect } | null>(null) + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) // 上方浮条 = 所有 frame 的 elements crop("分镜头编排"的输入素材) type ElPreview = { frameIdx: number; elementId: string; name: string } @@ -612,55 +628,30 @@ export function ImageGenNode({ data, selected }: any) { className="absolute left-0 right-0 grid grid-cols-5 gap-1.5" style={{ bottom: "calc(100% + 12px)" }} > - {elementCrops.map((p) => ( -
- -
- ))} + {p.name} + + + ) + })} )} @@ -688,6 +679,41 @@ export function ImageGenNode({ data, selected }: any) { )} + + {/* Portal hover 大预览 — 视口居中 */} + {mounted && hoverKey && job && (() => { + const [fi, ei] = hoverKey.split("_") + const frameIdx = parseInt(fi, 10) + const p = elementCrops.find((x) => x.frameIdx === frameIdx && x.elementId === ei) + if (!p) return null + return createPortal( +
+
+
+ {`preview +
+ {p.name} + 来自分镜 {p.frameIdx + 1} · 点击进入分镜头编排 +
+
+
, + document.body, + ) + })()}
) }