Files
20260512-skg-tk/web/components/storyboard-bar.tsx

105 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState } from "react"
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react"
import { type Job, effectiveFrameUrl } from "@/lib/api"
interface Props {
job: Job | null
selectedFrames: Set<number>
onExpandFrame: (idx: number) => void
}
export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
const [collapsed, setCollapsed] = useState(false)
if (!job) return null
// 按时间序排已选用的分镜
const frames = job.frames
.filter((f) => selectedFrames.has(f.index))
.sort((a, b) => a.timestamp - b.timestamp)
const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
const totalElements = frames.reduce(
(sum, f) => sum + (f.elements?.filter((e) => e.cutout_id).length ?? 0),
0,
)
return (
<div 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>
<span className="text-[10px] text-white/30 truncate">
·
</span>
</div>
<button
onClick={() => setCollapsed(!collapsed)}
className="text-white/50 hover:text-white text-[11px] inline-flex items-center gap-1 shrink-0"
title={collapsed ? "展开" : "折叠"}
>
{collapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
{collapsed ? "展开" : "折叠"}
</button>
</div>
{/* thumbnails row */}
{!collapsed && (
frames.length === 0 ? (
<div className="px-4 pb-3 text-[11px] text-white/40">
·
</div>
) : (
<div className="px-4 pb-3 flex gap-2 overflow-x-auto">
{frames.map((f, i) => {
const elementCount = f.elements?.filter((e) => e.cutout_id).length ?? 0
const totalElCount = f.elements?.length ?? 0
const cleaned = f.cleaned_applied
return (
<button
key={f.index}
onClick={() => onExpandFrame(f.index)}
title={`分镜 ${i + 1} · ${f.timestamp.toFixed(2)}s${cleaned ? " · 已清洗" : ""} · ${elementCount}/${totalElCount} 元素 · 点击编辑`}
className="group relative shrink-0 rounded-md border border-white/15 hover:border-violet-300/60 overflow-hidden transition shadow-lg hover:-translate-y-0.5"
style={{ width: 88, aspectRatio: aspect }}
>
<img
src={effectiveFrameUrl(job.id, f)}
alt={`frame ${f.index}`}
className="absolute inset-0 w-full h-full object-cover"
/>
{/* 左上:序号 */}
<div className="absolute top-1 left-1 text-[9.5px] font-bold text-white bg-violet-500/85 backdrop-blur px-1.5 py-0.5 rounded">
#{i + 1}
</div>
{/* 右上:清洗标记 */}
{cleaned && (
<div className="absolute top-1 right-1 text-[9px] text-white bg-cyan-500/85 backdrop-blur px-1 py-0.5 rounded font-bold" title="已清洗">
</div>
)}
{/* 底部:时间 + 元素数 */}
<div className="absolute bottom-0 right-0 left-0 px-1.5 py-0.5 text-[9px] font-mono text-white bg-gradient-to-t from-black/85 to-transparent flex items-center justify-between">
<span>{f.timestamp.toFixed(1)}s</span>
{totalElCount > 0 && (
<span className="inline-flex items-center gap-0.5">
<Sparkle className="h-2 w-2" />
{elementCount}/{totalElCount}
</span>
)}
</div>
</button>
)
})}
</div>
)
)}
</div>
)
}