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

174 lines
7.7 KiB
TypeScript
Raw Permalink 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 { 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,
)
}