UI-UX/src/components/artist/PerformanceGallery.tsx

199 lines
7.6 KiB
TypeScript
Raw 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 { 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,
)}
</>
);
}