81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
"use client"
|
||
import { LayoutGrid, ChevronDown, ChevronUp } from "lucide-react"
|
||
import { type Job, 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, workbenchOpen = false, onOpenWorkbench, onCloseWorkbench }: Props) {
|
||
if (!job) return null
|
||
|
||
const frames = job.frames
|
||
.filter((f) => selectedFrames.has(f.index))
|
||
.sort((a, b) => a.timestamp - b.timestamp)
|
||
|
||
const totalElements = frames.reduce(
|
||
(sum, f) => sum + (f.elements?.filter((e) => hasCutout(e)).length ?? 0),
|
||
0,
|
||
)
|
||
|
||
// focused 分镜数据
|
||
// focus 用来高亮分镜缩略图 + 顶部 indicator(不再触发底部详情面板展开)
|
||
const focusFrame = focusedFrame !== null
|
||
? job.frames.find((f) => f.index === focusedFrame) ?? null
|
||
: null
|
||
const focusSeq = focusFrame
|
||
? frames.findIndex((f) => f.index === focusFrame.index) + 1
|
||
: 0
|
||
|
||
|
||
return (
|
||
<div data-storyboard-bar="true" className="relative z-20 flex-shrink-0 border-b border-white/5 bg-black/30 backdrop-blur-xl">
|
||
{/* header */}
|
||
<div className="flex items-center justify-between px-4 py-2">
|
||
<div className="flex items-center gap-2 min-w-0">
|
||
<LayoutGrid className="h-3.5 w-3.5 text-violet-300 shrink-0" />
|
||
<span className="text-[12.5px] font-semibold text-white shrink-0">分镜头编排</span>
|
||
<span className="text-[10px] text-white/40 font-mono shrink-0">
|
||
{frames.length} 分镜 · {totalElements} 元素
|
||
</span>
|
||
{focusFrame ? (
|
||
<span className="text-[10px] text-violet-300/90 shrink-0 inline-flex items-center gap-1">
|
||
· 编排中:<span className="text-white font-medium">分镜 {focusSeq}</span>
|
||
</span>
|
||
) : (
|
||
<span className="text-[10px] text-white/30 truncate">
|
||
· 组织分镜画面 → 为生成视频做准备
|
||
</span>
|
||
)}
|
||
</div>
|
||
<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)
|
||
onOpenWorkbench?.(nextFrame)
|
||
}}
|
||
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 ? "先到关键帧节点选用分镜" : workbenchOpen ? "收起分镜头编排明细" : "下拉展开分镜头编排"}
|
||
>
|
||
{workbenchOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||
{workbenchOpen ? "收起编排" : "展开编排"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|