123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { ArrowLeft, ArrowRight, X } from "lucide-react";
|
|
export type GroupShot = {
|
|
id: string;
|
|
title: string;
|
|
url: string;
|
|
type: "image" | "video";
|
|
};
|
|
export function GroupStoryboardPreview({
|
|
shots,
|
|
onOrder,
|
|
onClose,
|
|
}: {
|
|
shots: GroupShot[];
|
|
onOrder: (ids: string[]) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const [index, setIndex] = useState(0),
|
|
shot = shots[Math.min(index, shots.length - 1)];
|
|
useEffect(() => {
|
|
const key = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
window.addEventListener("keydown", key);
|
|
return () => window.removeEventListener("keydown", key);
|
|
}, [onClose]);
|
|
if (!shot) return null;
|
|
const move = (delta: number) => {
|
|
const next = index + delta;
|
|
if (next < 0 || next >= shots.length) return;
|
|
const ids = shots.map((s) => s.id);
|
|
[ids[index], ids[next]] = [ids[next], ids[index]];
|
|
onOrder(ids);
|
|
setIndex(next);
|
|
};
|
|
return (
|
|
<div
|
|
data-canvas-overlay="true"
|
|
role="dialog"
|
|
aria-label="编组分镜预览"
|
|
className="fixed inset-0 z-[120] flex flex-col bg-black/95 p-8 text-white"
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
>
|
|
<header className="mb-4 flex items-center justify-between">
|
|
<h2>
|
|
分镜 {index + 1}/{shots.length} · {shot.title}
|
|
</h2>
|
|
<button aria-label="关闭分镜预览" onClick={onClose}>
|
|
<X />
|
|
</button>
|
|
</header>
|
|
<div className="flex min-h-0 flex-1 items-center justify-center">
|
|
{shot.type === "video" ? (
|
|
<video
|
|
key={shot.id}
|
|
src={shot.url}
|
|
controls
|
|
autoPlay
|
|
className="h-full max-w-full object-contain"
|
|
onEnded={() => setIndex((i) => Math.min(i + 1, shots.length - 1))}
|
|
/>
|
|
) : (
|
|
<img
|
|
src={shot.url}
|
|
alt={shot.title}
|
|
className="h-full max-w-full object-contain"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="mt-4 flex items-center justify-center gap-6">
|
|
<button
|
|
type="button"
|
|
disabled={index === 0}
|
|
onClick={() => setIndex((i) => i - 1)}
|
|
className="disabled:opacity-30"
|
|
>
|
|
<ArrowLeft />
|
|
</button>
|
|
<span>
|
|
{index + 1} / {shots.length}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
disabled={index === shots.length - 1}
|
|
onClick={() => setIndex((i) => i + 1)}
|
|
className="disabled:opacity-30"
|
|
>
|
|
<ArrowRight />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={index === 0}
|
|
onClick={() => move(-1)}
|
|
className="disabled:opacity-30"
|
|
>
|
|
镜头前移
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={index === shots.length - 1}
|
|
onClick={() => move(1)}
|
|
className="disabled:opacity-30"
|
|
>
|
|
镜头后移
|
|
</button>
|
|
</div>
|
|
<div className="mt-5 flex justify-center gap-3 overflow-x-auto">
|
|
{shots.map((s, i) => (
|
|
<button
|
|
type="button"
|
|
key={s.id}
|
|
onClick={() => setIndex(i)}
|
|
className={`shrink-0 rounded-lg border px-4 py-2 text-sm ${i === index ? "border-white bg-white/20" : "border-white/20"}`}
|
|
>
|
|
{i + 1} · {s.title}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|