193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
"use client"
|
|
|
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
|
import { Button } from "@/components/ui/button"
|
|
import { InfiniteScrollList } from "@/components/ui/infinite-scroll-list"
|
|
import RenderContactStatusText from "./components/RenderContactStatusText"
|
|
import { useHeartbeatRelationListInfinite } from "@/hooks/useIm"
|
|
import { HeartbeatRelationListOutput } from "@/services/im/types"
|
|
import { useMemo } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import AIRelationTag from "@/components/features/AIRelationTag"
|
|
import Link from "next/link"
|
|
import { usePrefetchRoutes } from "@/hooks/useGlobalPrefetchRoutes"
|
|
import Image from "next/image"
|
|
|
|
// 联系人数据类型现在使用API返回的数据结构
|
|
type ContactItem = HeartbeatRelationListOutput
|
|
|
|
// 联系人卡片组件
|
|
const ContactCard = ({ contact }: { contact: ContactItem }) => {
|
|
const router = useRouter();
|
|
|
|
// 计算年龄
|
|
const age = useMemo(() => {
|
|
if (!contact.birthday) return null;
|
|
const birthYear = new Date(contact.birthday).getFullYear();
|
|
const currentYear = new Date().getFullYear();
|
|
return currentYear - birthYear;
|
|
}, [contact.birthday]);
|
|
|
|
// 跳转到聊天页面
|
|
const handleChatClick = () => {
|
|
if (contact.aiId) {
|
|
router.push(`/chat/${contact.aiId}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-between w-full gap-4">
|
|
{/* 用户信息部分 */}
|
|
<div className="flex items-center gap-4 flex-1">
|
|
{/* 头像 */}
|
|
<Link href={`/@${contact.aiId}`} prefetch>
|
|
<Avatar className="size-16">
|
|
<AvatarImage src={contact.headImg} alt={contact.nickname || contact.roleName} />
|
|
<AvatarFallback className="bg-surface-element-normal text-txt-primary-normal txt-title-m">
|
|
{(contact.nickname || contact.roleName || 'A').charAt(0)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Link>
|
|
|
|
{/* 用户详细信息 */}
|
|
<div className="flex flex-col gap-2 flex-1">
|
|
{/* 名字和标签 */}
|
|
<div className="flex items-center gap-2">
|
|
<Link href={`/@${contact.aiId}`} prefetch>
|
|
<h3 className="txt-title-m text-white">
|
|
{contact.nickname || contact.roleName}
|
|
</h3>
|
|
</Link>
|
|
{contact.heartbeatLevel && contact.isShow && <AIRelationTag heartbeatLevel={contact.heartbeatLevel} size="small" />}
|
|
</div>
|
|
|
|
{/* 心动值和用户信息 */}
|
|
<div className="flex items-center gap-2">
|
|
{/* 心动值 */}
|
|
<div className="flex items-center gap-1">
|
|
<Image
|
|
src="/icons/heart.svg"
|
|
alt="Heart"
|
|
width={12}
|
|
height={12}
|
|
className="w-3 h-3"
|
|
/>
|
|
<span className="txt-numMonotype-s">
|
|
{contact.heartbeatVal || 0}°
|
|
</span>
|
|
</div>
|
|
|
|
{/* 分隔线 */}
|
|
<div className="w-0 h-3 border-l border-outline-normal" />
|
|
|
|
{/* 用户详细信息 */}
|
|
<span className="txt-label-m text-white text-ellipsis overflow-hidden whitespace-nowrap flex-1">
|
|
{[
|
|
age && `${age}`,
|
|
contact.characterName,
|
|
contact.tagName
|
|
].filter(Boolean).join(" · ")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 聊天按钮 */}
|
|
<Button className="min-w-[80px]" onClick={handleChatClick}>
|
|
<i className="iconfont icon-Chat !text-[24px]" />
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const ContactsPage = () => {
|
|
// 使用无限查询获取心动关系列表
|
|
const {
|
|
data,
|
|
fetchNextPage,
|
|
hasNextPage,
|
|
isLoading,
|
|
isFetchingNextPage,
|
|
error
|
|
} = useHeartbeatRelationListInfinite();
|
|
|
|
// 扁平化所有页面的数据
|
|
const allContacts = useMemo(() => {
|
|
return data?.pages.flatMap(page => page.datas || []) || [];
|
|
}, [data]);
|
|
const chatRoutes = useMemo(
|
|
() => allContacts.slice(0, 20).map((contact) => contact?.aiId ? `/chat/${contact.aiId}` : null),
|
|
[allContacts]
|
|
)
|
|
usePrefetchRoutes(chatRoutes)
|
|
|
|
// 加载状态骨架屏组件
|
|
const ContactSkeleton = () => (
|
|
<div className="flex items-center justify-between w-full gap-4 animate-pulse">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<div className="size-16 bg-surface-nest-normal rounded-full" />
|
|
<div className="flex flex-col gap-2 flex-1">
|
|
<div className="flex items-start gap-2">
|
|
<div className="h-6 bg-surface-nest-normal rounded w-24" />
|
|
<div className="h-5 bg-surface-nest-normal rounded w-16" />
|
|
</div>
|
|
<div className="h-4 bg-surface-nest-normal rounded w-48" />
|
|
</div>
|
|
</div>
|
|
<div className="w-[80px] h-10 bg-surface-nest-normal rounded" />
|
|
</div>
|
|
);
|
|
|
|
// 空状态组件
|
|
const EmptyState = () => (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
<Image
|
|
src="/icons/empty.svg"
|
|
alt="Empty"
|
|
width={64}
|
|
height={64}
|
|
className="w-16 h-16 mb-4 opacity-50"
|
|
/>
|
|
<h3 className="txt-title-m text-txt-secondary-normal mb-2">
|
|
No crushes found
|
|
</h3>
|
|
<p className="txt-body-m text-txt-tertiary-normal">
|
|
Start chatting with AI characters to build your crushes
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 w-[752px] mx-auto pt-28 pb-[200px]">
|
|
{/* 页面标题 */}
|
|
<h1 className="txt-title-l">
|
|
My Crushes
|
|
</h1>
|
|
|
|
{/* 心动值统计信息栏 */}
|
|
<RenderContactStatusText hasContacts={allContacts.length > 0} />
|
|
|
|
{/* 联系人列表 */}
|
|
<InfiniteScrollList<ContactItem>
|
|
items={allContacts}
|
|
renderItem={(contact) => (
|
|
<ContactCard contact={contact} />
|
|
)}
|
|
getItemKey={(contact) => contact.aiId?.toString() || 'unknown'}
|
|
hasNextPage={!!hasNextPage}
|
|
isLoading={isLoading || isFetchingNextPage}
|
|
fetchNextPage={fetchNextPage}
|
|
columns={1}
|
|
gap={4}
|
|
className="!grid-cols-1" // 强制单列布局
|
|
LoadingSkeleton={ContactSkeleton}
|
|
EmptyComponent={EmptyState}
|
|
hasError={!!error}
|
|
onRetry={() => window.location.reload()}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ContactsPage
|