新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。 Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
6.5 KiB
TypeScript
157 lines
6.5 KiB
TypeScript
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>
|
||
);
|
||
}
|