From e1ef9fb718354ddadc274b4d2d259d2c15de48b2 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 14:27:23 +0800 Subject: [PATCH] auto-save 2026-05-13 14:27 (~4) --- .memory/worklog.json | 7 + web/app/page.tsx | 10 +- web/components/nodes/index.tsx | 38 +++--- web/components/storyboard-bar.tsx | 208 ++++++++++++++++++++++++------ 4 files changed, 195 insertions(+), 68 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index adb88ef..7e30f63 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1662,6 +1662,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 14:16 (~1)", "files_changed": 2 + }, + { + "ts": "2026-05-13T14:21:50+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 14:21 (~2)", + "hash": "36dcc7d", + "files_changed": 2 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index 4e7b146..7321c1d 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -15,7 +15,6 @@ import { import { ThemeToggle } from "@/components/theme-toggle" import { Dashboard, type DashboardHandle } from "@/components/dashboard" import { StoryboardBar } from "@/components/storyboard-bar" -import { StoryboardEditor } from "@/components/storyboard-editor" import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" @@ -326,7 +325,8 @@ export default function Home() {
- {/* 分镜头编排专属面板 — imagegen 节点 / storyboard bar 缩略图点击进入 */} - setStoryboardFrame(null)} - /> ) diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index b30fd98..c76bbce 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -680,34 +680,38 @@ export function ImageGenNode({ data, selected }: any) { )} - {/* Portal hover 大预览 — 视口居中 */} - {mounted && hoverKey && job && (() => { - const [fi, ei] = hoverKey.split("_") + {/* Portal hover 大预览 — 浮在缩略图上方 */} + {mounted && hover && job && (() => { + const [fi, ei] = hover.key.split("_") const frameIdx = parseInt(fi, 10) const p = elementCrops.find((x) => x.frameIdx === frameIdx && x.elementId === ei) if (!p) return null + const vidAspect = job.height > 0 ? job.height / 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 } + 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 -
- {p.name} - 来自分镜 {p.frameIdx + 1} · 点击进入分镜头编排 +
+ {p.name} + 来自分镜 {p.frameIdx + 1}
, diff --git a/web/components/storyboard-bar.tsx b/web/components/storyboard-bar.tsx index eee3ae2..5121c9e 100644 --- a/web/components/storyboard-bar.tsx +++ b/web/components/storyboard-bar.tsx @@ -1,16 +1,17 @@ "use client" import { useEffect, useRef, useState } from "react" import { createPortal } from "react-dom" -import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react" -import { type Job, type KeyFrame, effectiveFrameUrl } from "@/lib/api" +import { LayoutGrid, ChevronDown, ChevronUp, Sparkle, X, Wand2, Brush } from "lucide-react" +import { type Job, type KeyFrame, effectiveFrameUrl, cutoutUrl } from "@/lib/api" interface Props { job: Job | null selectedFrames: Set - onOpenStoryboard: (idx: number) => void + focusedFrame: number | null // 当前 focus 的分镜(imagegen 节点 / bar 缩略图点击触发) + onFocusFrame: (idx: number | null) => void } -export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) { +export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame }: Props) { const [collapsed, setCollapsed] = useState(false) const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), []) @@ -20,7 +21,6 @@ export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) if (!job) return null - // 按时间序排已选用的分镜 const frames = job.frames .filter((f) => selectedFrames.has(f.index)) .sort((a, b) => a.timestamp - b.timestamp) @@ -31,6 +31,16 @@ export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) 0, ) + // focused 分镜数据 + const focusFrame = focusedFrame !== null + ? job.frames.find((f) => f.index === focusedFrame) ?? null + : null + const focusSeq = focusFrame + ? job.frames.filter((f) => selectedFrames.has(f.index) && f.timestamp <= focusFrame.timestamp).length + : 0 + const focusElements = focusFrame?.elements ?? [] + const focusCutCount = focusElements.filter((e) => e.cutout_id).length + return (
{/* header */} @@ -41,18 +51,36 @@ export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) {frames.length} 分镜 · {totalElements} 元素 - - · 组织分镜画面 → 为生成视频做准备 - + {focusFrame ? ( + + · 编排中:分镜 {focusSeq} + + ) : ( + + · 组织分镜画面 → 为生成视频做准备 + + )} +
+
+ {focusFrame && ( + + )} +
-
{/* thumbnails row */} @@ -67,18 +95,23 @@ export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) const elementCount = f.elements?.filter((e) => e.cutout_id).length ?? 0 const totalElCount = f.elements?.length ?? 0 const cleaned = f.cleaned_applied + const isFocused = focusedFrame === f.index return ( + + +
+
- , - document.body, + )} + + {/* Hover 大图预览 · 浮在缩略图下方(不挡其他界面) */} + {mounted && hover && (() => { + const vidAspect = job.height > 0 ? job.height / job.width : 16 / 9 + const maxH = Math.min(window.innerHeight * 0.7, window.innerHeight - hover.rect.bottom - 16) + const maxW = Math.min(window.innerWidth * 0.6, 600) + let h = maxH, w = h / vidAspect + if (w > maxW) { w = maxW; h = w * vidAspect } + 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 +
+ 分镜 {hover.seq} · {hover.frame.timestamp.toFixed(2)}s + 点击进入编排 +
+
+
, + document.body, + ) + })()} ) }