完善图片预览功能:添加左右切换和批量下载
- 修复图片点击事件被覆盖层阻挡的问题 - 添加图片左右切换功能,支持键盘方向键 - 添加批量下载所有图片功能 - 在工具栏显示当前图片位置(如 1/4) - 支持键盘快捷键:ESC关闭、切换、+/-缩放、R旋转 - 优化图片容器样式,确保图片完整显示
This commit is contained in:
parent
8b913b11a5
commit
406c95f6c1
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue