feat(character): 详情页面适配移动端

This commit is contained in:
liuyonghe0111 2025-12-19 11:28:16 +08:00
parent 023398cbea
commit abdf1f4615
4 changed files with 65 additions and 15 deletions

View File

@ -1,10 +1,14 @@
'use client'; 'use client';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type React from 'react';
import { useAsyncFn } from '@/hooks/tools'; import { useAsyncFn } from '@/hooks/tools';
import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat'; import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat';
export default function ChatButton({ id }: { id: string }) { export default function ChatButton({
id,
...props
}: { id: string } & React.ComponentProps<'button'>) {
const router = useRouter(); const router = useRouter();
const createChannel = useStreamChatStore((s) => s.createChannel); const createChannel = useStreamChatStore((s) => s.createChannel);
@ -15,7 +19,7 @@ export default function ChatButton({ id }: { id: string }) {
}); });
return ( return (
<Button loading={loading} onClick={() => createChannelAndPush()} variant="primary"> <Button loading={loading} onClick={() => createChannelAndPush()} variant="primary" {...props}>
Chat Chat
</Button> </Button>
); );

View File

@ -2,23 +2,33 @@ import ChatButton from './ChatButton';
import { fetchCharacter } from './service'; import { fetchCharacter } from './service';
import { Chip } from '@/components/ui/chip'; import { Chip } from '@/components/ui/chip';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { IconButton } from '@/components/ui/button';
import Link from 'next/link';
export default async function Page({ params }: { params: Promise<{ id: string }> }) { export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; const { id } = await params;
const character = await fetchCharacter(id); const character = await fetchCharacter(id);
return ( return (
<div className="flex px-4 pt-10"> <div className="flex h-full flex-col items-center mx-auto max-w-[752px] px-4 pt-4 sm:pt-10">
<div className="mx-auto w-full max-w-[752px]"> {/* header */}
<div className="w-full flex mb-4 items-center justify-between">
<Link href="/home">
<IconButton variant="ghost" size="large" iconfont="icon-arrow-left" />
</Link>
<IconButton variant="tertiary" size="large" iconfont="icon-Like" />
</div>
{/* 内容区 */}
<div className="mx-auto flex-1 w-full">
<header className="flex items-end gap-10 justify-between"> <header className="flex items-end gap-10 justify-between">
<div className="flex gap-6 items-end"> <div className="flex flex-1 sm:flex-row flex-col gap-6 items-center sm:items-end">
<Avatar className="size-32"> <Avatar className="size-32">
<AvatarImage src={character?.headPortrait} /> <AvatarImage src={character?.headPortrait} />
<AvatarFallback>{character?.name?.slice(0, 2)}</AvatarFallback> <AvatarFallback>{character?.name?.slice(0, 2)}</AvatarFallback>
</Avatar> </Avatar>
<div> <div className="text-center sm:text-left">
<div className="txt-headline-s">{character?.name}</div> <div className="txt-headline-s">{character?.name}</div>
<div className="flex flex-wrap mt-4 gap-2"> <div className="flex flex-wrap mt-4 gap-2 justify-center sm:justify-start">
{character?.tags?.map((tag: any) => ( {character?.tags?.map((tag: any) => (
<Chip className="rounded-xs" key={tag.tagId}> <Chip className="rounded-xs" key={tag.tagId}>
{tag.name} {tag.name}
@ -27,15 +37,39 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
</div> </div>
</div> </div>
</div> </div>
<div> <div className="sm:block hidden">
<ChatButton id={id}></ChatButton> <ChatButton id={id}></ChatButton>
</div> </div>
</header> </header>
<div className="mt-12 rounded-2xl bg-white/10 p-6"> <div className="w-full flex bg-white/10 rounded-m py-3 mt-12">
<div className="w-full flex items-center flex-col">
<div className="text-white">
<IconButton variant="ghost" size="small" iconfont="icon-Like-fill" />
9.9k
</div>
<div className="text-txt-tertiary">Liked</div>
</div>
<div className="w-full flex items-center flex-col">
<div className="text-[rgba(255,64,180,1)]">
<IconButton
className="text-[rgba(255,64,180,1)]"
variant="ghost"
size="small"
iconfont="icon-icon_im_other"
/>
123456
</div>
<div className="text-txt-tertiary">Hot</div>
</div>
</div>
<div className="mt-4 rounded-2xl bg-white/10 p-6">
<div className="txt-headline-s">Introduction</div> <div className="txt-headline-s">Introduction</div>
<div className="mt-4">{character?.description}</div> <div className="mt-4">{character?.description}</div>
</div> </div>
</div> </div>
<div className="sm:hidden w-full mb-4">
<ChatButton className="w-full" id={id}></ChatButton>
</div>
</div> </div>
); );
} }

View File

@ -97,7 +97,7 @@ export default function Input() {
variant="ghost" variant="ghost"
size="small" size="small"
iconfont="icon-voice_msg" iconfont="icon-voice_msg"
className="flex-shrink-0" className="flex-shrink-0 hover:bg-surface-element-hover"
/> />
<AuthHeightTextarea <AuthHeightTextarea
placeholder="Chat" placeholder="Chat"
@ -112,7 +112,7 @@ export default function Input() {
variant="ghost" variant="ghost"
size="small" size="small"
onClick={() => null} onClick={() => null}
className={cn('bg-surface-element-hover flex-shrink-0')} className={cn('hover:bg-surface-element-hover flex-shrink-0')}
iconfont="icon-prompt" iconfont="icon-prompt"
/> />
</div> </div>

View File

@ -11,7 +11,19 @@ import { useMedia } from '@/hooks/tools';
import Notice from './components/Notice'; import Notice from './components/Notice';
import { items } from './BottomBar'; import { items } from './BottomBar';
const mobileHidenMenus = ['/profile/edit', '/profile/account']; const mobileHidenMenus = ['/profile/edit', '/profile/account', '/character/:id'];
// 将路由模式转换为正则表达式进行匹配
function matchRoutePattern(pathname: string, patterns: string[]): boolean {
return patterns.some((pattern) => {
// 将路由模式中的 :param 转换为正则表达式
const regexPattern = pattern
.replace(/:[^/]+/g, '[^/]+') // 将 :id, :userId 等替换为 [^/]+
.replace(/\//g, '\\/'); // 转义斜杠
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(pathname);
});
}
function Topbar() { function Topbar() {
const [isBlur, setIsBlur] = useState(false); const [isBlur, setIsBlur] = useState(false);
@ -22,7 +34,7 @@ function Topbar() {
const response = useMedia(); const response = useMedia();
const searchParamsString = searchParams.toString(); const searchParamsString = searchParams.toString();
const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`; const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`;
// const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`; const loginHref = `/login?redirect=${encodeURIComponent(redirectURL)}`;
useEffect(() => { useEffect(() => {
function handleScroll(event: Event) { function handleScroll(event: Event) {
@ -73,7 +85,7 @@ function Topbar() {
const rightDomRender = () => { const rightDomRender = () => {
if (!user) if (!user)
return ( return (
<Link href="/login" prefetch> <Link href={loginHref} prefetch>
<Button size="small">Login in / Sign up</Button> <Button size="small">Login in / Sign up</Button>
</Link> </Link>
); );
@ -96,7 +108,7 @@ function Topbar() {
); );
}; };
if (response && !response.sm && mobileHidenMenus.some((item) => item === pathname)) return null; if (response && !response.sm && matchRoutePattern(pathname, mobileHidenMenus)) return null;
return ( return (
<header <header