feat(AlbumService, ImageGrid, ImageViewer): enhance album deletion, optimize image loading, and improve favorite handling

This commit is contained in:
shiyu
2025-05-25 17:24:36 +08:00
parent e82eb326f5
commit 4a8b447e0d
5 changed files with 124 additions and 84 deletions
+15 -2
View File
@@ -194,9 +194,22 @@ public class AlbumService(
if (album == null) if (album == null)
return false; 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); dbContext.Albums.Remove(album);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
+46 -32
View File
@@ -178,42 +178,36 @@ const ImageGrid: React.FC<ImageGridProps> = ({
const requestState = useRef({ const requestState = useRef({
inProgress: false, inProgress: false,
lastParams: '', lastParams: '',
noResultsFor: '' // 新增:记录哪些查询参数没有返回结果 noResultsFor: ''
}); });
// 优化构建查询参数函数 const favoriteOperationsInProgress = useRef<Map<number, boolean>>(new Map());
const buildQueryParams = useCallback((): PaginationParams => {
const params: PaginationParams = { const buildQueryParams = useCallback((): PaginationParams => ({
page: currentPage, page: currentPage,
pageSize, pageSize,
...queryParams, ...queryParams,
searchQuery: queryParams.searchQuery, searchQuery: queryParams.searchQuery,
tags: Array.isArray(queryParams.tags) ? queryParams.tags.join(',') : undefined, tags: Array.isArray(queryParams.tags) ? queryParams.tags.join(',') : undefined,
useVectorSearch: queryParams.useVectorSearch, useVectorSearch: queryParams.useVectorSearch,
similarityThreshold: queryParams.similarityThreshold similarityThreshold: queryParams.similarityThreshold
}; }), [currentPage, pageSize, queryParams]);
return params;
}, [currentPage, pageSize, queryParams]);
// 优化加载数据函数,减少依赖项
const loadImages = useCallback(async () => { const loadImages = useCallback(async () => {
if (isUsingExternalData || requestState.current.inProgress) return; if (isUsingExternalData || requestState.current.inProgress) return;
const params = buildQueryParams(); const params = buildQueryParams();
const paramsString = JSON.stringify(params); const paramsString = JSON.stringify(params);
// 检查是否是已知没有结果的查询 if (requestState.current.noResultsFor === paramsString && images.length === 0) {
return;
}
if (requestState.current.noResultsFor === paramsString) { if (requestState.current.noResultsFor === paramsString) {
// 如果这个查询之前没有结果,且当前还是空结果状态,不再重复请求
if (images.length === 0) {
return;
}
// 如果之前没结果但现在有图片显示,重置noResultsFor让新查询可以执行
requestState.current.noResultsFor = ''; requestState.current.noResultsFor = '';
} }
if (requestState.current.lastParams === paramsString) { if (requestState.current.lastParams === paramsString) {
// 如果参数没变且已有数据或无数据状态已确认,跳过请求
return; return;
} }
@@ -228,16 +222,13 @@ const ImageGrid: React.FC<ImageGridProps> = ({
try { try {
const result = await getPictures(params); const result = await getPictures(params);
// 无论成功与否,都更新lastParams以避免相同参数的重复请求
requestState.current.lastParams = paramsString; requestState.current.lastParams = paramsString;
if (result.success) { if (result.success) {
// 更新图片数据
setImages(result.data || []); setImages(result.data || []);
setTotalImages(result.totalCount || 0); setTotalImages(result.totalCount || 0);
onImagesLoaded?.(result.data || [], result.totalCount || 0); onImagesLoaded?.(result.data || [], result.totalCount || 0);
// 如果结果为空,记录到noResultsFor
if (!result.data || result.data.length === 0) { if (!result.data || result.data.length === 0) {
requestState.current.noResultsFor = paramsString; requestState.current.noResultsFor = paramsString;
} }
@@ -264,10 +255,20 @@ const ImageGrid: React.FC<ImageGridProps> = ({
if (isUsingExternalData && dataSource) setImages(dataSource); if (isUsingExternalData && dataSource) setImages(dataSource);
}, [dataSource, isUsingExternalData]); }, [dataSource, isUsingExternalData]);
// 优化收藏/取消收藏逻辑 // 优化收藏/取消收藏逻辑,添加防抖功能
const handleToggleFavorite = async (image: PictureResponse) => { const handleToggleFavorite = async (image: PictureResponse) => {
const { id, isFavorited } = image;
// 检查此图片是否有收藏操作正在进行中
if (favoriteOperationsInProgress.current.get(id)) {
// 如果有操作正在进行中,直接返回,不执行新的操作
return;
}
try { try {
const { id, isFavorited } = image; // 设置操作锁定状态
favoriteOperationsInProgress.current.set(id, true);
const api = isFavorited ? unfavoritePicture : favoritePicture; const api = isFavorited ? unfavoritePicture : favoritePicture;
const result = await api(id); const result = await api(id);
@@ -293,6 +294,11 @@ const ImageGrid: React.FC<ImageGridProps> = ({
} }
} catch (error) { } catch (error) {
message.error('操作失败,请重试'); message.error('操作失败,请重试');
} finally {
// 操作完成后,延迟300ms释放锁定状态,进一步防止快速重复点击
setTimeout(() => {
favoriteOperationsInProgress.current.delete(id);
}, 300);
} }
}; };
@@ -382,12 +388,20 @@ const ImageGrid: React.FC<ImageGridProps> = ({
// 处理图片更新成功 // 处理图片更新成功
const handleImageUpdateSuccess = (updatedImage: PictureResponse) => { const handleImageUpdateSuccess = (updatedImage: PictureResponse) => {
// 更新本地图片列表中对应的图片
setImages(prevImages => setImages(prevImages =>
prevImages.map(img => prevImages.map(img => {
img.id === updatedImage.id ? { ...img, ...updatedImage } : img if (img.id === updatedImage.id) {
) return {
...img,
...updatedImage,
userId: img.userId,
permission: updatedImage.permission ?? img.permission
};
}
return img;
})
); );
closeContextMenu();
}; };
// 修改handleMenuAction中的编辑处理 // 修改handleMenuAction中的编辑处理
@@ -716,7 +730,7 @@ const ImageGrid: React.FC<ImageGridProps> = ({
onClose={() => setViewerState({ ...viewerState, visible: false })} onClose={() => setViewerState({ ...viewerState, visible: false })}
images={images} images={images}
initialIndex={viewerState.index} initialIndex={viewerState.index}
onFavorite={onToggleFavorite || handleToggleFavorite} onFavorite={handleToggleFavorite} // 总是传递本地的handleToggleFavorite而不是条件判断
showFavoriteCount={showFavoriteCount} showFavoriteCount={showFavoriteCount}
onShare={handleShareImage} onShare={handleShareImage}
/> />
+12 -3
View File
@@ -200,12 +200,15 @@
border: none; border: none;
background: transparent !important; background: transparent !important;
font-size: 18px; font-size: 18px;
border-radius: 50%; border-radius: 8px;
height: 40px; height: 40px;
width: 40px; min-width: 40px;
padding: 0 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
transition: background-color 0.2s ease;
} }
.footer-btn:hover { .footer-btn:hover {
@@ -213,8 +216,14 @@
} }
.footer-btn span { .footer-btn span {
margin-left: 4px;
font-size: 14px; font-size: 14px;
line-height: 1;
}
.footer-btn .anticon + span {
display: inline-block;
min-width: 16px;
text-align: center;
} }
.image-info-drawer { .image-info-drawer {
+42 -43
View File
@@ -63,6 +63,8 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
const [shareDialogVisible, setShareDialogVisible] = useState(false); const [shareDialogVisible, setShareDialogVisible] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
const [currentLoading, setCurrentLoading] = useState(false); const [currentLoading, setCurrentLoading] = useState(false);
const [fadeTransition, setFadeTransition] = useState(false);
const [, setActiveImage] = useState<string | null>(null);
const [zoomPanState, setZoomPanState] = useState<ZoomPanState>({ const [zoomPanState, setZoomPanState] = useState<ZoomPanState>({
scale: 1, scale: 1,
@@ -79,6 +81,9 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
const imageRef = useRef<HTMLImageElement>(null); const imageRef = useRef<HTMLImageElement>(null);
const imageCache = useRef<ImageCache>({}); const imageCache = useRef<ImageCache>({});
const sessionKey = useRef<string>(Date.now().toString()); const sessionKey = useRef<string>(Date.now().toString());
const currentLoadingUrl = useRef<string | null>(null);
const preloadedImagesRef = useRef<{[key: string]: HTMLImageElement}>({});
const favoriteOperationsInProgress = useRef<Map<number, boolean>>(new Map());
const currentImage = localImages[currentIndex]; const currentImage = localImages[currentIndex];
const preloadRange = 2; const preloadRange = 2;
@@ -103,19 +108,8 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
}); });
}, []); }, []);
// 当前加载中的图片URL追踪
const currentLoadingUrl = useRef<string | null>(null);
// 预渲染图片容器
const preloadedImagesRef = useRef<{[key: string]: HTMLImageElement}>({});
// 图片过渡状态
const [fadeTransition, setFadeTransition] = useState(false);
const [, setActiveImage] = useState<string | null>(null);
const loadImage = useCallback((imageUrl: string): Promise<HTMLImageElement> => { const loadImage = useCallback((imageUrl: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 检查缓存
if (imageCache.current[imageUrl]?.loaded) { if (imageCache.current[imageUrl]?.loaded) {
if (currentImage && imageUrl === currentImage.path) { if (currentImage && imageUrl === currentImage.path) {
setImageLoaded(true); setImageLoaded(true);
@@ -152,13 +146,11 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
}); });
}, [currentImage]); }, [currentImage]);
// 图片切换逻辑优化
useEffect(() => { useEffect(() => {
setImageLoaded(false); setImageLoaded(false);
setCurrentLoading(true); setCurrentLoading(true);
setFadeTransition(true); setFadeTransition(true);
// 利用缓存快速显示
if (currentImage && imageCache.current[currentImage.path]?.loaded) { if (currentImage && imageCache.current[currentImage.path]?.loaded) {
setActiveImage(currentImage.path); setActiveImage(currentImage.path);
setImageLoaded(true); setImageLoaded(true);
@@ -167,7 +159,6 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
setTimeout(() => setFadeTransition(false), 100); setTimeout(() => setFadeTransition(false), 100);
} }
// 重置缩放状态
setZoomPanState(prev => ({ setZoomPanState(prev => ({
...prev, ...prev,
scale: 1, scale: 1,
@@ -177,7 +168,6 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
})); }));
}, [currentIndex]); }, [currentIndex]);
// 可见性变化处理
useEffect(() => { useEffect(() => {
if (visible && !wasVisible.current) { if (visible && !wasVisible.current) {
resetViewerState(); resetViewerState();
@@ -188,14 +178,12 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
wasVisible.current = visible; wasVisible.current = visible;
}, [visible, resetViewerState]); }, [visible, resetViewerState]);
// 初始索引处理
useEffect(() => { useEffect(() => {
if (visible && initialIndex >= 0 && initialIndex < images.length) { if (visible && initialIndex >= 0 && initialIndex < images.length) {
setCurrentIndex(initialIndex); setCurrentIndex(initialIndex);
} }
}, [visible, initialIndex, images.length]); }, [visible, initialIndex, images.length]);
// 图片加载逻辑
useEffect(() => { useEffect(() => {
if (!currentImage || !visible) return; if (!currentImage || !visible) return;
@@ -218,7 +206,6 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
setCurrentLoading(false); setCurrentLoading(false);
}); });
// 预加载相邻图片
if (localImages.length > 1) { if (localImages.length > 1) {
setTimeout(() => { setTimeout(() => {
for (let i = 1; i <= preloadRange; i++) { for (let i = 1; i <= preloadRange; i++) {
@@ -272,29 +259,39 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
const handleFavoriteClick = useCallback(async () => { const handleFavoriteClick = useCallback(async () => {
if (!currentImage) return; if (!currentImage) return;
if (favoriteOperationsInProgress.current.get(currentImage.id)) {
return;
}
try { try {
favoriteOperationsInProgress.current.set(currentImage.id, true);
if (onFavorite) { if (onFavorite) {
onFavorite(currentImage); onFavorite(currentImage);
return; return;
} }
const isFavorited = currentImage.isFavorited; const isFavorited = currentImage.isFavorited;
const result = isFavorited const result = isFavorited
? await unfavoritePicture(currentImage.id) ? await unfavoritePicture(currentImage.id)
: await favoritePicture(currentImage.id); : await favoritePicture(currentImage.id);
if (result.success) { if (result.success) {
message.success(isFavorited ? '已取消收藏' : '已添加到收藏'); message.success(isFavorited ? '已取消收藏' : '已添加到收藏');
const updatedImage = {
...currentImage,
isFavorited: !isFavorited,
favoriteCount: isFavorited
? Math.max(0, (currentImage.favoriteCount || 0) - 1)
: (currentImage.favoriteCount || 0) + 1
};
setLocalImages(prevImages => setLocalImages(prevImages =>
prevImages.map(img => prevImages.map(img =>
img.id === currentImage.id ? { img.id === currentImage.id ? updatedImage : img
...img,
isFavorited: !isFavorited,
favoriteCount: isFavorited
? Math.max(0, (img.favoriteCount || 0) - 1)
: (img.favoriteCount || 0) + 1
} : img
) )
); );
} else { } else {
@@ -303,6 +300,10 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
} catch (error) { } catch (error) {
console.error('收藏操作失败:', error); console.error('收藏操作失败:', error);
message.error('操作失败,请重试'); message.error('操作失败,请重试');
} finally {
setTimeout(() => {
favoriteOperationsInProgress.current.delete(currentImage.id);
}, 300);
} }
}, [currentImage, onFavorite]); }, [currentImage, onFavorite]);
@@ -484,7 +485,6 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
} }
}, [visible, handleMouseUp, handleTouchEnd]); }, [visible, handleMouseUp, handleTouchEnd]);
// 渲染优化:减少重绘和提高性能
if (images.length === 0 || !currentImage) { if (images.length === 0 || !currentImage) {
return null; return null;
} }
@@ -595,21 +595,20 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
<div className="image-name">{currentImage.name}</div> <div className="image-name">{currentImage.name}</div>
<div className="footer-actions"> <div className="footer-actions">
{onFavorite && ( <Button
<Button type="text"
type="text" icon={currentImage.isFavorited ?
icon={currentImage.isFavorited ? <HeartFilled style={{ color: '#ff4d4f' }} /> :
<HeartFilled style={{ color: '#ff4d4f' }} /> : <HeartOutlined style={{ color: '#fff' }} />
<HeartOutlined style={{ color: '#fff' }} /> }
} onClick={handleFavoriteClick}
onClick={handleFavoriteClick} className="footer-btn"
className="footer-btn" >
> {showFavoriteCount && typeof currentImage.favoriteCount === 'number' && (
{showFavoriteCount && typeof currentImage.favoriteCount === 'number' && ( <span>{currentImage.favoriteCount}</span>
<span>{currentImage.favoriteCount}</span> )}
)} </Button>
</Button>
)}
<Dropdown menu={{ items: albumItems }} disabled={loadingAlbums || albums.length === 0}> <Dropdown menu={{ items: albumItems }} disabled={loadingAlbums || albums.length === 0}>
<Button <Button
type="text" type="text"
+9 -4
View File
@@ -19,7 +19,7 @@ type OutletContextType = {
function AlbumDetail() { function AlbumDetail() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { updateBreadcrumbTitle } = useOutletContext<OutletContextType>(); const { updateBreadcrumbTitle = () => {} } = useOutletContext<OutletContextType>();
const [album, setAlbum] = useState<AlbumResponse | null>(null); const [album, setAlbum] = useState<AlbumResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -28,6 +28,7 @@ function AlbumDetail() {
const [selectedPictures, setSelectedPictures] = useState<number[]>([]); const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); // 添加刷新触发器
const loadAlbum = async () => { const loadAlbum = async () => {
if (!id) return; if (!id) return;
@@ -36,8 +37,10 @@ function AlbumDetail() {
const result = await getAlbumById(parseInt(id)); const result = await getAlbumById(parseInt(id));
if (result.success && result.data) { if (result.success && result.data) {
setAlbum(result.data); setAlbum(result.data);
// 更新面包屑标题 // 添加检查确保 updateBreadcrumbTitle 是一个函数
updateBreadcrumbTitle(result.data.name); if (typeof updateBreadcrumbTitle === 'function') {
updateBreadcrumbTitle(result.data.name);
}
} else { } else {
message.error(result.message || '获取相册失败'); message.error(result.message || '获取相册失败');
} }
@@ -114,6 +117,7 @@ function AlbumDetail() {
message.success(`已添加 ${selectedPictures.length} 张图片到相册`); message.success(`已添加 ${selectedPictures.length} 张图片到相册`);
setSelectedPictures([]); setSelectedPictures([]);
loadAlbum(); loadAlbum();
setRefreshTrigger(prev => prev + 1); // 更新刷新触发器
} else { } else {
message.error(result.message || '添加图片到相册失败'); message.error(result.message || '添加图片到相册失败');
} }
@@ -289,7 +293,8 @@ function AlbumDetail() {
</div> </div>
<ImageGrid <ImageGrid
queryParams={{ albumId: parseInt(id || '0') }} // 直接传入albumId参数获取相册图片 key={refreshTrigger}
queryParams={{ albumId: parseInt(id || '0') }}
onToggleFavorite={handleToggleFavorite} onToggleFavorite={handleToggleFavorite}
showFavoriteCount={true} showFavoriteCount={true}
showPagination={true} showPagination={true}