199 lines
7.6 KiB
TypeScript
199 lines
7.6 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { createPortal } from "react-dom";
|
||
import { X, ChevronLeft, ChevronRight } from "lucide-react";
|
||
import { AnimatePresence, motion } from "framer-motion";
|
||
import Image from "next/image";
|
||
import { cn } from "@/lib/cn";
|
||
|
||
interface PerformanceGalleryProps {
|
||
images: string[];
|
||
/** 当无真实图时,用此颜色生成占位 */
|
||
themeColor?: string;
|
||
/** 占位标签(如 "定妆照"、"表演中") */
|
||
placeholderLabels?: string[];
|
||
}
|
||
|
||
const DEFAULT_LABELS = ["定妆照", "表演中", "幕后花絮", "舞台 1", "舞台 2", "未公开"];
|
||
|
||
export default function PerformanceGallery({
|
||
images,
|
||
themeColor = "#8b5cf6",
|
||
placeholderLabels = DEFAULT_LABELS,
|
||
}: PerformanceGalleryProps) {
|
||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||
const [mounted, setMounted] = useState(false);
|
||
|
||
useEffect(() => setMounted(true), []);
|
||
|
||
// ESC 关闭
|
||
useEffect(() => {
|
||
if (lightboxIndex == null) return;
|
||
const handler = (e: KeyboardEvent) => {
|
||
if (e.key === "Escape") setLightboxIndex(null);
|
||
if (e.key === "ArrowLeft")
|
||
setLightboxIndex((i) => (i! > 0 ? i! - 1 : images.length - 1));
|
||
if (e.key === "ArrowRight")
|
||
setLightboxIndex((i) => (i! < images.length - 1 ? i! + 1 : 0));
|
||
};
|
||
window.addEventListener("keydown", handler);
|
||
const prev = document.body.style.overflow;
|
||
document.body.style.overflow = "hidden";
|
||
return () => {
|
||
window.removeEventListener("keydown", handler);
|
||
document.body.style.overflow = prev;
|
||
};
|
||
}, [lightboxIndex, images.length]);
|
||
|
||
return (
|
||
<>
|
||
<div className="grid grid-cols-3 sm:grid-cols-3 lg:grid-cols-6 gap-2 sm:gap-3">
|
||
{images.map((src, i) => (
|
||
<button
|
||
type="button"
|
||
key={i}
|
||
onClick={() => setLightboxIndex(i)}
|
||
className="group relative aspect-[4/3] rounded-lg overflow-hidden border border-white/[0.08] hover:border-purple-500/50 hover:shadow-[0_0_16px_rgba(139,92,246,0.25)] transition-all"
|
||
>
|
||
{src ? (
|
||
<Image
|
||
src={src}
|
||
alt={`表演图 ${i + 1}`}
|
||
fill
|
||
sizes="(max-width: 768px) 33vw, 200px"
|
||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||
/>
|
||
) : (
|
||
<div
|
||
className="absolute inset-0 flex items-center justify-center text-white/55 text-[11px]"
|
||
style={{
|
||
background: `linear-gradient(135deg, ${themeColor}25 0%, #1a1638 70%)`,
|
||
}}
|
||
>
|
||
<span className="font-label tracking-widest uppercase text-[10px]">
|
||
{placeholderLabels[i] || `Image ${i + 1}`}
|
||
</span>
|
||
<span className="absolute top-1.5 right-1.5 text-white/15 text-[10px]">
|
||
✦
|
||
</span>
|
||
</div>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<p className="text-xs text-white/40 mt-3">点击图片打开大图查看</p>
|
||
|
||
{/* Lightbox */}
|
||
{mounted &&
|
||
createPortal(
|
||
<AnimatePresence>
|
||
{lightboxIndex !== null && (
|
||
<motion.div
|
||
key="lightbox"
|
||
className="fixed inset-0 z-[110] flex items-center justify-center"
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
>
|
||
<button
|
||
type="button"
|
||
aria-label="关闭"
|
||
onClick={() => setLightboxIndex(null)}
|
||
className="absolute inset-0 bg-black/90 backdrop-blur-lg cursor-default"
|
||
/>
|
||
|
||
{/* 顶部关闭 */}
|
||
<button
|
||
type="button"
|
||
onClick={() => setLightboxIndex(null)}
|
||
aria-label="关闭"
|
||
className="absolute top-5 right-5 w-10 h-10 rounded-full bg-white/10 border border-white/15 backdrop-blur-md flex items-center justify-center text-white hover:bg-white/20 z-20"
|
||
>
|
||
<X size={18} />
|
||
</button>
|
||
|
||
{/* 左右切换 */}
|
||
{images.length > 1 && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
setLightboxIndex(
|
||
(i) => (i! > 0 ? i! - 1 : images.length - 1),
|
||
)
|
||
}
|
||
aria-label="上一张"
|
||
className="absolute left-5 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-white/10 border border-white/15 backdrop-blur-md flex items-center justify-center text-white hover:bg-white/20 z-20"
|
||
>
|
||
<ChevronLeft size={22} />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
setLightboxIndex(
|
||
(i) => (i! < images.length - 1 ? i! + 1 : 0),
|
||
)
|
||
}
|
||
aria-label="下一张"
|
||
className="absolute right-5 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-white/10 border border-white/15 backdrop-blur-md flex items-center justify-center text-white hover:bg-white/20 z-20"
|
||
>
|
||
<ChevronRight size={22} />
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{/* 图片本体 */}
|
||
<motion.div
|
||
key={lightboxIndex}
|
||
initial={{ opacity: 0, scale: 0.97 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.25 }}
|
||
className="relative max-w-5xl w-full mx-6 max-h-[85vh] aspect-[4/3] z-10"
|
||
>
|
||
{images[lightboxIndex] ? (
|
||
<Image
|
||
src={images[lightboxIndex]!}
|
||
alt={`表演图 ${lightboxIndex + 1}`}
|
||
fill
|
||
sizes="100vw"
|
||
className="object-contain"
|
||
/>
|
||
) : (
|
||
<div
|
||
className={cn(
|
||
"absolute inset-0 rounded-xl flex items-center justify-center",
|
||
"border border-white/10",
|
||
)}
|
||
style={{
|
||
background: `linear-gradient(135deg, ${themeColor}30 0%, #1a1638 70%)`,
|
||
}}
|
||
>
|
||
<div className="text-center">
|
||
<span className="font-logo text-6xl text-white/30 tracking-widest">
|
||
✦
|
||
</span>
|
||
<p className="font-label text-xs tracking-widest text-white/55 uppercase mt-3">
|
||
{placeholderLabels[lightboxIndex] ||
|
||
`Image ${lightboxIndex + 1}`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* 索引 */}
|
||
<div className="absolute bottom-5 left-1/2 -translate-x-1/2 font-display text-xs text-white/55 tracking-widest z-20 tabular-nums">
|
||
{lightboxIndex + 1} / {images.length}
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>,
|
||
document.body,
|
||
)}
|
||
</>
|
||
);
|
||
}
|