crush-level-web/src/app/(main)/contact/contact-page.tsx

178 lines
6.2 KiB
TypeScript
Raw Normal View History

2025-11-28 06:31:36 +00:00
'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'
2025-11-13 08:38:25 +00:00
// 联系人数据类型现在使用API返回的数据结构
type ContactItem = HeartbeatRelationListOutput
// 联系人卡片组件
const ContactCard = ({ contact }: { contact: ContactItem }) => {
2025-11-28 06:31:36 +00:00
const router = useRouter()
2025-11-13 08:38:25 +00:00
// 计算年龄
const age = useMemo(() => {
2025-11-28 06:31:36 +00:00
if (!contact.birthday) return null
const birthYear = new Date(contact.birthday).getFullYear()
const currentYear = new Date().getFullYear()
return currentYear - birthYear
}, [contact.birthday])
2025-11-13 08:38:25 +00:00
// 跳转到聊天页面
const handleChatClick = () => {
if (contact.aiId) {
2025-11-28 06:31:36 +00:00
router.push(`/chat/${contact.aiId}`)
2025-11-13 08:38:25 +00:00
}
2025-11-28 06:31:36 +00:00
}
2025-11-13 08:38:25 +00:00
return (
2025-11-28 06:31:36 +00:00
<div className="flex w-full items-center justify-between gap-4">
2025-11-13 08:38:25 +00:00
{/* 用户信息部分 */}
2025-11-28 06:31:36 +00:00
<div className="flex flex-1 items-center gap-4">
2025-11-13 08:38:25 +00:00
{/* 头像 */}
2025-11-24 03:47:20 +00:00
<Link href={`/@${contact.aiId}`} prefetch>
2025-11-13 08:38:25 +00:00
<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>
{/* 用户详细信息 */}
2025-11-28 06:31:36 +00:00
<div className="flex flex-1 flex-col gap-2">
2025-11-13 08:38:25 +00:00
{/* 名字和标签 */}
<div className="flex items-center gap-2">
2025-11-24 03:47:20 +00:00
<Link href={`/@${contact.aiId}`} prefetch>
2025-11-28 06:31:36 +00:00
<h3 className="txt-title-m text-white">{contact.nickname || contact.roleName}</h3>
2025-11-13 08:38:25 +00:00
</Link>
2025-11-28 06:31:36 +00:00
{contact.heartbeatLevel && contact.isShow && (
<AIRelationTag heartbeatLevel={contact.heartbeatLevel} size="small" />
)}
2025-11-13 08:38:25 +00:00
</div>
{/* 心动值和用户信息 */}
<div className="flex items-center gap-2">
{/* 心动值 */}
<div className="flex items-center gap-1">
2025-11-28 06:31:36 +00:00
<Image
src="/icons/heart.svg"
2025-11-13 08:38:25 +00:00
alt="Heart"
2025-11-24 03:47:20 +00:00
width={12}
height={12}
2025-11-28 06:31:36 +00:00
className="h-3 w-3"
2025-11-13 08:38:25 +00:00
/>
2025-11-28 06:31:36 +00:00
<span className="txt-numMonotype-s">{contact.heartbeatVal || 0}°</span>
2025-11-13 08:38:25 +00:00
</div>
2025-11-28 06:31:36 +00:00
2025-11-13 08:38:25 +00:00
{/* 分隔线 */}
2025-11-28 06:31:36 +00:00
<div className="border-outline-normal h-3 w-0 border-l" />
2025-11-13 08:38:25 +00:00
{/* 用户详细信息 */}
2025-11-28 06:31:36 +00:00
<span className="txt-label-m flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-white">
{[age && `${age}`, contact.characterName, contact.tagName]
.filter(Boolean)
.join(' · ')}
2025-11-13 08:38:25 +00:00
</span>
</div>
</div>
</div>
{/* 聊天按钮 */}
<Button className="min-w-[80px]" onClick={handleChatClick}>
<i className="iconfont icon-Chat !text-[24px]" />
</Button>
</div>
)
}
const ContactsPage = () => {
// 使用无限查询获取心动关系列表
2025-11-28 06:31:36 +00:00
const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage, error } =
useHeartbeatRelationListInfinite()
2025-11-13 08:38:25 +00:00
// 扁平化所有页面的数据
const allContacts = useMemo(() => {
2025-11-28 06:31:36 +00:00
return data?.pages.flatMap((page) => page.datas || []) || []
}, [data])
2025-11-24 03:47:20 +00:00
const chatRoutes = useMemo(
2025-11-28 06:31:36 +00:00
() =>
allContacts.slice(0, 20).map((contact) => (contact?.aiId ? `/chat/${contact.aiId}` : null)),
2025-11-24 03:47:20 +00:00
[allContacts]
)
usePrefetchRoutes(chatRoutes)
2025-11-13 08:38:25 +00:00
// 加载状态骨架屏组件
const ContactSkeleton = () => (
2025-11-28 06:31:36 +00:00
<div className="flex w-full animate-pulse items-center justify-between gap-4">
<div className="flex flex-1 items-center gap-4">
<div className="bg-surface-nest-normal size-16 rounded-full" />
<div className="flex flex-1 flex-col gap-2">
2025-11-13 08:38:25 +00:00
<div className="flex items-start gap-2">
2025-11-28 06:31:36 +00:00
<div className="bg-surface-nest-normal h-6 w-24 rounded" />
<div className="bg-surface-nest-normal h-5 w-16 rounded" />
2025-11-13 08:38:25 +00:00
</div>
2025-11-28 06:31:36 +00:00
<div className="bg-surface-nest-normal h-4 w-48 rounded" />
2025-11-13 08:38:25 +00:00
</div>
</div>
2025-11-28 06:31:36 +00:00
<div className="bg-surface-nest-normal h-10 w-[80px] rounded" />
2025-11-13 08:38:25 +00:00
</div>
2025-11-28 06:31:36 +00:00
)
2025-11-13 08:38:25 +00:00
// 空状态组件
const EmptyState = () => (
<div className="flex flex-col items-center justify-center py-16 text-center">
2025-11-28 06:31:36 +00:00
<Image
src="/icons/empty.svg"
alt="Empty"
2025-11-24 03:47:20 +00:00
width={64}
height={64}
2025-11-28 06:31:36 +00:00
className="mb-4 h-16 w-16 opacity-50"
2025-11-13 08:38:25 +00:00
/>
2025-11-28 06:31:36 +00:00
<h3 className="txt-title-m text-txt-secondary-normal mb-2">No crushes found</h3>
2025-11-13 08:38:25 +00:00
<p className="txt-body-m text-txt-tertiary-normal">
Start chatting with AI characters to build your crushes
</p>
</div>
2025-11-28 06:31:36 +00:00
)
2025-11-13 08:38:25 +00:00
return (
2025-11-28 06:31:36 +00:00
<div className="mx-auto flex w-[752px] flex-col gap-6 pt-28 pb-[200px]">
2025-11-13 08:38:25 +00:00
{/* 页面标题 */}
2025-11-28 06:31:36 +00:00
<h1 className="txt-title-l">My Crushes</h1>
2025-11-13 08:38:25 +00:00
{/* 心动值统计信息栏 */}
<RenderContactStatusText hasContacts={allContacts.length > 0} />
{/* 联系人列表 */}
<InfiniteScrollList<ContactItem>
items={allContacts}
2025-11-28 06:31:36 +00:00
renderItem={(contact) => <ContactCard contact={contact} />}
2025-11-13 08:38:25 +00:00
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>
)
}
2025-11-24 03:47:20 +00:00
export default ContactsPage