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