import Link from "next/link"; import { cookies, headers } from "next/headers"; import { Metadata } from "next"; import { FileText, MessageCircle, Flame, Calendar, Hash, Shield, Settings, KeyRound, Info, PenLine, Bookmark, UserPlus, Mail, } from "lucide-react"; import { fetchUserProfile, fetchUserComments, fetchLoginDevices, fetchUserFavorites, fetchUserFollowing, type UserProfile, type UserCommentsResponse, type LoginDevice, type UserFavoritesResponse, type UserFollowingResponse, } from "@/lib/api"; import { authCookieHeader, sessionCookieHeader } from "@/lib/cookies"; import { getMeCached, getPublicSettingsCached, getBoardsCached } 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 LevelBadge from "@/components/LevelBadge"; import BadgeChip from "@/components/BadgeChip"; import { boardColor } from "@/components/BoardIcon"; import { roleMeta } from "@/lib/roles"; import StartDMButton from "./StartDMButton"; import FollowButton from "@/components/FollowButton"; import StaffUserMenu from "@/components/StaffUserMenu"; import { requestCanonical } from "@/lib/canonicalMeta"; import { absoluteCanonical, canonicalPage } from "@/lib/siteUrl"; import { userPath } from "@/lib/urlStyle"; import { profileJsonLd } from "@/lib/jsonLd"; import JsonLd from "@/components/JsonLd"; interface UserPageProps { params: Promise<{ id: string }>; searchParams: Promise<{ page?: string; tab?: string; sort?: string }>; } export type UserTab = "posts" | "comments" | "favorites" | "following" | "settings" | "security"; function parseTab(rawTab?: string): UserTab { return rawTab === "comments" ? "comments" : rawTab === "favorites" ? "favorites" : rawTab === "following" ? "following" : rawTab === "settings" ? "settings" : rawTab === "security" ? "security" : "posts"; } export async function generateMetadata({ params, searchParams }: UserPageProps): Promise { const { id } = await params; const sp = await searchParams; const tab = parseTab(sp.tab); try { const data = await fetchUserProfile(id, 1); const pub = await getPublicSettingsCached(); const allowComments = pub.allow_comments !== false; const urlStyle = pub.url_style; 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"; if (tab === "favorites") query.tab = "favorites"; if (tab === "following") query.tab = "following"; if (tab === "posts" && (sp.sort === "hot" || sp.sort === "essence")) query.sort = sp.sort; const page = canonicalPage(sp.page); if (page) query.page = page; return { title, description, ...(await requestCanonical(userPath(urlStyle, 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 tab = parseTab(sp.tab); const sort = sp.sort === "hot" || sp.sort === "essence" ? sp.sort : "new"; const cookie = authCookieHeader(await cookies()); const me = await getMeCached(cookie || undefined); const settings = await getPublicSettingsCached(); const allowComments = settings.allow_comments !== false; const urlStyle = settings.url_style; const commentsRequireLogin = settings.comments_require_login === true; const allowMessages = settings.allow_messages !== false; const canReadComments = allowComments && (!commentsRequireLogin || !!me.user); let data: UserProfile; try { // 带 cookie:超管/站长才能拿到 banned 字段 data = await fetchUserProfile(id, page, cookie || undefined, sort); } catch { return (

用户不存在

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

返回首页
); } // 板块 id → 名称/配色:板块管理员徽章展示「{板块名}板块管理员」并对齐板块色 const boardNames: Record = {}; const boardColors: Record = {}; try { for (const b of (await getBoardsCached()).boards) { boardNames[b.id] = b.name; boardColors[b.id] = boardColor(b); } } catch { /* 板块列表失败时回落通用角色徽章 */ } // 判断是否本人(决定能否看到 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 (!canReadComments && effectiveTab === "comments") effectiveTab = "posts"; // 评论 tab:SSR 直出对应分页 let commentData: UserCommentsResponse | null = null; let commentError = false; if (canReadComments && effectiveTab === "comments") { try { commentData = await fetchUserComments(id, page, cookie || undefined); } catch { commentError = true; } } // 收藏 tab:SSR 直出对应分页(公开可见) let favoriteData: UserFavoritesResponse | null = null; let favoriteError = false; if (effectiveTab === "favorites") { try { favoriteData = await fetchUserFavorites(id, page); } catch { favoriteError = true; } } // 关注 tab:SSR 直出对应分页(公开可见) let followingData: UserFollowingResponse | null = null; let followingError = false; if (effectiveTab === "following") { try { followingData = await fetchUserFollowing(id, page); } catch { followingError = 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 mainNavItems: { key: UserTab; label: string; count?: number; icon: React.ReactNode }[] = [ { key: "posts", label: "发帖", count: data.stats.post_count, icon: }, ...(canReadComments ? [{ key: "comments" as const, label: "回帖", count: data.stats.comment_count, icon: }] : []), { key: "favorites", label: "收藏", count: data.stats.favorite_count, icon: }, { key: "following", label: "关注", count: data.stats.following_count, icon: }, ]; const settingsNavItems: { key: UserTab; label: string; count?: number; icon: React.ReactNode }[] = isOwner ? [ { key: "settings", label: "资料设置", icon: }, { key: "security", label: "安全设置", icon: }, ] : []; const visibleNav = [...mainNavItems, ...settingsNavItems]; const tabHref = (t: UserTab) => t === "posts" ? userPath(urlStyle, Number(id)) : `${userPath(urlStyle, Number(id))}?tab=${t}`; const profileQuery: Record = {}; if (canReadComments && effectiveTab === "comments") profileQuery.tab = "comments"; if (effectiveTab === "favorites") profileQuery.tab = "favorites"; if (effectiveTab === "following") profileQuery.tab = "following"; if (effectiveTab === "posts" && sort !== "new") profileQuery.sort = sort; 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(userPath(urlStyle, u.id), profileQuery), urlStyle, }); /** 本页卡片统一阴影;暗色随 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 && ( 已封禁 )}
{/* 徽章墙:管理员颁布的全部徽章,悬浮显示授予时间 */} {(u.badges ?? []).length > 0 && (
{(u.badges ?? []).map((ub) => ( ))}
)}

{data.stats.post_count} {" "}帖子 {canReadComments && ( <> · {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 === "favorites" || effectiveTab === "following" ? ( ) : effectiveTab === "settings" && isOwner && currentUser ? (

编辑个人资料

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

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

修改密码

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

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