Files
wish-pool/components/image-gallery.tsx
2025-07-19 02:12:37 +08:00

215 lines
7.4 KiB
TypeScript

"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { X, ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut, RotateCw, Maximize2 } from "lucide-react"
import { formatFileSize, type ImageFile } from "@/lib/image-utils"
interface ImageGalleryProps {
images: ImageFile[]
className?: string
}
interface ImageModalProps {
images: ImageFile[]
currentIndex: number
onClose: () => void
onNavigate: (index: number) => void
}
function ImageModal({ images, currentIndex, onClose, onNavigate }: ImageModalProps) {
const [zoom, setZoom] = useState(1)
const [rotation, setRotation] = useState(0)
const currentImage = images[currentIndex]
const handlePrevious = () => {
const newIndex = currentIndex > 0 ? currentIndex - 1 : images.length - 1
onNavigate(newIndex)
setZoom(1)
setRotation(0)
}
const handleNext = () => {
const newIndex = currentIndex < images.length - 1 ? currentIndex + 1 : 0
onNavigate(newIndex)
setZoom(1)
setRotation(0)
}
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.25, 3))
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.25, 0.25))
const handleRotate = () => setRotation((prev) => (prev + 90) % 360)
const handleDownload = () => {
// 創建下載連結
const link = document.createElement("a")
link.href = currentImage.url
link.download = currentImage.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return (
<div className="fixed inset-0 z-50 bg-black/90 backdrop-blur-sm flex items-center justify-center">
{/* 關閉按鈕 */}
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="absolute top-4 right-4 z-10 text-white hover:bg-white/20"
>
<X className="w-6 h-6" />
</Button>
{/* 工具列 */}
<div className="absolute top-4 left-4 z-10 flex items-center gap-2">
<Badge className="bg-black/50 text-white border-white/20">
{currentIndex + 1} / {images.length}
</Badge>
<div className="flex items-center gap-1 bg-black/50 rounded-lg p-1">
<Button variant="ghost" size="sm" onClick={handleZoomOut} className="text-white hover:bg-white/20 p-2">
<ZoomOut className="w-4 h-4" />
</Button>
<span className="text-white text-sm px-2">{Math.round(zoom * 100)}%</span>
<Button variant="ghost" size="sm" onClick={handleZoomIn} className="text-white hover:bg-white/20 p-2">
<ZoomIn className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={handleRotate} className="text-white hover:bg-white/20 p-2">
<RotateCw className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={handleDownload} className="text-white hover:bg-white/20 p-2">
<Download className="w-4 h-4" />
</Button>
</div>
</div>
{/* 導航按鈕 */}
{images.length > 1 && (
<>
<Button
variant="ghost"
size="sm"
onClick={handlePrevious}
className="absolute left-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/20 p-3"
>
<ChevronLeft className="w-6 h-6" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleNext}
className="absolute right-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/20 p-3"
>
<ChevronRight className="w-6 h-6" />
</Button>
</>
)}
{/* 圖片容器 */}
<div className="flex items-center justify-center w-full h-full p-16">
<img
src={currentImage.url || "/placeholder.svg"}
alt={currentImage.name}
className="max-w-full max-h-full object-contain transition-transform duration-200"
style={{
transform: `scale(${zoom}) rotate(${rotation}deg)`,
}}
onError={(e) => {
// 如果圖片載入失敗,顯示錯誤訊息
const target = e.target as HTMLImageElement
target.src = "/placeholder.svg?height=400&width=400&text=圖片載入失敗"
}}
/>
</div>
{/* 圖片資訊 */}
<div className="absolute bottom-4 left-4 right-4 z-10">
<Card className="bg-black/50 backdrop-blur-sm border-white/20">
<CardContent className="p-3">
<div className="text-white text-sm">
<div className="font-medium truncate">{currentImage.name}</div>
<div className="text-white/70 text-xs mt-1">
{formatFileSize(currentImage.size)} {currentImage.type}
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
export default function ImageGallery({ images, className = "" }: ImageGalleryProps) {
const [modalOpen, setModalOpen] = useState(false)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
if (images.length === 0) return null
const openModal = (index: number) => {
setCurrentImageIndex(index)
setModalOpen(true)
}
const closeModal = () => {
setModalOpen(false)
}
return (
<>
<div className={`space-y-3 ${className}`}>
<div className="flex items-center gap-2">
<Badge className="bg-blue-500/20 text-blue-200 border border-blue-400/30">
📷 ({images.length})
</Badge>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{images.map((image, index) => (
<div key={image.id} className="relative group cursor-pointer" onClick={() => openModal(index)}>
<div className="aspect-square rounded-lg overflow-hidden bg-slate-700/50 border border-slate-600/50 hover:border-cyan-400/50 transition-all duration-200">
<img
src={image.url || "/placeholder.svg"}
alt={image.name}
className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
onError={(e) => {
// 如果圖片載入失敗,顯示預設圖片
const target = e.target as HTMLImageElement
target.src = "/placeholder.svg?height=200&width=200&text=圖片載入失敗"
}}
/>
{/* 懸停覆蓋層 */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
<div className="text-white text-center">
<Maximize2 className="w-6 h-6 mx-auto mb-1" />
<div className="text-xs"></div>
</div>
</div>
</div>
{/* 檔案名稱 */}
<div className="mt-1 text-xs text-slate-400 truncate" title={image.name}>
{image.name}
</div>
</div>
))}
</div>
</div>
{/* 圖片模態框 */}
{modalOpen && (
<ImageModal
images={images}
currentIndex={currentImageIndex}
onClose={closeModal}
onNavigate={setCurrentImageIndex}
/>
)}
</>
)
}