完善图片预览功能:添加左右切换和批量下载
- 修复图片点击事件被覆盖层阻挡的问题 - 添加图片左右切换功能,支持键盘方向键 - 添加批量下载所有图片功能 - 在工具栏显示当前图片位置(如 1/4) - 支持键盘快捷键:ESC关闭、切换、+/-缩放、R旋转 - 优化图片容器样式,确保图片完整显示
This commit is contained in:
parent
8b913b11a5
commit
406c95f6c1
|
|
@ -11,7 +11,10 @@ import {
|
||||||
MapPin,
|
MapPin,
|
||||||
User,
|
User,
|
||||||
Shirt,
|
Shirt,
|
||||||
AlertCircle
|
AlertCircle,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
DownloadCloud
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GroundingSource } from '../types/ragGrounding';
|
import { GroundingSource } from '../types/ragGrounding';
|
||||||
|
|
||||||
|
|
@ -27,6 +30,12 @@ interface ImagePreviewModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
/** 下载回调 */
|
/** 下载回调 */
|
||||||
onDownload?: (source: GroundingSource) => void;
|
onDownload?: (source: GroundingSource) => void;
|
||||||
|
/** 所有图片列表 */
|
||||||
|
images?: string[];
|
||||||
|
/** 当前图片索引 */
|
||||||
|
currentIndex?: number;
|
||||||
|
/** 导航回调 */
|
||||||
|
onNavigate?: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,7 +46,10 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
source,
|
source,
|
||||||
onClose,
|
onClose,
|
||||||
onDownload
|
onDownload,
|
||||||
|
images = [],
|
||||||
|
currentIndex = 0,
|
||||||
|
onNavigate
|
||||||
}) => {
|
}) => {
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
@ -45,6 +57,83 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
@ -165,6 +254,29 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<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">
|
<div className="flex items-center space-x-1 bg-white rounded-lg border border-gray-200 p-1">
|
||||||
<button
|
<button
|
||||||
|
|
@ -197,14 +309,28 @@ export const ImagePreviewModal: React.FC<ImagePreviewModalProps> = ({
|
||||||
|
|
||||||
{/* 下载按钮 */}
|
{/* 下载按钮 */}
|
||||||
{onDownload && (
|
{onDownload && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isDownloading}
|
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"
|
className="p-2 text-gray-500 hover:text-pink-500 transition-colors bg-white rounded-lg border border-gray-200 disabled:opacity-50"
|
||||||
title="下载到本地"
|
title="下载当前图片"
|
||||||
>
|
>
|
||||||
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
|
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
|
||||||
</button>
|
</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">
|
<div className="flex-1 flex items-center justify-center bg-gray-100 overflow-hidden">
|
||||||
{imageUri && !imageError ? (
|
{imageUri && !imageError ? (
|
||||||
<div className="relative">
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
src={imageUri}
|
src={imageUri}
|
||||||
alt={description || title}
|
alt={description || title}
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,13 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
||||||
const [previewModal, setPreviewModal] = useState<{
|
const [previewModal, setPreviewModal] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
source: GroundingSource | null;
|
source: GroundingSource | null;
|
||||||
|
images: string[];
|
||||||
|
currentIndex: number;
|
||||||
}>({
|
}>({
|
||||||
isOpen: false,
|
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) => {
|
const openImagePreview = useCallback((imageUrl: string, title: string, allImages: string[] = [], currentIndex: number = 0) => {
|
||||||
console.log('🖼️ 打开图片预览:', { imageUrl, title });
|
console.log('🖼️ 打开图片预览:', { imageUrl, title, allImages, currentIndex });
|
||||||
const source: GroundingSource = {
|
const source: GroundingSource = {
|
||||||
uri: imageUrl,
|
uri: imageUrl,
|
||||||
title: title,
|
title: title,
|
||||||
|
|
@ -111,16 +115,20 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
||||||
};
|
};
|
||||||
setPreviewModal({
|
setPreviewModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
source
|
source,
|
||||||
|
images: allImages,
|
||||||
|
currentIndex
|
||||||
});
|
});
|
||||||
console.log('🖼️ 预览模态框状态已更新:', { isOpen: true, source });
|
console.log('🖼️ 预览模态框状态已更新:', { isOpen: true, source, images: allImages, currentIndex });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 关闭图片预览
|
// 关闭图片预览
|
||||||
const closeImagePreview = useCallback(() => {
|
const closeImagePreview = useCallback(() => {
|
||||||
setPreviewModal({
|
setPreviewModal({
|
||||||
isOpen: false,
|
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 && (
|
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{record.outfit_images.slice(0, 4).map((image, index) => (
|
{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
|
||||||
<img
|
key={image.id}
|
||||||
src={getImageSrc(image.image_url)}
|
className="aspect-square rounded-lg overflow-hidden relative group cursor-pointer"
|
||||||
alt={`穿搭图片 ${index + 1}`}
|
|
||||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
console.log('🖱️ 图片点击事件触发:', {
|
console.log('🖱️ 图片容器点击事件触发:', {
|
||||||
imageUrl: image.image_url,
|
imageUrl: image.image_url,
|
||||||
title: `穿搭图片 ${index + 1}`,
|
title: `穿搭图片 ${index + 1}`,
|
||||||
event: e
|
event: e
|
||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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
|
||||||
|
src={getImageSrc(image.image_url)}
|
||||||
|
alt={`穿搭图片 ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
|
||||||
/>
|
/>
|
||||||
{/* 预览按钮覆盖层 */}
|
{/* 预览按钮覆盖层 */}
|
||||||
<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" />
|
<Eye className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -442,7 +454,8 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
||||||
});
|
});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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
|
<img
|
||||||
|
|
@ -485,6 +498,23 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
||||||
isOpen={previewModal.isOpen}
|
isOpen={previewModal.isOpen}
|
||||||
source={previewModal.source}
|
source={previewModal.source}
|
||||||
onClose={closeImagePreview}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue