Files
jiang13-bbs/frontend/app/u/[id]/page.tsx
freefire e2e5ba077c feat: 添加用户个人资料编辑功能,优化页面布局与导航
1. 后端新增用户资料更新接口,支持修改昵称、头像、邮箱和签名
2. 前端新增资料编辑表单页面,适配响应式布局
3. 优化帖子详情页布局与移动端适配逻辑
4. 重构首页侧边栏导航,使用卡片式板块展示
5. 移除全局板块缓存,调整Header导航移除板块菜单
6. 补充用户签名展示功能,完善用户信息返回字段
2026-09-14 16:10:38 +08:00

149 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { Metadata } from "next";
import { ChevronRight, FileText, MessageCircle, Flame, AlertCircle } from "lucide-react";
import { fetchUserProfile, fetchUserComments, type UserProfile, type UserCommentsResponse } from "@/lib/api";
import UserProfileContent from "./UserProfileContent";
import Avatar from "@/components/Avatar";
interface UserPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ page?: string; tab?: string }>;
}
export async function generateMetadata({ params }: UserPageProps): Promise<Metadata> {
const { id } = await params;
try {
const data = await fetchUserProfile(id, 1);
return {
title: data.user.nickname || data.user.username,
description: `查看 ${data.user.nickname} 的资料、发帖与评论`,
};
} catch {
return { title: "用户不存在" };
}
}
export default async function UserPage({ params, searchParams }: UserPageProps) {
const { id } = await params;
const sp = await searchParams;
const page = Math.max(1, parseInt(sp.page || "1", 10) || 1);
const tab: "posts" | "comments" = sp.tab === "comments" ? "comments" : "posts";
let data: UserProfile;
try {
data = await fetchUserProfile(id, page);
} catch {
return (
<div className="max-w-md mx-auto text-center py-24">
<p className="text-lg font-extrabold mb-1.5" style={{ color: "var(--ink)" }}>
用户不存在
</p>
<p className="meta mb-5">该用户可能已注销或链接有误</p>
<Link href="/" className="btn btn-line btn-sm">
返回首页
</Link>
</div>
);
}
// 评论 tab:SSR 直出对应分页,URL(?tab=comments&page=N)与内容严格一致
let commentData: UserCommentsResponse | null = null;
let commentError = false;
if (tab === "comments") {
try {
commentData = await fetchUserComments(id, page);
} catch {
commentError = true;
}
}
const u = data.user;
const stats = [
{ label: "发帖", value: data.stats.post_count, icon: <FileText size={15} /> },
{ label: "评论", value: data.stats.comment_count, icon: <MessageCircle size={15} /> },
];
const showPoints = data.stats.points > 0;
return (
<div className="max-w-4xl mx-auto">
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--accent)" }}>{u.nickname || u.username}</span>
</nav>
{/* 用户信息卡片 */}
<section className="panel relative overflow-hidden p-6 sm:p-8 mb-6">
<span
aria-hidden
className="absolute -top-20 -right-16 w-60 h-60 rounded-full"
style={{ background: "var(--accent-soft)" }}
/>
<div className="relative flex items-start gap-5 sm:gap-6">
<Avatar
name={u.nickname || u.username}
src={u.avatar || undefined}
size={88}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2.5 flex-wrap">
<h1 className="text-[24px] sm:text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
{u.nickname || u.username}
</h1>
{u.role === "admin" && (
<span
className="text-[11.5px] font-bold px-2.5 py-1 rounded-full"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
>
管理员
</span>
)}
</div>
<p className="meta mt-1.5 text-[13px]">
@{u.username} · {new Date(u.created_at).toLocaleDateString("zh-CN")} 加入
</p>
{u.signature && (
<p className="mt-3 text-[14px] leading-relaxed" style={{ color: "var(--ink-2)" }}>
{u.signature}
</p>
)}
<div className="flex gap-2.5 mt-4 flex-wrap">
{stats.map((s) => (
<span
key={s.label}
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-full text-[13px] font-semibold"
style={{ background: "var(--panel-2)", color: "var(--ink-2)" }}
>
<span style={{ color: "var(--accent)" }}>{s.icon}</span>
<span className="tabular-nums">{s.value}</span> {s.label}
</span>
))}
{showPoints && (
<span
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-full text-[13px] font-semibold"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
title={`连续签到 ${data.stats.streak} 天`}
>
<Flame size={15} />
<span className="tabular-nums">{data.stats.points}</span> 积分
{data.stats.streak > 0 && <span className="font-normal opacity-80">· 连签 {data.stats.streak} 天</span>}
</span>
)}
</div>
</div>
</div>
</section>
{/* 发帖 / 评论 切换(URL 驱动,SSR 直出) */}
<UserProfileContent
userId={id}
profile={data}
tab={tab}
commentData={commentData}
commentError={commentError}
page={page}
/>
</div>
);
}