98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
"use client"
|
|
import { useEffect } from "react"
|
|
import { X, ChevronLeft, ChevronRight, Check } from "lucide-react"
|
|
import { frameUrl, type KeyFrame } from "@/lib/api"
|
|
|
|
interface Props {
|
|
jobId: string
|
|
frames: KeyFrame[]
|
|
activeIndex: number | null
|
|
selected: Set<number>
|
|
onClose: () => void
|
|
onChange: (idx: number) => void
|
|
onToggleSelect: (idx: number) => void
|
|
}
|
|
|
|
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect }: Props) {
|
|
useEffect(() => {
|
|
if (activeIndex === null) return
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose()
|
|
if (e.key === "ArrowLeft" && activeIndex > 0) onChange(activeIndex - 1)
|
|
if (e.key === "ArrowRight" && activeIndex < frames.length - 1) onChange(activeIndex + 1)
|
|
if (e.key === " " || e.key === "Enter") {
|
|
e.preventDefault()
|
|
onToggleSelect(activeIndex)
|
|
}
|
|
}
|
|
window.addEventListener("keydown", onKey)
|
|
return () => window.removeEventListener("keydown", onKey)
|
|
}, [activeIndex, frames.length, onClose, onChange, onToggleSelect])
|
|
|
|
if (activeIndex === null || !frames[activeIndex]) return null
|
|
const f = frames[activeIndex]
|
|
const isSelected = selected.has(f.index)
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[100] bg-black/85 backdrop-blur-sm flex items-center justify-center"
|
|
onClick={onClose}
|
|
>
|
|
{/* 关闭 */}
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onClose() }}
|
|
className="absolute top-5 right-5 h-10 w-10 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center"
|
|
aria-label="关闭"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
|
|
{/* 左右切换 */}
|
|
{activeIndex > 0 && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onChange(activeIndex - 1) }}
|
|
className="absolute left-5 h-12 w-12 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center"
|
|
>
|
|
<ChevronLeft className="h-6 w-6" />
|
|
</button>
|
|
)}
|
|
{activeIndex < frames.length - 1 && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onChange(activeIndex + 1) }}
|
|
className="absolute right-5 h-12 w-12 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center"
|
|
>
|
|
<ChevronRight className="h-6 w-6" />
|
|
</button>
|
|
)}
|
|
|
|
{/* 大图 */}
|
|
<div onClick={(e) => e.stopPropagation()} className="flex flex-col items-center gap-4 max-w-[92vw] max-h-[92vh]">
|
|
<img
|
|
src={frameUrl(jobId, f.index)}
|
|
alt={`frame ${f.index}`}
|
|
className="max-w-[88vw] max-h-[72vh] rounded-xl shadow-2xl object-contain"
|
|
/>
|
|
<div className="flex items-center gap-4 text-white">
|
|
<div className="font-mono text-sm tabular-nums">
|
|
{String(f.index + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
|
|
<span className="mx-3 text-white/40">·</span>
|
|
<span className="text-white/70">{f.timestamp.toFixed(2)}s</span>
|
|
</div>
|
|
<button
|
|
onClick={() => onToggleSelect(f.index)}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium inline-flex items-center gap-2 transition ${
|
|
isSelected
|
|
? "bg-emerald-500 text-white hover:bg-emerald-400"
|
|
: "bg-white/10 text-white hover:bg-white/20"
|
|
}`}
|
|
>
|
|
<Check className="h-4 w-4" />
|
|
{isSelected ? "已选用(点取消)" : "选用此帧"}
|
|
</button>
|
|
</div>
|
|
<div className="text-[11px] text-white/40 font-mono">←/→ 切换 · Space 选用 · ESC 关闭</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|