Files
jiang13-bbs/frontend/app/page.tsx
freefire 833bcd33a1 chore: 去掉未上线前的旧兼容垫片,板块流收口到首页
鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:04:47 +08:00

268 lines
8.9 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 type { Metadata } from "next";
import { cookies } from "next/headers";
import Link from "next/link";
import { PenSquare, SearchX, MessagesSquare } from "lucide-react";
import {
fetchPosts,
fetchOverview,
fetchBoardSidebar,
type OverviewResponse,
type BoardSidebarResponse,
} from "@/lib/api";
import { authCookieHeader } from "@/lib/cookies";
import { getMeCached } from "@/lib/serverData";
import Pagination from "@/components/Pagination";
import PostRow from "@/components/PostRow";
import FeedTabs, { type FeedSort } from "@/components/home/FeedTabs";
import FeedNewTip from "@/components/home/FeedNewTip";
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
import {
BoardPanels,
BoardPanelsMobile,
BoardMissingPanel,
} from "@/components/home/BoardPanels";
export const metadata: Metadata = {
title: "姜十三论坛 · 技术讨论社区",
description: "姜十三论坛,分享技术、讨论问题、交流心得的社区",
};
interface HomePageProps {
searchParams: Promise<{ q?: string; page?: string; sort?: string; board?: string }>;
}
function normalizeSort(v?: string): FeedSort {
if (v === "new" || v === "recommended") return v;
return "latest";
}
// 空结果面板(无帖 / 无搜索结果)
function EmptyState({ keyword }: { keyword: string }) {
return (
<div className="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)" }}
>
{keyword ? <SearchX size={26} /> : <MessagesSquare size={26} />}
</span>
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>
{keyword ? "没有找到相关帖子" : "还没有帖子"}
</p>
<p className="meta mt-1.5">
{keyword ? `换个关键词试试,或者发起“${keyword}”相关的话题` : "成为第一个发起讨论的人吧"}
</p>
{keyword ? (
<div className="flex items-center justify-center gap-3 mt-6">
<Link href="/" className="btn btn-line">
清除搜索
</Link>
<Link href="/compose" className="btn btn-primary">
<PenSquare size={15} /> 发起这个话题
</Link>
</div>
) : (
<Link href="/compose" className="btn btn-primary mt-6">
<PenSquare size={15} /> 写新帖
</Link>
)}
</div>
);
}
export default async function HomePage({ searchParams }: HomePageProps) {
const sp = await searchParams;
const keyword = sp.q?.trim() || "";
const page = Math.max(1, parseInt(sp.page || "1", 10) || 1);
const sort = normalizeSort(sp.sort);
const boardId = Math.max(0, parseInt(sp.board || "0", 10) || 0);
const isSearch = keyword.length > 0;
const cookie = authCookieHeader(await cookies());
// 与 layout 共享同一份 /me 请求(React cache 去重)
const mePromise = getMeCached(cookie || undefined);
const postsPromise = fetchPosts(
page,
20,
isSearch || boardId <= 0 ? undefined : boardId,
sort,
keyword,
sort === "recommended",
cookie
);
// 聚合面板失败不阻塞帖子流
const overviewPromise: Promise<OverviewResponse | null> = fetchOverview(cookie).catch(() => null);
const boardSidebarPromise: Promise<BoardSidebarResponse | null> =
boardId > 0 && !isSearch
? fetchBoardSidebar(boardId, cookie).catch(() => null)
: Promise.resolve(null);
const [me, postsData, overview, boardSidebar] = await Promise.all([
mePromise,
postsPromise,
overviewPromise,
boardSidebarPromise,
]);
const user = me.user;
const boards = overview?.boards ?? [];
const activeBoard = boards.find((b) => b.id === boardId) || boardSidebar?.board || null;
const showBoardPanels = boardId > 0 && !isSearch;
// boardId 有效但接口失败 → 显示简卡;成功则板级右栏;否则全站右栏
const boardPanelsMissing = showBoardPanels && !boardSidebar;
// 搜索态:单栏占满,无 Tabs/侧栏
if (isSearch) {
return (
<div className="max-w-3xl mx-auto">
<div className="mb-5">
<h1 className="text-[24px] sm:text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
搜索“<span style={{ color: "var(--accent)" }}>{keyword}</span>”
</h1>
<p className="meta mt-2">共 {postsData.total} 篇结果 · 按相关度与时间排序</p>
</div>
<div className="panel overflow-hidden">
{postsData.posts.length > 0 ? (
postsData.posts.map((post, i) => (
<PostRow key={post.id} post={post} boardHref index={i} showViews={false} showLike={false} />
))
) : (
<EmptyState keyword={keyword} />
)}
</div>
<Pagination
total={postsData.total}
page={page}
size={20}
basePath="/"
query={{ q: keyword }}
/>
</div>
);
}
// 置顶帖直接按后端 pinned DESC 顺序并入统一信息流;标题仅供屏幕阅读器
const heading = activeBoard
? activeBoard.name
: sort === "new"
? "最新发布"
: sort === "recommended"
? "推荐帖"
: "最新讨论";
const pageQuery: Record<string, string> = {};
if (boardId > 0) pageQuery.board = String(boardId);
if (sort !== "latest") pageQuery.sort = sort;
return (
<div>
<WorkspaceNavMobile boards={boards} user={user} />
<div className="flex items-start gap-6 xl:gap-7 mt-5 lg:mt-0">
<WorkspaceNav boards={boards} user={user} />
{/* 中栏:tab 栏与帖子流为同一个整体卡片(左栏切板块只替换这一列) */}
<div className="flex-1 min-w-0">
<h1 className="sr-only">{heading}</h1>
<div className="panel overflow-hidden">
<div className="flex items-center justify-between gap-2 px-3 sm:px-4 pt-3 pb-2.5">
<FeedTabs current={sort} boardId={boardId} />
<span className="meta shrink-0 text-[12px] tabular-nums">
共 {postsData.total} 篇
</span>
</div>
{user && (
<FeedNewTip
sort={sort}
boardId={boardId}
page={page}
userId={user.id}
/>
)}
{postsData.posts.length > 0 ? (
postsData.posts.map((post, i) => (
<PostRow
key={post.id}
post={post}
boardHref={!activeBoard}
compact
index={i}
showViews={false}
showLike={false}
/>
))
) : (
<EmptyState keyword="" />
)}
</div>
<Pagination
total={postsData.total}
page={page}
size={20}
basePath="/"
query={Object.keys(pageQuery).length > 0 ? pageQuery : undefined}
/>
</div>
{/* 右栏:选中板块时切板级面板,否则全站社区面板 */}
{showBoardPanels ? (
boardSidebar ? (
<BoardPanels
board={boardSidebar.board}
moderators={boardSidebar.moderators}
stats={boardSidebar.stats}
hot={boardSidebar.hot}
activeUsers={boardSidebar.active_users}
checkin={boardSidebar.checkin ?? undefined}
/>
) : boardPanelsMissing ? (
<BoardMissingPanel />
) : null
) : (
overview && (
<CommunityPanels
stats={overview.stats}
hot={overview.hot}
activeUsers={overview.active_users}
announcements={overview.announcements}
newUsers={overview.new_users}
checkin={overview.checkin ?? undefined}
/>
)
)}
</div>
{/* 窄屏:面板下沉到帖子流之后 */}
{showBoardPanels ? (
boardSidebar ? (
<div className="mt-6">
<BoardPanelsMobile
board={boardSidebar.board}
moderators={boardSidebar.moderators}
stats={boardSidebar.stats}
hot={boardSidebar.hot}
activeUsers={boardSidebar.active_users}
checkin={boardSidebar.checkin ?? undefined}
/>
</div>
) : null
) : (
overview && (
<div className="mt-6">
<CommunityPanelsMobile
stats={overview.stats}
hot={overview.hot}
activeUsers={overview.active_users}
announcements={overview.announcements}
newUsers={overview.new_users}
checkin={overview.checkin ?? undefined}
/>
</div>
)
)}
</div>
);
}