diff --git a/src/components/ImagePreviewModal/index.js b/src/components/ImagePreviewModal/index.js
new file mode 100644
index 00000000..04b6608b
--- /dev/null
+++ b/src/components/ImagePreviewModal/index.js
@@ -0,0 +1,270 @@
+/**
+ * 图片预览弹窗组件
+ * 支持多张图片左右切换、缩放、下载
+ */
+
+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';
+
+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 (
+
+
+
+
+
+
+ {/* 图片显示区域 */}
+
+
+ 1 ? 'grab' : 'default'}
+ userSelect="none"
+ />
+
+
+
+ {/* 左右切换按钮(仅多张图片时显示) */}
+ {images.length > 1 && (
+ <>
+ }
+ 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"
+ />
+
+ }
+ 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"
+ />
+ >
+ )}
+
+ {/* 底部工具栏 */}
+
+
+ {/* 缩放控制 */}
+
+ }
+ size="sm"
+ variant="ghost"
+ color="white"
+ onClick={handleZoomOut}
+ isDisabled={scale <= 0.5}
+ _hover={{ bg: 'whiteAlpha.200' }}
+ aria-label="缩小"
+ />
+
+ {Math.round(scale * 100)}%
+
+ }
+ size="sm"
+ variant="ghost"
+ color="white"
+ onClick={handleZoomIn}
+ isDisabled={scale >= 3}
+ _hover={{ bg: 'whiteAlpha.200' }}
+ aria-label="放大"
+ />
+
+
+ {/* 下载按钮 */}
+ }
+ size="sm"
+ variant="ghost"
+ color="white"
+ onClick={handleDownload}
+ _hover={{ bg: 'whiteAlpha.200' }}
+ aria-label="下载图片"
+ />
+
+ {/* 图片计数(仅多张图片时显示) */}
+ {images.length > 1 && (
+
+ {currentIndex + 1} / {images.length}
+
+ )}
+
+
+
+ {/* 快捷键提示 */}
+
+
+ 快捷键: ← → 切换 | + - 缩放 | ESC 关闭
+
+
+
+
+
+ );
+};
+
+export default ImagePreviewModal;
diff --git a/src/views/ValueForum/PostDetail.js b/src/views/ValueForum/PostDetail.js
index 1e9bd5e7..23350bfb 100644
--- a/src/views/ValueForum/PostDetail.js
+++ b/src/views/ValueForum/PostDetail.js
@@ -40,6 +40,7 @@ import {
} from '@services/elasticsearchService';
import EventTimeline from './components/EventTimeline';
import CommentSection from './components/CommentSection';
+import ImagePreviewModal from '@components/ImagePreviewModal';
const MotionBox = motion(Box);
@@ -53,6 +54,10 @@ const PostDetail = () => {
const [isLiked, setIsLiked] = useState(false);
const [likes, setLikes] = useState(0);
+ // 图片预览相关状态
+ const [isImagePreviewOpen, setIsImagePreviewOpen] = useState(false);
+ const [previewImageIndex, setPreviewImageIndex] = useState(0);
+
// 加载帖子数据
useEffect(() => {
const loadPostData = async () => {
@@ -91,6 +96,12 @@ const PostDetail = () => {
}
};
+ // 打开图片预览
+ const handleImageClick = (index) => {
+ setPreviewImageIndex(index);
+ setIsImagePreviewOpen(true);
+ };
+
// 格式化时间
const formatTime = (dateString) => {
const date = new Date(dateString);
@@ -272,6 +283,7 @@ const PostDetail = () => {
border="1px solid"
borderColor={forumColors.border.default}
cursor="pointer"
+ onClick={() => handleImageClick(index)}
_hover={{
transform: 'scale(1.05)',
boxShadow: forumColors.shadows.gold,
@@ -363,6 +375,14 @@ const PostDetail = () => {
+
+ {/* 图片预览弹窗 */}
+ setIsImagePreviewOpen(false)}
+ images={post?.images || []}
+ initialIndex={previewImageIndex}
+ />
);
};