crush-level-web/src/app/(main)/user/[userId]/components/AlbumList.tsx

219 lines
7.4 KiB
TypeScript

'use client';
import { useParams } from "next/navigation";
import { useGetAIUserAlbumInfinite, useLikeAlbumImage, useUnlockAlbumImage, useUnlockImage } from "@/hooks/aiUser";
import { IAlbumItem, LikedStatus, LockStatus } from "@/services/user/types";
import Empty from "@/components/ui/empty";
import { toast } from "sonner";
import AlbumItem from "./AlbumItem";
import { InfiniteScrollList } from "@/components/ui/infinite-scroll-list";
import { ImageViewer, ImageViewerPaginationContent } from "@/components/ui/image-viewer";
import { useImageViewer } from "@/hooks/useImageViewer";
import { useMemo, useRef, useState } from "react";
import { useAIUser } from "../context/aiUser";
import AlbumImageViewerAction from "./AlbumImageViewerAction";
import Image from "next/image";
import { formatFromCents } from "@/utils/number";
// 专门的相册骨架屏组件
const AlbumSkeleton = () => (
<div className="relative pb-[134%] rounded-2xl overflow-hidden bg-surface-nest-normal animate-pulse">
<div className="absolute inset-0">
<div className="w-full h-full rounded-2xl" />
</div>
</div>
);
const AlbumList = () => {
const { userId } = useParams();
const pageSize = 20;
const unlockingAlbumIdsRef = useRef<Set<number>>(new Set());
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetAIUserAlbumInfinite(Number(userId), pageSize);
const likeMutation = useLikeAlbumImage();
const unlockMutation = useUnlockImage();
const { isOwner } = useAIUser();
const [tempList, setTempList] = useState<IAlbumItem[]>([]);
// 图片查看器
const {
isOpen: isViewerOpen,
currentIndex: viewerIndex,
openViewer,
closeViewer,
handleIndexChange,
} = useImageViewer();
// 展平所有页面的数据
const albumItems = useMemo(() => {
if (!data?.pages) return [];
return data.pages.flatMap(page => page.datas || []);
}, [data?.pages]);
const handleLike = (albumId: number, isLiked: boolean) => {
likeMutation.mutate(
{ albumId, likedStatus: isLiked ? LikedStatus.Canceled : LikedStatus.Liked, aiId: Number(userId) },
);
};
const handleUnlock = async (imageId: number) => {
await unlockMutation.mutateAsync(
{ aiId: Number(userId), albumId: imageId },
{
onSuccess: () => {
toast.success("Unlocked successfully!");
},
onError: (error) => {
}
}
);
};
const handleImageClick = (item: IAlbumItem, index: number) => {
// 获取所有图片URL
const imageUrls = albumItems.map(albumItem => albumItem.imgUrl || albumItem.img3);
setTempList(albumItems);
// 打开图片查看器
openViewer(imageUrls, index);
};
const viewerImages: IAlbumItem[] = useMemo(() => {
return tempList.map((item) => {
const { albumId } = item;
const data = albumItems.find((item) => item.albumId === albumId) || {};
return data as IAlbumItem;
});
}, [tempList, albumItems]);
return (
<>
<InfiniteScrollList<IAlbumItem>
items={albumItems}
renderItem={(item, index) => (
<AlbumItem
item={item}
onLike={handleLike}
onImageClick={() => handleImageClick(item, index)}
/>
)}
getItemKey={(item) => item.albumId}
hasNextPage={!!hasNextPage}
isLoading={isLoading || isFetchingNextPage}
fetchNextPage={fetchNextPage}
columns={{
default: 2,
sm: 3,
md: 4,
lg: 4,
xl: 5
}}
gap={4}
LoadingSkeleton={AlbumSkeleton}
EmptyComponent={() => (
<div className="bg-surface-base-normal rounded-lg p-6 py-[117px]">
<Empty title="No photos yet" />
</div>
)}
threshold={300}
enabled={true}
/>
{/* 图片查看器 */}
<ImageViewer
images={viewerImages.map(albumItem => albumItem.imgUrl || albumItem.img3)}
currentIndex={viewerIndex}
open={isViewerOpen}
onClose={closeViewer}
onIndexChange={handleIndexChange}
showChooseButton={false}
ActionComponent={() => {
return (
<AlbumImageViewerAction
datas={tempList}
originalDatas={albumItems}
currentIndex={viewerIndex}
onUnlock={handleUnlock}
unlockingAlbumIdsRef={unlockingAlbumIdsRef}
onDeleted={(nextIndex) => {
if (nextIndex === null) {
// 删除后没有图片了
closeViewer();
return;
}
// 调整到新的索引,避免越界
handleIndexChange(nextIndex);
}}
/>
)
}}
OverlayComponent={() => {
const findItem = albumItems.find((item, index) => index === viewerIndex);
const { unlockPrice, lockStatus } = findItem || {};
if (isOwner || !lockStatus || lockStatus === LockStatus.Unlock) {
return null;
}
return (
<div className="absolute inset-0 z-10 bg-black/15 backdrop-blur-3xl flex flex-col justify-center items-center gap-6">
<i className="iconfont icon-private !text-[48px] leading-none" />
<div className="flex items-center gap-2">
<Image src="/icons/diamond.svg" alt="diamond" width={32} height={32} />
<div className="txt-title-m">{`${formatFromCents(unlockPrice || 0)} to unlock`}</div>
</div>
</div>
)
}}
PaginationComponent={() => {
const currentIndex = viewerIndex + 1;
const findItem = tempList.find((item, index) => index === viewerIndex);
const { albumId } = findItem || {};
const { lockStatus } = albumItems.find((item) => item.albumId === albumId) || {};
if (isOwner) {
if (lockStatus) {
return (
<ImageViewerPaginationContent className="gap-2">
<i className="iconfont icon-private !text-[16px] leading-none" />
<span>{`${currentIndex}/${albumItems.length}`}</span>
</ImageViewerPaginationContent>
)
}
return (
<ImageViewerPaginationContent>
<span>{`${currentIndex}/${albumItems.length}`}</span>
</ImageViewerPaginationContent>
)
}
if (lockStatus === LockStatus.Lock) {
return (
<ImageViewerPaginationContent className="bg-primary-gradient-normal gap-2">
<i className="iconfont icon-private !text-[16px] leading-none" />
<span>{`${currentIndex}/${albumItems.length}`}</span>
</ImageViewerPaginationContent>
)
}
if (lockStatus === LockStatus.Unlock) {
return (
<ImageViewerPaginationContent className="bg-primary-gradient-normal gap-2">
<i className="iconfont icon-public !text-[16px] leading-none" />
<span>{`${currentIndex}/${albumItems.length}`}</span>
</ImageViewerPaginationContent>
)
}
return (
<ImageViewerPaginationContent>
<span>{`${currentIndex}/${albumItems.length}`}</span>
</ImageViewerPaginationContent>
)
}}
/>
</>
);
};
export default AlbumList;