mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-14 03:49:58 +08:00
feat(AlbumService, ImageGrid, ImageViewer): enhance album deletion, optimize image loading, and improve favorite handling
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user