完善图片预览功能:添加左右切换和批量下载

- 修复图片点击事件被覆盖层阻挡的问题
- 添加图片左右切换功能,支持键盘方向键
- 添加批量下载所有图片功能
- 在工具栏显示当前图片位置(如 1/4)
- 支持键盘快捷键:ESC关闭、切换、+/-缩放、R旋转
- 优化图片容器样式,确保图片完整显示
This commit is contained in:
imeepos 2025-07-30 22:52:59 +08:00
parent 8b913b11a5
commit 406c95f6c1
2 changed files with 186 additions and 30 deletions

View File

@ -11,7 +11,10 @@ import {
MapPin,
User,
Shirt,
AlertCircle
AlertCircle,
ChevronLeft,
ChevronRight,
DownloadCloud
} from 'lucide-react';
import { GroundingSource } from '../types/ragGrounding';
@ -27,6 +30,12 @@ interface ImagePreviewModalProps {
onClose: () => void;
/** 下载回调 */
onDownload?: (source: GroundingSource) => void;
/** 所有图片列表 */
images?: string[];
/** 当前图片索引 */
currentIndex?: number;
/** 导航回调 */
onNavigate?: (index: number) => void;
}
/**
@ -37,7 +46,10 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
isOpen,
source,
onClose,
onDownload
onDownload,
images = [],
currentIndex = 0,
onNavigate
}) => {
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
@ -45,6 +57,83 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
const [rotation, setRotation] = useState(0);
const [isDownloading, setIsDownloading] = useState(false);
// 切换到上一张图片
const handlePrevious = useCallback(() => {
if (images.length > 1 && onNavigate) {
const newIndex = currentIndex > 0 ? currentIndex - 1 : images.length - 1;
onNavigate(newIndex);
}
}, [images, currentIndex, onNavigate]);
// 切换到下一张图片
const handleNext = useCallback(() => {
if (images.length > 1 && onNavigate) {
const newIndex = currentIndex < images.length - 1 ? currentIndex + 1 : 0;
onNavigate(newIndex);
}
}, [images, currentIndex, onNavigate]);
// 批量下载所有图片
const handleBatchDownload = useCallback(async () => {
if (!images.length || !onDownload) return;
setIsDownloading(true);
try {
for (let i = 0; i < images.length; i++) {
const imageSource: GroundingSource = {
uri: images[i],
title: `穿搭图片 ${i + 1}`,
content: { description: '穿搭生成图片' }
};
await onDownload(imageSource);
// 添加小延迟避免过快下载
await new Promise(resolve => setTimeout(resolve, 500));
}
} catch (error) {
console.error('批量下载失败:', error);
} finally {
setIsDownloading(false);
}
}, [images, onDownload]);
// 键盘事件处理
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isOpen) return;
switch (event.key) {
case 'Escape':
onClose();
break;
case 'ArrowLeft':
event.preventDefault();
handlePrevious();
break;
case 'ArrowRight':
event.preventDefault();
handleNext();
break;
case '+':
case '=':
event.preventDefault();
setZoom(prev => Math.min(prev + 0.25, 3));
break;
case '-':
event.preventDefault();
setZoom(prev => Math.max(prev - 0.25, 0.25));
break;
case 'r':
case 'R':
event.preventDefault();
setRotation(prev => prev + 90);
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, handlePrevious, handleNext]);
// 重置状态当模态框打开时
useEffect(() => {
if (isOpen) {
@ -165,6 +254,29 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
</div>
<div className="flex items-center space-x-2">
{/* 图片导航 */}
{images.length > 1 && (
<div className="flex items-center space-x-1 bg-white rounded-lg border border-gray-200 p-1">
<button
onClick={handlePrevious}
className="p-1 text-gray-500 hover:text-gray-700 transition-colors"
title="上一张 (←)"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs text-gray-600 px-2 min-w-[3rem] text-center">
{currentIndex + 1}/{images.length}
</span>
<button
onClick={handleNext}
className="p-1 text-gray-500 hover:text-gray-700 transition-colors"
title="下一张 (→)"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
)}
{/* 缩放控制 */}
<div className="flex items-center space-x-1 bg-white rounded-lg border border-gray-200 p-1">
<button
@ -197,14 +309,28 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
{/* 下载按钮 */}
{onDownload && (
<button
onClick={handleDownload}
disabled={isDownloading}
className="p-2 text-gray-500 hover:text-pink-500 transition-colors bg-white rounded-lg border border-gray-200 disabled:opacity-50"
title="下载到本地"
>
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
</button>
<>
<button
onClick={handleDownload}
disabled={isDownloading}
className="p-2 text-gray-500 hover:text-pink-500 transition-colors bg-white rounded-lg border border-gray-200 disabled:opacity-50"
title="下载当前图片"
>
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
</button>
{/* 批量下载按钮 */}
{images.length > 1 && (
<button
onClick={handleBatchDownload}
disabled={isDownloading}
className="p-2 text-gray-500 hover:text-green-500 transition-colors bg-white rounded-lg border border-gray-200 disabled:opacity-50"
title={`批量下载所有 ${images.length} 张图片`}
>
<DownloadCloud className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
</button>
)}
</>
)}
{/* 外部链接 */}
@ -236,7 +362,7 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
{/* 图片展示区域 */}
<div className="flex-1 flex items-center justify-center bg-gray-100 overflow-hidden">
{imageUri && !imageError ? (
<div className="relative">
<div className="relative w-full h-full flex items-center justify-center">
<img
src={imageUri}
alt={description || title}

View File

@ -48,9 +48,13 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
const [previewModal, setPreviewModal] = useState<{
isOpen: boolean;
source: GroundingSource | null;
images: string[];
currentIndex: number;
}>({
isOpen: false,
source: null
source: null,
images: [],
currentIndex: 0
});
// 删除确认对话框状态
@ -102,8 +106,8 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
}, []);
// 打开图片预览
const openImagePreview = useCallback((imageUrl: string, title: string) => {
console.log('🖼️ 打开图片预览:', { imageUrl, title });
const openImagePreview = useCallback((imageUrl: string, title: string, allImages: string[] = [], currentIndex: number = 0) => {
console.log('🖼️ 打开图片预览:', { imageUrl, title, allImages, currentIndex });
const source: GroundingSource = {
uri: imageUrl,
title: title,
@ -111,16 +115,20 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
};
setPreviewModal({
isOpen: true,
source
source,
images: allImages,
currentIndex
});
console.log('🖼️ 预览模态框状态已更新:', { isOpen: true, source });
console.log('🖼️ 预览模态框状态已更新:', { isOpen: true, source, images: allImages, currentIndex });
}, []);
// 关闭图片预览
const closeImagePreview = useCallback(() => {
setPreviewModal({
isOpen: false,
source: null
source: null,
images: [],
currentIndex: 0
});
}, []);
@ -334,24 +342,28 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
<div className="grid grid-cols-2 gap-2">
{record.outfit_images.slice(0, 4).map((image, index) => (
<div key={image.id} className="aspect-square rounded-lg overflow-hidden relative group cursor-pointer">
<div
key={image.id}
className="aspect-square rounded-lg overflow-hidden relative group cursor-pointer"
onClick={(e) => {
console.log('🖱️ 图片容器点击事件触发:', {
imageUrl: image.image_url,
title: `穿搭图片 ${index + 1}`,
event: e
});
e.preventDefault();
e.stopPropagation();
const allImages = record.outfit_images.map(img => img.image_url);
openImagePreview(image.image_url, `穿搭图片 ${index + 1}`, allImages, index);
}}
>
<img
src={getImageSrc(image.image_url)}
alt={`穿搭图片 ${index + 1}`}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
onClick={(e) => {
console.log('🖱️ 图片点击事件触发:', {
imageUrl: image.image_url,
title: `穿搭图片 ${index + 1}`,
event: e
});
e.preventDefault();
e.stopPropagation();
openImagePreview(image.image_url, `穿搭图片 ${index + 1}`);
}}
/>
{/* 预览按钮覆盖层 */}
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center">
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center pointer-events-none">
<Eye className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
</div>
</div>
@ -442,7 +454,8 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
});
e.preventDefault();
e.stopPropagation();
openImagePreview(image.image_url, `穿搭图片 ${index + 1}`);
const allImages = record.outfit_images.map(img => img.image_url);
openImagePreview(image.image_url, `穿搭图片 ${index + 1}`, allImages, index);
}}
>
<img
@ -485,6 +498,23 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
isOpen={previewModal.isOpen}
source={previewModal.source}
onClose={closeImagePreview}
images={previewModal.images}
currentIndex={previewModal.currentIndex}
onNavigate={(newIndex) => {
if (previewModal.images[newIndex]) {
const newImageUrl = previewModal.images[newIndex];
const newSource: GroundingSource = {
uri: newImageUrl,
title: `穿搭图片 ${newIndex + 1}`,
content: { description: '穿搭生成图片' }
};
setPreviewModal({
...previewModal,
source: newSource,
currentIndex: newIndex
});
}
}}
/>
</div>
);