import Link from "next/link"; import { cookies, headers } from "next/headers"; import { Metadata } from "next"; import { ChevronRight, FileText, MessageCircle, Flame, Calendar, Hash, Shield, Settings, KeyRound, Info, PenLine, } from "lucide-react"; import { fetchUserProfile, fetchUserComments, fetchLoginDevices, type UserProfile, type UserCommentsResponse, type LoginDevice, } from "@/lib/api"; import { authCookieHeader, sessionCookieHeader } from "@/lib/cookies"; import { getMeCached, getPublicSettingsCached } from "@/lib/serverData"; import UserProfileContent from "./UserProfileContent"; import ProfileForm from "./ProfileForm"; import PasswordForm from "./PasswordForm"; import LoginDevices from "./LoginDevices"; import ProfileHeroAvatar from "./ProfileHeroAvatar"; import RoleBadge from "@/components/RoleBadge"; import { roleMeta } from "@/lib/roles"; import StartDMButton from "./StartDMButton"; import StaffUserMenu from "@/components/StaffUserMenu"; import { requestCanonical } from "@/lib/canonicalMeta"; import { absoluteCanonical, canonicalPage } from "@/lib/siteUrl"; import { profileJsonLd } from "@/lib/jsonLd"; import JsonLd from "@/components/JsonLd"; interface UserPageProps { params: Promise<{ id: string }>; searchParams: Promise<{ page?: string; tab?: string }>; } export type UserTab = "posts" | "comments" | "settings" | "security"; export async function generateMetadata({ params, searchParams }: UserPageProps): Promise { const { id } = await params; const sp = await searchParams; const rawTab = sp.tab; const tab: UserTab = rawTab === "comments" ? "comments" : rawTab === "settings" ? "settings" : rawTab === "security" ? "security" : "posts"; try { const data = await fetchUserProfile(id, 1); const allowComments = (await getPublicSettingsCached()).allow_comments !== false; const title = data.user.nickname || data.user.username; const description = allowComments ? `查看 ${data.user.nickname} 的资料、发帖与评论` : `查看 ${data.user.nickname} 的资料与发帖`; // 资料/安全 tab 仅本人可见,不声明 Canonical,避免被当成公开资料页或继承首页 if (tab === "settings" || tab === "security") { return { title, description }; } const query: Record = {}; if (allowComments && tab === "comments") query.tab = "comments"; const page = canonicalPage(sp.page); if (page) query.page = page; return { title, description, ...(await requestCanonical(`/u/${data.user.id}`, query)), }; } catch { return { title: "用户不存在" }; } } // 装饰几何色:全部由主题变量派生,浅色为淡靛、暗色自动提亮 const ACCENT_RING_1 = "color-mix(in srgb, var(--accent) 14%, transparent)"; const ACCENT_RING_2 = "color-mix(in srgb, var(--accent) 9%, transparent)"; 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 rawTab = sp.tab; const tab: UserTab = rawTab === "comments" ? "comments" : rawTab === "settings" ? "settings" : rawTab === "security" ? "security" : "posts"; const cookie = authCookieHeader(await cookies()); const me = await getMeCached(cookie || undefined); const settings = await getPublicSettingsCached(); const allowComments = settings.allow_comments !== false; const allowMessages = settings.allow_messages !== false; let data: UserProfile; try { // 带 cookie:超管/站长才能拿到 banned 字段 data = await fetchUserProfile(id, page, cookie || undefined); } catch { return (

用户不存在

该用户可能已注销或链接有误

返回首页
); } // 判断是否本人(决定能否看到 settings / security tab) const isOwner = me.user?.id === data.user.id; const currentUser = me.user; let effectiveTab: UserTab = (tab === "settings" || tab === "security") && !isOwner ? "posts" : tab; if (!allowComments && effectiveTab === "comments") effectiveTab = "posts"; // 评论 tab:SSR 直出对应分页 let commentData: UserCommentsResponse | null = null; let commentError = false; if (allowComments && effectiveTab === "comments") { try { commentData = await fetchUserComments(id, page); } catch { commentError = true; } } // 安全设置:仍有效的登录会话(仅本人) // SSR 直连后端时必须转发 refresh cookie + 浏览器 UA/IP,才能标「当前在线」并避免把 Node fetch 当成一台设备 let loginDevices: LoginDevice[] = []; if (effectiveTab === "security" && isOwner) { try { const hdrs = await headers(); const sessionCookie = sessionCookieHeader(await cookies()); loginDevices = await fetchLoginDevices(sessionCookie || cookie || undefined, { userAgent: hdrs.get("user-agent") || undefined, clientIP: hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() || hdrs.get("x-real-ip") || undefined, }); } catch { loginDevices = []; } } const u = data.user; const joinDate = new Date(u.created_at).toLocaleDateString("zh-CN"); const showPoints = data.stats.points > 0 || isOwner; const hasHeroActions = isOwner || !!currentUser; // 导航项(桌面左栏 / 移动 chips 共用定义) const navItems: { key: UserTab; label: string; count?: number; icon: React.ReactNode; ownerOnly?: boolean }[] = [ { key: "posts", label: "发帖", count: data.stats.post_count, icon: }, ...(allowComments ? [{ key: "comments" as const, label: "评论", count: data.stats.comment_count, icon: }] : []), { key: "settings", label: "资料设置", icon: , ownerOnly: true }, { key: "security", label: "安全设置", icon: , ownerOnly: true }, ]; const visibleNav = navItems.filter((t) => !t.ownerOnly || isOwner); const tabHref = (t: UserTab) => (t === "posts" ? `/u/${id}` : `/u/${id}?tab=${t}`); const profileQuery: Record = {}; if (allowComments && effectiveTab === "comments") profileQuery.tab = "comments"; if (page > 1) profileQuery.page = page; const profileLd = effectiveTab === "settings" || effectiveTab === "security" ? null : profileJsonLd({ id: u.id, name: u.nickname || u.username, username: u.username, signature: (u.signature || "").trim() || undefined, pageUrl: absoluteCanonical(`/u/${u.id}`, profileQuery), }); /** 本页卡片统一阴影;暗色随 color-mix 自动加深 */ const cardShadow = "0 1px 3px color-mix(in srgb, #000 18%, transparent)"; return (
{/* ═══ Hero:头像 + 昵称 + 简介 + 行内统计,约 120px 高 ═══ */}
{/* 装饰层单独裁圆角,避免 overflow-hidden 裁掉管理下拉菜单 */}
{/* 本人操作按钮 / 访客私聊:绝对定位在卡片右上角 */} {isOwner ? (
编辑资料
) : currentUser ? (
{allowMessages ? : null}
) : null} {/* 身份 + 行内统计:垂直居中,高度约 120px */}

{u.nickname || u.username}

{u.role !== "user" && ( )} {u.banned === true && ( 已封禁 )}

{data.stats.post_count} {" "}帖子 {allowComments && ( <> · {data.stats.comment_count} {" "}评论 )} · {data.stats.points} {" "}积分 {data.stats.streak > 0 && ( <> · {data.stats.streak}天连签 )}

{u.signature && (

{u.signature}

)}
{/* 移动端 Tab chips(md 以下,约 768px) */}
{visibleNav.map((t) => ( {t.icon} {t.label} {t.count !== undefined && ( {t.count} )} ))}
{/* ═══ 三栏:左 240 / 中自适应 / 右 300;md 以下侧栏折叠 ═══ */}
{/* 左栏:Tab 导航 */} {/* 中栏:Tab 内容 */}
{effectiveTab === "posts" || effectiveTab === "comments" ? ( ) : effectiveTab === "settings" && isOwner && currentUser ? (

编辑个人资料

昵称、头像和签名会公开展示,邮箱仅你自己可见

) : effectiveTab === "security" && isOwner ? (

修改密码

修改成功后将在所有设备登出,需要使用新密码重新登录

) : null}
{/* 右栏:账号信息 + 积分 + 安全提示 */}
); }