'use client'; import { X } from 'lucide-react'; import Image from 'next/image'; import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; interface ImageViewerProps { isOpen: boolean; onClose: () => void; imageUrl: string; alt?: string; } const ImageViewer: React.FC = ({ isOpen, onClose, imageUrl, alt = '图片', }) => { const [isVisible, setIsVisible] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [mounted, setMounted] = useState(false); // 确保组件在客户端挂载后才渲染 Portal useEffect(() => { setMounted(true); }, []); // 控制动画状态 useEffect(() => { let animationId: number; let timer: NodeJS.Timeout; if (isOpen) { setIsVisible(true); animationId = requestAnimationFrame(() => { animationId = requestAnimationFrame(() => { setIsAnimating(true); }); }); } else { setIsAnimating(false); timer = setTimeout(() => { setIsVisible(false); }, 200); } return () => { if (animationId) { cancelAnimationFrame(animationId); } if (timer) { clearTimeout(timer); } }; }, [isOpen]); // 阻止背景滚动 useEffect(() => { if (isVisible) { const scrollY = window.scrollY; const scrollX = window.scrollX; const body = document.body; const html = document.documentElement; const scrollBarWidth = window.innerWidth - html.clientWidth; const originalBodyStyle = { position: body.style.position, top: body.style.top, left: body.style.left, right: body.style.right, width: body.style.width, paddingRight: body.style.paddingRight, overflow: body.style.overflow, }; body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.left = `-${scrollX}px`; body.style.right = '0'; body.style.width = '100%'; body.style.overflow = 'hidden'; body.style.paddingRight = `${scrollBarWidth}px`; return () => { body.style.position = originalBodyStyle.position; body.style.top = originalBodyStyle.top; body.style.left = originalBodyStyle.left; body.style.right = originalBodyStyle.right; body.style.width = originalBodyStyle.width; body.style.paddingRight = originalBodyStyle.paddingRight; body.style.overflow = originalBodyStyle.overflow; requestAnimationFrame(() => { window.scrollTo(scrollX, scrollY); }); }; } }, [isVisible]); // ESC键关闭 useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }; if (isVisible) { document.addEventListener('keydown', handleEsc); return () => document.removeEventListener('keydown', handleEsc); } }, [isVisible, onClose]); if (!isVisible || !mounted) return null; const content = (
{/* 背景遮罩 */}
{/* 关闭按钮 */} {/* 图片容器 */}
e.stopPropagation()} >
{alt}
); return createPortal(content, document.body); }; export default ImageViewer;