auto-save 2026-05-13 20:07 (~5)

This commit is contained in:
2026-05-13 20:07:24 +08:00
parent 3f9075f2ce
commit 52c120cd50
5 changed files with 104 additions and 108 deletions

View File

@@ -2,17 +2,19 @@
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, hasCutout } from "@/lib/api"
import { type Job, effectiveFrameUrl, hasCutout } from "@/lib/api"
interface Props {
job: Job | null
selectedFrames: Set<number>
focusedFrame: number | null
onFocusFrame: (idx: number | null) => void
workbenchOpen?: boolean
onOpenWorkbench?: (frameIdx?: number) => void
onCloseWorkbench?: () => void
}
export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame, onOpenWorkbench }: Props) {
export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame, workbenchOpen = false, onOpenWorkbench, onCloseWorkbench }: Props) {
const [collapsed, setCollapsed] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
@@ -64,6 +66,10 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame,
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => {
if (workbenchOpen) {
onCloseWorkbench?.()
return
}
if (frames.length === 0) return
const nextFrame = focusedFrame ?? frames[0].index
if (focusedFrame === null) onFocusFrame(nextFrame)
@@ -72,13 +78,17 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame,
}}
disabled={frames.length === 0}
className="text-[11px] px-2.5 py-1 rounded-md bg-gradient-to-r from-violet-500 to-pink-500 hover:from-violet-400 hover:to-pink-400 text-white inline-flex items-center gap-1 disabled:opacity-40 disabled:cursor-not-allowed font-medium shadow"
title={frames.length === 0 ? "先到关键帧节点选用分镜" : "下拉展开分镜头编排"}
title={frames.length === 0 ? "先到关键帧节点选用分镜" : workbenchOpen ? "收起分镜头编排明细" : "下拉展开分镜头编排"}
>
<ChevronDown className="h-3 w-3" />
{workbenchOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
{workbenchOpen ? "收起编排" : "展开编排"}
</button>
<button
onClick={() => setCollapsed(!collapsed)}
onClick={() => {
const nextCollapsed = !collapsed
setCollapsed(nextCollapsed)
if (nextCollapsed) onCloseWorkbench?.()
}}
className="text-white/50 hover:text-white text-[11px] inline-flex items-center gap-1"
title={collapsed ? "展开" : "折叠"}
>

View File

@@ -1,9 +1,9 @@
"use client"
import { useEffect, useState, useRef, type ReactNode } from "react"
import { X, LayoutGrid, Loader2, Check, Wand2 } from "lucide-react"
import { useEffect, useState, useRef } from "react"
import { X, Loader2, Check, Wand2, GripHorizontal } from "lucide-react"
import {
type Job, type StoryboardScene, type ImageRef,
effectiveFrameUrl, updateStoryboard, resolveImageRefUrl,
updateStoryboard, resolveImageRefUrl,
} from "@/lib/api"
import { toast } from "sonner"
@@ -29,6 +29,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
const [form, setForm] = useState<StoryboardScene>(emptyScene())
const [saving, setSaving] = useState(false)
const [savedTick, setSavedTick] = useState(0)
const [panelHeight, setPanelHeight] = useState(320)
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
// Esc 关闭
@@ -91,98 +92,37 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
}, 600)
}
const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
const clampPanelHeight = (height: number) => {
const max = typeof window === "undefined" ? 680 : Math.max(300, window.innerHeight - 190)
return Math.max(180, Math.min(max, Math.round(height)))
}
const startResize = (e: React.PointerEvent) => {
e.preventDefault()
e.stopPropagation()
const startY = e.clientY
const startHeight = panelHeight
const onMove = (ev: PointerEvent) => {
setPanelHeight(clampPanelHeight(startHeight + ev.clientY - startY))
}
const onUp = () => {
window.removeEventListener("pointermove", onMove)
window.removeEventListener("pointerup", onUp)
}
window.addEventListener("pointermove", onMove)
window.addEventListener("pointerup", onUp)
}
return (
<div
className="relative z-20 h-[min(680px,calc(100vh-170px))] flex-shrink-0 border-b border-white/10 bg-black/90 backdrop-blur-xl flex flex-col shadow-2xl"
style={{ animation: "drawer-in 0.2s cubic-bezier(0.32, 0.72, 0, 1)" }}
className="relative z-20 flex-shrink-0 border-t border-white/5 border-b border-white/10 bg-black/70 backdrop-blur-xl shadow-2xl"
style={{ height: panelHeight, animation: "drawer-in 0.2s cubic-bezier(0.32, 0.72, 0, 1)" }}
>
{/* Header */}
<header className="flex items-center justify-between px-5 py-2.5 border-b border-white/10 bg-black/40 flex-shrink-0">
<div className="flex items-center gap-2 text-white">
<LayoutGrid className="h-4 w-4 text-violet-300" />
<span className="text-[14px] font-semibold"></span>
<span className="text-[11px] text-white/40 font-mono ml-2">
{frames.length}
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-[11px] text-white/45 font-mono inline-flex items-center gap-1">
{saving ? (<><Loader2 className="h-2.5 w-2.5 animate-spin" /> </>)
: savedTick > 0 ? (<><Check className="h-2.5 w-2.5 text-emerald-300" /> </>)
: "字段变更自动保存"}
</span>
<button
onClick={onClose}
className="h-8 px-3 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center gap-1.5 text-[12px]"
title="收起编排 (Esc)"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
</header>
<div className="flex flex-1 min-h-0">
{/* 左侧分镜列表 */}
<aside
className="flex-shrink-0 border-r border-white/10 overflow-y-auto bg-black/20"
style={{ width: 232 }}
>
{frames.length === 0 ? (
<div className="p-4 text-[11px] text-white/40 leading-relaxed">
·
</div>
) : (
<div className="p-2 space-y-1">
{frames.map((f, i) => {
const isFocused = focusedIdx === f.index
const sb = f.storyboard
return (
<button
key={f.index}
onClick={() => setFocusedIdx(f.index)}
className={`w-full text-left rounded-lg p-2 transition flex gap-2 items-start ${
isFocused
? "bg-violet-500/25 border border-violet-300/60"
: "hover:bg-white/[0.04] border border-transparent"
}`}
>
<div
className="flex-shrink-0 rounded-md overflow-hidden bg-black"
style={{ width: 64, aspectRatio: aspect }}
>
<img src={effectiveFrameUrl(job.id, f)} className="w-full h-full object-cover" alt="" />
</div>
<div className="flex-1 min-w-0">
<div className="text-[11.5px] font-semibold text-white flex items-center gap-1.5">
<span> {i + 1}</span>
{sb?.duration ? (
<span className="text-[9px] text-violet-300/80 font-mono">{sb.duration}s</span>
) : null}
</div>
<div className="text-[9.5px] text-white/40 font-mono">
{f.timestamp.toFixed(2)}s · {f.elements?.length ?? 0}
</div>
{sb?.subject && (
<div className="text-[10px] text-white/65 truncate mt-0.5" title={sb.subject}>
{sb.subject}
</div>
)}
</div>
</button>
)
})}
</div>
)}
</aside>
{/* 右侧详情 — 4 图槽 + 时长 */}
<main className="flex-1 overflow-y-auto">
{!focusFrame ? (
<div className="p-8 text-[13px] text-white/40"></div>
) : (
<div className="max-w-5xl mx-auto p-6 space-y-5">
<div className="h-full overflow-y-auto pb-4">
{!focusFrame ? (
<div className="px-4 py-5 text-[12px] text-white/40"></div>
) : (
<div className="max-w-6xl mx-auto px-4 py-4 space-y-4">
{/* 顶栏:分镜信息 + 剪贴板提示 + 时长 */}
<div className="flex items-center justify-between gap-4">
<div className="text-[15.5px] font-semibold text-white">
@@ -210,6 +150,18 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
/>
</label>
<span className="text-[11px] text-white/45 font-mono inline-flex items-center gap-1">
{saving ? (<><Loader2 className="h-2.5 w-2.5 animate-spin" /> </>)
: savedTick > 0 ? (<><Check className="h-2.5 w-2.5 text-emerald-300" /> </>)
: "自动保存"}
</span>
<button
onClick={onClose}
className="h-7 px-2.5 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center gap-1 text-[11.5px]"
title="收起编排 (Esc)"
>
<X className="h-3 w-3" />
</button>
</div>
</div>
@@ -330,15 +282,17 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
4 + + SKG Seedance / Kling / Veo3
</div>
</section>
</div>
)}
</main>
</div>
)}
</div>
{/* 底部快捷 */}
<footer className="border-t border-white/10 px-5 py-1.5 text-[10px] text-white/40 font-mono text-center bg-black/40">
ESC ·
</footer>
<button
type="button"
onPointerDown={startResize}
className="absolute bottom-0 left-0 right-0 z-10 h-4 cursor-ns-resize border-t border-white/10 bg-black/50 text-white/45 hover:bg-violet-500/25 hover:text-white inline-flex items-center justify-center transition"
title="拖动上推 / 下拉调整编排区高度"
>
<GripHorizontal className="h-3.5 w-3.5" />
</button>
</div>
)
}