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

View File

@@ -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();

View File

@@ -178,42 +178,36 @@ const ImageGrid: React.FC<ImageGridProps> = ({
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<Map<number, boolean>>(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<ImageGridProps> = ({
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<ImageGridProps> = ({
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<ImageGridProps> = ({
}
} catch (error) {
message.error('操作失败,请重试');
} finally {
// 操作完成后延迟300ms释放锁定状态进一步防止快速重复点击
setTimeout(() => {
favoriteOperationsInProgress.current.delete(id);
}, 300);
}
};
@@ -382,12 +388,20 @@ const ImageGrid: React.FC<ImageGridProps> = ({
// 处理图片更新成功
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<ImageGridProps> = ({
onClose={() => setViewerState({ ...viewerState, visible: false })}
images={images}
initialIndex={viewerState.index}
onFavorite={onToggleFavorite || handleToggleFavorite}
onFavorite={handleToggleFavorite} // 总是传递本地的handleToggleFavorite而不是条件判断
showFavoriteCount={showFavoriteCount}
onShare={handleShareImage}
/>

View File

@@ -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 {

View File

@@ -63,6 +63,8 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
const [shareDialogVisible, setShareDialogVisible] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [currentLoading, setCurrentLoading] = useState(false);
const [fadeTransition, setFadeTransition] = useState(false);
const [, setActiveImage] = useState<string | null>(null);
const [zoomPanState, setZoomPanState] = useState<ZoomPanState>({
scale: 1,
@@ -79,6 +81,9 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
const imageRef = useRef<HTMLImageElement>(null);
const imageCache = useRef<ImageCache>({});
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 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> => {
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<ImageViewerProps> = ({
});
}, [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<ImageViewerProps> = ({
setTimeout(() => setFadeTransition(false), 100);
}
// 重置缩放状态
setZoomPanState(prev => ({
...prev,
scale: 1,
@@ -177,7 +168,6 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
}));
}, [currentIndex]);
// 可见性变化处理
useEffect(() => {
if (visible && !wasVisible.current) {
resetViewerState();
@@ -188,14 +178,12 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
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<ImageViewerProps> = ({
setCurrentLoading(false);
});
// 预加载相邻图片
if (localImages.length > 1) {
setTimeout(() => {
for (let i = 1; i <= preloadRange; i++) {
@@ -272,29 +259,39 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
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<ImageViewerProps> = ({
} 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<ImageViewerProps> = ({
}
}, [visible, handleMouseUp, handleTouchEnd]);
// 渲染优化:减少重绘和提高性能
if (images.length === 0 || !currentImage) {
return null;
}
@@ -595,21 +595,20 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
<div className="image-name">{currentImage.name}</div>
<div className="footer-actions">
{onFavorite && (
<Button
type="text"
icon={currentImage.isFavorited ?
<HeartFilled style={{ color: '#ff4d4f' }} /> :
<HeartOutlined style={{ color: '#fff' }} />
}
onClick={handleFavoriteClick}
className="footer-btn"
>
{showFavoriteCount && typeof currentImage.favoriteCount === 'number' && (
<span>{currentImage.favoriteCount}</span>
)}
</Button>
)}
<Button
type="text"
icon={currentImage.isFavorited ?
<HeartFilled style={{ color: '#ff4d4f' }} /> :
<HeartOutlined style={{ color: '#fff' }} />
}
onClick={handleFavoriteClick}
className="footer-btn"
>
{showFavoriteCount && typeof currentImage.favoriteCount === 'number' && (
<span>{currentImage.favoriteCount}</span>
)}
</Button>
<Dropdown menu={{ items: albumItems }} disabled={loadingAlbums || albums.length === 0}>
<Button
type="text"

View File

@@ -19,7 +19,7 @@ type OutletContextType = {
function AlbumDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { updateBreadcrumbTitle } = useOutletContext<OutletContextType>();
const { updateBreadcrumbTitle = () => {} } = useOutletContext<OutletContextType>();
const [album, setAlbum] = useState<AlbumResponse | null>(null);
const [loading, setLoading] = useState(true);
@@ -28,6 +28,7 @@ function AlbumDetail() {
const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
const [editForm] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); // 添加刷新触发器
const loadAlbum = async () => {
if (!id) return;
@@ -36,8 +37,10 @@ function AlbumDetail() {
const result = await getAlbumById(parseInt(id));
if (result.success && result.data) {
setAlbum(result.data);
// 更新面包屑标题
updateBreadcrumbTitle(result.data.name);
// 添加检查确保 updateBreadcrumbTitle 是一个函数
if (typeof updateBreadcrumbTitle === 'function') {
updateBreadcrumbTitle(result.data.name);
}
} else {
message.error(result.message || '获取相册失败');
}
@@ -114,6 +117,7 @@ function AlbumDetail() {
message.success(`已添加 ${selectedPictures.length} 张图片到相册`);
setSelectedPictures([]);
loadAlbum();
setRefreshTrigger(prev => prev + 1); // 更新刷新触发器
} else {
message.error(result.message || '添加图片到相册失败');
}
@@ -289,7 +293,8 @@ function AlbumDetail() {
</div>
<ImageGrid
queryParams={{ albumId: parseInt(id || '0') }} // 直接传入albumId参数获取相册图片
key={refreshTrigger}
queryParams={{ albumId: parseInt(id || '0') }}
onToggleFavorite={handleToggleFavorite}
showFavoriteCount={true}
showPagination={true}