Files
jiang13-bbs/frontend/app/board/[id]/page.tsx
freefire 42c395a864 feat: 实时消息与管理端运营能力
新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 22:58:16 +08:00

157 lines
6.5 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 type { Metadata } from "next";
import { cookies } from "next/headers";
import { PenSquare, MessagesSquare, ChevronRight } from "lucide-react";
import { fetchPosts, fetchBoards } from "@/lib/api";
import { authCookieHeader } from "@/lib/cookies";
import { getMeCached } from "@/lib/serverData";
import Pagination from "@/components/Pagination";
import PostRow from "@/components/PostRow";
import PinnedPosts from "@/components/PinnedPosts";
import FeedNewTip from "@/components/home/FeedNewTip";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ page?: string }>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = await params;
const { boards } = await fetchBoards();
const board = boards.find((b) => b.id === Number(id));
return {
title: board ? `${board.name} · 姜十三论坛` : "板块 · 姜十三论坛",
description: board?.description || "板块帖子列表",
};
}
export default async function BoardPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { page: pageStr } = await searchParams;
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
const cookie = authCookieHeader(await cookies());
const [postsData, boardsData, me] = await Promise.all([
fetchPosts(page, 20, Number(id), "latest", "", false, cookie),
fetchBoards(),
getMeCached(cookie || undefined),
]);
const boardIndex = boardsData.boards.findIndex((b) => b.id === Number(id));
const board = boardIndex >= 0 ? boardsData.boards[boardIndex] : null;
const user = me.user;
// 第一页:置顶帖独立成区,列表只保留非置顶
const pinned = page === 1 ? postsData.posts.filter((p) => p.pinned > 0) : [];
const rows = page === 1 ? postsData.posts.filter((p) => p.pinned <= 0) : postsData.posts;
return (
<div>
{/* 面包屑 */}
<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)" }}>{board?.name || "未知板块"}</span>
</nav>
<div className="panel p-6 sm:p-7 mb-6 relative overflow-hidden">
<span
aria-hidden
className="absolute -top-16 -right-12 w-48 h-48 rounded-full"
style={{ background: "var(--accent-soft)" }}
/>
<div className="relative flex items-end justify-between gap-4 flex-wrap">
<div className="flex items-center gap-4">
<span
className={`av-${Math.max(0, boardIndex) % 8} w-14 h-14 sm:w-16 sm:h-16 rounded-2xl flex items-center justify-center text-2xl font-extrabold shrink-0`}
>
{board?.name.slice(0, 1) || "?"}
</span>
<div>
<h1 className="text-2xl sm:text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
{board?.name || "未知板块"}
</h1>
{board?.description && (
<p className="mt-1 text-sm" style={{ color: "var(--ink-2)" }}>
{board.description}
</p>
)}
<p className="meta mt-1.5">{postsData.total} 篇讨论</p>
</div>
</div>
<Link href="/compose" className="btn btn-primary">
<PenSquare size={15} /> 在此板块发帖
</Link>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_290px] gap-6">
<div>
<PinnedPosts posts={pinned} showBoard={false} />
{rows.length > 0 ? (
<div className="panel overflow-hidden">
<div className="px-4 sm:px-5 py-3.5 flex items-center justify-between" style={{ borderBottom: "1px solid var(--line)" }}>
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}>全部讨论</span>
<span className="meta text-xs">第 {page} 页</span>
</div>
{user && (
<div className="pt-2.5">
<FeedNewTip
sort="latest"
boardId={Number(id)}
page={page}
userId={user.id}
/>
</div>
)}
{rows.map((post, i) => (
<PostRow key={post.id} post={post} index={i} />
))}
</div>
) : (
pinned.length === 0 && (
<div className="panel text-center py-20 px-6">
<span
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<MessagesSquare size={26} />
</span>
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>该板块还没有帖子</p>
<p className="meta mt-1.5">来发第一篇,抢占沙发</p>
<Link href="/compose" className="btn btn-primary mt-6">
<PenSquare size={15} /> 写新帖
</Link>
</div>
)
)}
<Pagination total={postsData.total} page={page} size={20} basePath={`/board/${id}`} />
</div>
<aside>
<div className="panel p-5">
<p className="font-bold text-[15px] mb-3.5" style={{ color: "var(--ink)" }}>其他板块</p>
<div className="space-y-1">
{boardsData.boards
.map((b, i) => ({ b, i }))
.filter(({ b }) => b.id !== Number(id))
.map(({ b, i }) => (
<Link
key={b.id}
href={`/board/${b.id}`}
className="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded-xl transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
>
<span className={`av-${i % 8} w-8 h-8 rounded-xl flex items-center justify-center text-[13px] font-bold shrink-0`}>
{b.name.slice(0, 1)}
</span>
<span className="text-sm font-medium flex-1 truncate group-hover:text-[var(--accent)] transition-colors" style={{ color: "var(--ink)" }}>
{b.name}
</span>
</Link>
))}
</div>
</div>
</aside>
</div>
</div>
);
}