From 4a8b447e0dc62c816348b67053947e4f99100ed4 Mon Sep 17 00:00:00 2001 From: shiyu Date: Sun, 25 May 2025 17:24:36 +0800 Subject: [PATCH] feat(AlbumService, ImageGrid, ImageViewer): enhance album deletion, optimize image loading, and improve favorite handling --- Services/Media/AlbumService.cs | 17 ++++- Web/src/components/image/ImageGrid.tsx | 78 +++++++++++++--------- Web/src/components/image/ImageViewer.css | 15 ++++- Web/src/components/image/ImageViewer.tsx | 85 ++++++++++++------------ Web/src/pages/albumDetail/Index.tsx | 13 ++-- 5 files changed, 124 insertions(+), 84 deletions(-) diff --git a/Services/Media/AlbumService.cs b/Services/Media/AlbumService.cs index fc81e6d..a165e78 100644 --- a/Services/Media/AlbumService.cs +++ b/Services/Media/AlbumService.cs @@ -194,9 +194,22 @@ public class AlbumService( if (album == null) return false; - // 注意:相册删除前,需要确保关联的图片被正确处理 - // 这里只移除相册,而不删除图片 + // 先找出所有属于这个相册的图片 + var pictures = await dbContext.Pictures + .Where(p => p.AlbumId == id) + .ToListAsync(); + + // 将这些图片的AlbumId设置为null + foreach (var picture in pictures) + { + picture.AlbumId = null; + picture.Album = null; + } + // 保存图片更改 + await dbContext.SaveChangesAsync(); + + // 然后删除相册 dbContext.Albums.Remove(album); await dbContext.SaveChangesAsync(); diff --git a/Web/src/components/image/ImageGrid.tsx b/Web/src/components/image/ImageGrid.tsx index 7ef8832..99ab512 100644 --- a/Web/src/components/image/ImageGrid.tsx +++ b/Web/src/components/image/ImageGrid.tsx @@ -178,42 +178,36 @@ const ImageGrid: React.FC = ({ const requestState = useRef({ inProgress: false, lastParams: '', - noResultsFor: '' // 新增:记录哪些查询参数没有返回结果 + noResultsFor: '' }); - // 优化构建查询参数函数 - const buildQueryParams = useCallback((): PaginationParams => { - const params: PaginationParams = { - page: currentPage, - pageSize, - ...queryParams, - searchQuery: queryParams.searchQuery, - tags: Array.isArray(queryParams.tags) ? queryParams.tags.join(',') : undefined, - useVectorSearch: queryParams.useVectorSearch, - similarityThreshold: queryParams.similarityThreshold - }; - return params; - }, [currentPage, pageSize, queryParams]); + const favoriteOperationsInProgress = useRef>(new Map()); + + const buildQueryParams = useCallback((): PaginationParams => ({ + page: currentPage, + pageSize, + ...queryParams, + searchQuery: queryParams.searchQuery, + tags: Array.isArray(queryParams.tags) ? queryParams.tags.join(',') : undefined, + useVectorSearch: queryParams.useVectorSearch, + similarityThreshold: queryParams.similarityThreshold + }), [currentPage, pageSize, queryParams]); - // 优化加载数据函数,减少依赖项 const loadImages = useCallback(async () => { if (isUsingExternalData || requestState.current.inProgress) return; const params = buildQueryParams(); const paramsString = JSON.stringify(params); - // 检查是否是已知没有结果的查询 + if (requestState.current.noResultsFor === paramsString && images.length === 0) { + return; + } + if (requestState.current.noResultsFor === paramsString) { - // 如果这个查询之前没有结果,且当前还是空结果状态,不再重复请求 - if (images.length === 0) { - return; - } - // 如果之前没结果但现在有图片显示,重置noResultsFor让新查询可以执行 requestState.current.noResultsFor = ''; } if (requestState.current.lastParams === paramsString) { - // 如果参数没变且已有数据或无数据状态已确认,跳过请求 return; } @@ -228,16 +222,13 @@ const ImageGrid: React.FC = ({ try { const result = await getPictures(params); - // 无论成功与否,都更新lastParams以避免相同参数的重复请求 requestState.current.lastParams = paramsString; if (result.success) { - // 更新图片数据 setImages(result.data || []); setTotalImages(result.totalCount || 0); onImagesLoaded?.(result.data || [], result.totalCount || 0); - // 如果结果为空,记录到noResultsFor if (!result.data || result.data.length === 0) { requestState.current.noResultsFor = paramsString; } @@ -264,10 +255,20 @@ const ImageGrid: React.FC = ({ if (isUsingExternalData && dataSource) setImages(dataSource); }, [dataSource, isUsingExternalData]); - // 优化收藏/取消收藏逻辑 + // 优化收藏/取消收藏逻辑,添加防抖功能 const handleToggleFavorite = async (image: PictureResponse) => { + const { id, isFavorited } = image; + + // 检查此图片是否有收藏操作正在进行中 + if (favoriteOperationsInProgress.current.get(id)) { + // 如果有操作正在进行中,直接返回,不执行新的操作 + return; + } + try { - const { id, isFavorited } = image; + // 设置操作锁定状态 + favoriteOperationsInProgress.current.set(id, true); + const api = isFavorited ? unfavoritePicture : favoritePicture; const result = await api(id); @@ -293,6 +294,11 @@ const ImageGrid: React.FC = ({ } } catch (error) { message.error('操作失败,请重试'); + } finally { + // 操作完成后,延迟300ms释放锁定状态,进一步防止快速重复点击 + setTimeout(() => { + favoriteOperationsInProgress.current.delete(id); + }, 300); } }; @@ -382,12 +388,20 @@ const ImageGrid: React.FC = ({ // 处理图片更新成功 const handleImageUpdateSuccess = (updatedImage: PictureResponse) => { - // 更新本地图片列表中对应的图片 setImages(prevImages => - prevImages.map(img => - img.id === updatedImage.id ? { ...img, ...updatedImage } : img - ) + prevImages.map(img => { + if (img.id === updatedImage.id) { + return { + ...img, + ...updatedImage, + userId: img.userId, + permission: updatedImage.permission ?? img.permission + }; + } + return img; + }) ); + closeContextMenu(); }; // 修改handleMenuAction中的编辑处理 @@ -716,7 +730,7 @@ const ImageGrid: React.FC = ({ onClose={() => setViewerState({ ...viewerState, visible: false })} images={images} initialIndex={viewerState.index} - onFavorite={onToggleFavorite || handleToggleFavorite} + onFavorite={handleToggleFavorite} // 总是传递本地的handleToggleFavorite而不是条件判断 showFavoriteCount={showFavoriteCount} onShare={handleShareImage} /> diff --git a/Web/src/components/image/ImageViewer.css b/Web/src/components/image/ImageViewer.css index 833fabc..4883805 100644 --- a/Web/src/components/image/ImageViewer.css +++ b/Web/src/components/image/ImageViewer.css @@ -200,12 +200,15 @@ border: none; background: transparent !important; font-size: 18px; - border-radius: 50%; + border-radius: 8px; height: 40px; - width: 40px; + min-width: 40px; + padding: 0 12px; display: flex; align-items: center; justify-content: center; + position: relative; + transition: background-color 0.2s ease; } .footer-btn:hover { @@ -213,8 +216,14 @@ } .footer-btn span { - margin-left: 4px; font-size: 14px; + line-height: 1; +} + +.footer-btn .anticon + span { + display: inline-block; + min-width: 16px; + text-align: center; } .image-info-drawer { diff --git a/Web/src/components/image/ImageViewer.tsx b/Web/src/components/image/ImageViewer.tsx index 94b5723..5b77a6b 100644 --- a/Web/src/components/image/ImageViewer.tsx +++ b/Web/src/components/image/ImageViewer.tsx @@ -63,6 +63,8 @@ const ImageViewer: React.FC = ({ const [shareDialogVisible, setShareDialogVisible] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); const [currentLoading, setCurrentLoading] = useState(false); + const [fadeTransition, setFadeTransition] = useState(false); + const [, setActiveImage] = useState(null); const [zoomPanState, setZoomPanState] = useState({ scale: 1, @@ -79,6 +81,9 @@ const ImageViewer: React.FC = ({ const imageRef = useRef(null); const imageCache = useRef({}); const sessionKey = useRef(Date.now().toString()); + const currentLoadingUrl = useRef(null); + const preloadedImagesRef = useRef<{[key: string]: HTMLImageElement}>({}); + const favoriteOperationsInProgress = useRef>(new Map()); const currentImage = localImages[currentIndex]; const preloadRange = 2; @@ -103,19 +108,8 @@ const ImageViewer: React.FC = ({ }); }, []); - // 当前加载中的图片URL追踪 - const currentLoadingUrl = useRef(null); - - // 预渲染图片容器 - const preloadedImagesRef = useRef<{[key: string]: HTMLImageElement}>({}); - - // 图片过渡状态 - const [fadeTransition, setFadeTransition] = useState(false); - const [, setActiveImage] = useState(null); - const loadImage = useCallback((imageUrl: string): Promise => { return new Promise((resolve, reject) => { - // 检查缓存 if (imageCache.current[imageUrl]?.loaded) { if (currentImage && imageUrl === currentImage.path) { setImageLoaded(true); @@ -152,13 +146,11 @@ const ImageViewer: React.FC = ({ }); }, [currentImage]); - // 图片切换逻辑优化 useEffect(() => { setImageLoaded(false); setCurrentLoading(true); setFadeTransition(true); - // 利用缓存快速显示 if (currentImage && imageCache.current[currentImage.path]?.loaded) { setActiveImage(currentImage.path); setImageLoaded(true); @@ -167,7 +159,6 @@ const ImageViewer: React.FC = ({ setTimeout(() => setFadeTransition(false), 100); } - // 重置缩放状态 setZoomPanState(prev => ({ ...prev, scale: 1, @@ -177,7 +168,6 @@ const ImageViewer: React.FC = ({ })); }, [currentIndex]); - // 可见性变化处理 useEffect(() => { if (visible && !wasVisible.current) { resetViewerState(); @@ -188,14 +178,12 @@ const ImageViewer: React.FC = ({ wasVisible.current = visible; }, [visible, resetViewerState]); - // 初始索引处理 useEffect(() => { if (visible && initialIndex >= 0 && initialIndex < images.length) { setCurrentIndex(initialIndex); } }, [visible, initialIndex, images.length]); - // 图片加载逻辑 useEffect(() => { if (!currentImage || !visible) return; @@ -218,7 +206,6 @@ const ImageViewer: React.FC = ({ setCurrentLoading(false); }); - // 预加载相邻图片 if (localImages.length > 1) { setTimeout(() => { for (let i = 1; i <= preloadRange; i++) { @@ -272,29 +259,39 @@ const ImageViewer: React.FC = ({ const handleFavoriteClick = useCallback(async () => { if (!currentImage) return; - + + if (favoriteOperationsInProgress.current.get(currentImage.id)) { + return; + } + try { + favoriteOperationsInProgress.current.set(currentImage.id, true); + if (onFavorite) { onFavorite(currentImage); return; } - + const isFavorited = currentImage.isFavorited; + const result = isFavorited ? await unfavoritePicture(currentImage.id) : await favoritePicture(currentImage.id); if (result.success) { message.success(isFavorited ? '已取消收藏' : '已添加到收藏'); + + const updatedImage = { + ...currentImage, + isFavorited: !isFavorited, + favoriteCount: isFavorited + ? Math.max(0, (currentImage.favoriteCount || 0) - 1) + : (currentImage.favoriteCount || 0) + 1 + }; + setLocalImages(prevImages => prevImages.map(img => - img.id === currentImage.id ? { - ...img, - isFavorited: !isFavorited, - favoriteCount: isFavorited - ? Math.max(0, (img.favoriteCount || 0) - 1) - : (img.favoriteCount || 0) + 1 - } : img + img.id === currentImage.id ? updatedImage : img ) ); } else { @@ -303,6 +300,10 @@ const ImageViewer: React.FC = ({ } catch (error) { console.error('收藏操作失败:', error); message.error('操作失败,请重试'); + } finally { + setTimeout(() => { + favoriteOperationsInProgress.current.delete(currentImage.id); + }, 300); } }, [currentImage, onFavorite]); @@ -484,7 +485,6 @@ const ImageViewer: React.FC = ({ } }, [visible, handleMouseUp, handleTouchEnd]); - // 渲染优化:减少重绘和提高性能 if (images.length === 0 || !currentImage) { return null; } @@ -595,21 +595,20 @@ const ImageViewer: React.FC = ({
{currentImage.name}
- {onFavorite && ( - - )} + +