auto-save 2026-05-13 13:53 (+1, ~4)
This commit is contained in:
@@ -1602,6 +1602,13 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-13 13:42 (+1, ~4)",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T13:48:39+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-13 13:48 (~3)",
|
||||
"hash": "2d297ec",
|
||||
"files_changed": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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"
|
||||
|
||||
@@ -65,6 +66,7 @@ export default function Home() {
|
||||
const [selectedFrames, setSelectedFrames] = useState<Set<number>>(new Set())
|
||||
const [expandedFrame, setExpandedFrame] = useState<number | null>(null)
|
||||
const [videoLightboxOpen, setVideoLightboxOpen] = useState(false)
|
||||
const [storyboardFrame, setStoryboardFrame] = useState<number | null>(null)
|
||||
const dashboardRef = useRef<DashboardHandle>(null)
|
||||
|
||||
// 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active
|
||||
@@ -259,6 +261,7 @@ export default function Home() {
|
||||
onOpenPanel: (key: string) => dashboardRef.current?.openPanel(key),
|
||||
onDeleteFrame: handleDeleteFrame,
|
||||
onDeleteGenerated: handleDeleteGenerated,
|
||||
onOpenStoryboard: (idx: number) => setStoryboardFrame(idx),
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
@@ -323,7 +326,7 @@ export default function Home() {
|
||||
<StoryboardBar
|
||||
job={job}
|
||||
selectedFrames={selectedFrames}
|
||||
onExpandFrame={setExpandedFrame}
|
||||
onOpenStoryboard={setStoryboardFrame}
|
||||
/>
|
||||
<div className="relative flex-1 min-h-0">
|
||||
<ReactFlow
|
||||
@@ -357,6 +360,13 @@ export default function Home() {
|
||||
onClose={() => setVideoLightboxOpen(false)}
|
||||
onAddFrame={handleAddManualFrame}
|
||||
/>
|
||||
|
||||
{/* 分镜头编排专属面板 — imagegen 节点 / storyboard bar 缩略图点击进入 */}
|
||||
<StoryboardEditor
|
||||
job={job}
|
||||
frameIndex={storyboardFrame}
|
||||
onClose={() => setStoryboardFrame(null)}
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface NodeData {
|
||||
onOpenPanel?: (key: string) => void // 控制 sidebar 哪个 drawer 展开
|
||||
onDeleteFrame?: (idx: number) => void // 删整张关键帧
|
||||
onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图
|
||||
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
|
||||
}
|
||||
|
||||
/* ---- 状态映射工具 ---- */
|
||||
|
||||
@@ -7,10 +7,10 @@ import { type Job, type KeyFrame, effectiveFrameUrl } from "@/lib/api"
|
||||
interface Props {
|
||||
job: Job | null
|
||||
selectedFrames: Set<number>
|
||||
onExpandFrame: (idx: number) => void
|
||||
onOpenStoryboard: (idx: number) => void
|
||||
}
|
||||
|
||||
export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
|
||||
export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
@@ -71,7 +71,7 @@ export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
|
||||
<button
|
||||
key={f.index}
|
||||
ref={(el) => { btnRefs.current[f.index] = el }}
|
||||
onClick={() => onExpandFrame(f.index)}
|
||||
onClick={() => onOpenStoryboard(f.index)}
|
||||
onMouseEnter={() => {
|
||||
const el = btnRefs.current[f.index]
|
||||
if (el) setHover({ frame: f, seq: i + 1, rect: el.getBoundingClientRect() })
|
||||
|
||||
173
web/components/storyboard-editor.tsx
Normal file
173
web/components/storyboard-editor.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
"use client"
|
||||
import { useEffect, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { X, LayoutGrid, Sparkle, Wand2, Brush } from "lucide-react"
|
||||
import { type Job, effectiveFrameUrl, cutoutUrl } from "@/lib/api"
|
||||
|
||||
interface Props {
|
||||
job: Job | null
|
||||
frameIndex: number | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function StoryboardEditor({ job, frameIndex, onClose }: Props) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
useEffect(() => {
|
||||
if (frameIndex === null) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }
|
||||
window.addEventListener("keydown", onKey)
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [frameIndex, onClose])
|
||||
|
||||
if (!mounted || !job || frameIndex === null) return null
|
||||
const frame = job.frames.find((f) => f.index === frameIndex)
|
||||
if (!frame) return null
|
||||
|
||||
const elements = frame.elements ?? []
|
||||
const elementsWithCutout = elements.filter((e) => e.cutout_id)
|
||||
const seq = job.frames.filter((f) => f.timestamp <= frame.timestamp).length
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-[150] bg-black/80 backdrop-blur-xl flex items-center justify-center p-6"
|
||||
style={{ animation: "drawer-in 0.2s cubic-bezier(0.32, 0.72, 0, 1)" }}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-5xl bg-black/60 rounded-2xl border border-white/15 overflow-hidden flex flex-col"
|
||||
style={{ maxHeight: "88vh" }}
|
||||
>
|
||||
{/* 顶部栏 — 分镜头编排用紫粉渐变 */}
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2.5 text-white"
|
||||
style={{ background: "linear-gradient(135deg, #d946ef, #ec4899)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
<span className="text-[14px] font-semibold">分镜头编排</span>
|
||||
<span className="text-[11px] text-white/70 font-mono ml-2">
|
||||
分镜 {seq} · {frame.timestamp.toFixed(2)}s
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center"
|
||||
title="关闭 (Esc)"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 主体 — 左大图 + 右素材 / 操作 */}
|
||||
<div className="flex gap-4 p-4 overflow-hidden flex-1 min-h-0">
|
||||
{/* 左:分镜大图 */}
|
||||
<div className="flex-shrink-0 flex flex-col gap-2" style={{ width: 360 }}>
|
||||
<img
|
||||
src={effectiveFrameUrl(job.id, frame)}
|
||||
alt={`frame ${frame.index}`}
|
||||
className="w-full rounded-lg object-contain"
|
||||
style={{ maxHeight: "65vh" }}
|
||||
/>
|
||||
<div className="text-[10.5px] text-white/50 text-center">
|
||||
{frame.cleaned_applied ? "✨ 已清洗版本" : "原图"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右:元素 + Phase 2 操作占位 */}
|
||||
<div className="flex-1 min-w-0 overflow-y-auto space-y-3">
|
||||
<section>
|
||||
<div className="text-[12.5px] font-semibold text-white mb-1.5 flex items-center gap-1.5">
|
||||
<Sparkle className="h-3.5 w-3.5 text-violet-300" />
|
||||
该分镜的元素清单
|
||||
<span className="text-[10px] text-white/40 font-mono">
|
||||
· {elementsWithCutout.length}/{elements.length} 已裁切
|
||||
</span>
|
||||
</div>
|
||||
{elements.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-white/15 p-3 text-[11px] text-white/40">
|
||||
暂无元素 · 到关键帧节点画框提取后会出现在这里
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{elements.map((e) => (
|
||||
<div key={e.id} className="rounded-md bg-white/[0.04] border border-white/10 p-1.5">
|
||||
<div className="w-full aspect-square rounded bg-black/40 overflow-hidden mb-1">
|
||||
{e.cutout_id ? (
|
||||
<img
|
||||
src={cutoutUrl(job.id, frame.index, e.id)}
|
||||
alt={e.name_zh}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full inline-flex items-center justify-center">
|
||||
<Sparkle className="h-4 w-4 text-white/20" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10.5px] text-white truncate">{e.name_zh}</div>
|
||||
<div className="text-[9px] text-white/40 font-mono truncate">{e.name_en || "(无英文)"}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 编排操作占位(Phase 2) */}
|
||||
<section className="rounded-lg border border-dashed border-violet-300/30 bg-violet-500/5 p-3">
|
||||
<div className="text-[12.5px] font-semibold text-white mb-2 flex items-center gap-1.5">
|
||||
<Wand2 className="h-3.5 w-3.5 text-violet-300" />
|
||||
编排操作 · Phase 2 待实施
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
disabled
|
||||
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
|
||||
📐 多视角
|
||||
</div>
|
||||
<div className="text-[9.5px] text-white/45 leading-tight">
|
||||
侧面 / 仰视 / 俯视等同元素的不同角度
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
disabled
|
||||
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
|
||||
<Brush className="h-3 w-3" /> 风格融合
|
||||
</div>
|
||||
<div className="text-[9.5px] text-white/45 leading-tight">
|
||||
元素 + 自由风格 prompt 生成新画面
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
disabled
|
||||
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
|
||||
🎬 布局调整
|
||||
</div>
|
||||
<div className="text-[9.5px] text-white/45 leading-tight">
|
||||
拖动元素 / 调整画面结构
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] text-white/35 leading-relaxed">
|
||||
这些功能稍后填入。当前先用关键帧节点的「清洗 + 提取」准备好素材。
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部 */}
|
||||
<div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
|
||||
ESC 关闭
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user