迁移以下 10 个组件使用 glassConfig.ts 统一配置: - GlassCard: GLASS_BLUR/GLASS_SHADOW 替换硬编码 blur/glow - SubTabContainer: GLASS_BLUR.lg 替换 blur(20px) - HomeNavbar: GLASS_BLUR.sm 替换 blur(10px) - AuthModalManager: GLASS_BLUR.sm 替换 blur(10px) - WechatRegister: GLASS_BLUR.xs 替换 blur(4px) - SubscriptionModal: GLASS_BLUR.xs 替换 blur(4px) - SubscriptionContentNew: GLASS_BLUR.sm/lg/xl 替换多处硬编码 - ImageLightbox: GLASS_BLUR.sm 替换 blur(10px) - ImagePreviewModal: GLASS_BLUR.sm 替换 blur(10px) - FuiContainer: GLASS_BLUR.md 替换 blur(16px) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
272 lines
7.7 KiB
JavaScript
272 lines
7.7 KiB
JavaScript
/**
|
|
* 图片预览弹窗组件
|
|
* 支持多张图片左右切换、缩放、下载
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import {
|
|
Modal,
|
|
ModalOverlay,
|
|
ModalContent,
|
|
ModalBody,
|
|
ModalCloseButton,
|
|
Image,
|
|
IconButton,
|
|
HStack,
|
|
Text,
|
|
Box,
|
|
} from '@chakra-ui/react';
|
|
import { ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { GLASS_BLUR } from '@/constants/glassConfig';
|
|
|
|
const MotionBox = motion(Box);
|
|
|
|
const ImagePreviewModal = ({ isOpen, onClose, images = [], initialIndex = 0 }) => {
|
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
const [scale, setScale] = useState(1);
|
|
|
|
// 切换到上一张
|
|
const handlePrevious = () => {
|
|
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
|
|
setScale(1); // 重置缩放
|
|
};
|
|
|
|
// 切换到下一张
|
|
const handleNext = () => {
|
|
setCurrentIndex((prev) => (prev + 1) % images.length);
|
|
setScale(1); // 重置缩放
|
|
};
|
|
|
|
// 放大
|
|
const handleZoomIn = () => {
|
|
setScale((prev) => Math.min(prev + 0.25, 3));
|
|
};
|
|
|
|
// 缩小
|
|
const handleZoomOut = () => {
|
|
setScale((prev) => Math.max(prev - 0.25, 0.5));
|
|
};
|
|
|
|
// 下载图片
|
|
const handleDownload = () => {
|
|
const link = document.createElement('a');
|
|
link.href = images[currentIndex];
|
|
link.download = `image-${currentIndex + 1}.jpg`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
};
|
|
|
|
// 键盘快捷键
|
|
React.useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if (!isOpen) return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowLeft':
|
|
handlePrevious();
|
|
break;
|
|
case 'ArrowRight':
|
|
handleNext();
|
|
break;
|
|
case 'Escape':
|
|
onClose();
|
|
break;
|
|
case '+':
|
|
case '=':
|
|
handleZoomIn();
|
|
break;
|
|
case '-':
|
|
handleZoomOut();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, currentIndex]);
|
|
|
|
// 关闭时重置状态
|
|
const handleClose = () => {
|
|
setScale(1);
|
|
setCurrentIndex(initialIndex);
|
|
onClose();
|
|
};
|
|
|
|
if (!images || images.length === 0) return null;
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} onClose={handleClose} size="full" isCentered>
|
|
<ModalOverlay bg="blackAlpha.900" backdropFilter="blur(10px)" />
|
|
<ModalContent bg="transparent" boxShadow="none" m="0">
|
|
<ModalCloseButton
|
|
size="lg"
|
|
color="white"
|
|
bg="blackAlpha.600"
|
|
_hover={{ bg: 'blackAlpha.800' }}
|
|
zIndex="2"
|
|
top="20px"
|
|
right="20px"
|
|
/>
|
|
|
|
<ModalBody
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
position="relative"
|
|
p="0"
|
|
>
|
|
{/* 图片显示区域 */}
|
|
<AnimatePresence mode="wait">
|
|
<MotionBox
|
|
key={currentIndex}
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.9 }}
|
|
transition={{ duration: 0.3 }}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
maxH="90vh"
|
|
maxW="90vw"
|
|
>
|
|
<Image
|
|
src={images[currentIndex]}
|
|
alt={`图片 ${currentIndex + 1}`}
|
|
maxH="90vh"
|
|
maxW="90vw"
|
|
objectFit="contain"
|
|
transform={`scale(${scale})`}
|
|
transition="transform 0.3s"
|
|
cursor={scale > 1 ? 'grab' : 'default'}
|
|
userSelect="none"
|
|
/>
|
|
</MotionBox>
|
|
</AnimatePresence>
|
|
|
|
{/* 左右切换按钮(仅多张图片时显示) */}
|
|
{images.length > 1 && (
|
|
<>
|
|
<IconButton
|
|
icon={<ChevronLeft size={32} />}
|
|
position="absolute"
|
|
left="20px"
|
|
top="50%"
|
|
transform="translateY(-50%)"
|
|
onClick={handlePrevious}
|
|
size="lg"
|
|
borderRadius="full"
|
|
bg="blackAlpha.600"
|
|
color="white"
|
|
_hover={{ bg: 'blackAlpha.800', transform: 'translateY(-50%) scale(1.1)' }}
|
|
_active={{ transform: 'translateY(-50%) scale(0.95)' }}
|
|
aria-label="上一张"
|
|
zIndex="2"
|
|
/>
|
|
|
|
<IconButton
|
|
icon={<ChevronRight size={32} />}
|
|
position="absolute"
|
|
right="20px"
|
|
top="50%"
|
|
transform="translateY(-50%)"
|
|
onClick={handleNext}
|
|
size="lg"
|
|
borderRadius="full"
|
|
bg="blackAlpha.600"
|
|
color="white"
|
|
_hover={{ bg: 'blackAlpha.800', transform: 'translateY(-50%) scale(1.1)' }}
|
|
_active={{ transform: 'translateY(-50%) scale(0.95)' }}
|
|
aria-label="下一张"
|
|
zIndex="2"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{/* 底部工具栏 */}
|
|
<Box
|
|
position="absolute"
|
|
bottom="30px"
|
|
left="50%"
|
|
transform="translateX(-50%)"
|
|
bg="blackAlpha.700"
|
|
borderRadius="full"
|
|
px="6"
|
|
py="3"
|
|
backdropFilter={GLASS_BLUR.sm}
|
|
zIndex="2"
|
|
>
|
|
<HStack spacing="4">
|
|
{/* 缩放控制 */}
|
|
<HStack spacing="2">
|
|
<IconButton
|
|
icon={<ZoomOut size={18} />}
|
|
size="sm"
|
|
variant="ghost"
|
|
color="white"
|
|
onClick={handleZoomOut}
|
|
isDisabled={scale <= 0.5}
|
|
_hover={{ bg: 'whiteAlpha.200' }}
|
|
aria-label="缩小"
|
|
/>
|
|
<Text color="white" fontSize="sm" fontWeight="500" minW="60px" textAlign="center">
|
|
{Math.round(scale * 100)}%
|
|
</Text>
|
|
<IconButton
|
|
icon={<ZoomIn size={18} />}
|
|
size="sm"
|
|
variant="ghost"
|
|
color="white"
|
|
onClick={handleZoomIn}
|
|
isDisabled={scale >= 3}
|
|
_hover={{ bg: 'whiteAlpha.200' }}
|
|
aria-label="放大"
|
|
/>
|
|
</HStack>
|
|
|
|
{/* 下载按钮 */}
|
|
<IconButton
|
|
icon={<Download size={18} />}
|
|
size="sm"
|
|
variant="ghost"
|
|
color="white"
|
|
onClick={handleDownload}
|
|
_hover={{ bg: 'whiteAlpha.200' }}
|
|
aria-label="下载图片"
|
|
/>
|
|
|
|
{/* 图片计数(仅多张图片时显示) */}
|
|
{images.length > 1 && (
|
|
<Text color="white" fontSize="sm" fontWeight="500">
|
|
{currentIndex + 1} / {images.length}
|
|
</Text>
|
|
)}
|
|
</HStack>
|
|
</Box>
|
|
|
|
{/* 快捷键提示 */}
|
|
<Box
|
|
position="absolute"
|
|
top="80px"
|
|
left="20px"
|
|
bg="blackAlpha.600"
|
|
borderRadius="md"
|
|
px="4"
|
|
py="2"
|
|
backdropFilter={GLASS_BLUR.sm}
|
|
>
|
|
<Text color="whiteAlpha.800" fontSize="xs">
|
|
快捷键: ← → 切换 | + - 缩放 | ESC 关闭
|
|
</Text>
|
|
</Box>
|
|
</ModalBody>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default ImagePreviewModal;
|