219 lines
7.4 KiB
TypeScript
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;
|