- 新增首页欢迎横幅组件,包含时段问候、用户问候语、发帖引导按钮和社区统计数据 - 从CommunityPanels中移除stats相关逻辑,将社区统计迁移到新的HomeHero组件中 - 优化全局样式,添加横幅相关的CSS动画和样式类
220 lines
7.3 KiB
TypeScript
220 lines
7.3 KiB
TypeScript
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,
|
||
type OverviewResponse,
|
||
} 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 { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
|
||
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
|
||
import HomeHero from "@/components/home/HomeHero";
|
||
|
||
export const metadata: Metadata = {
|
||
title: "姜十三论坛 · 技术讨论社区",
|
||
description: "姜十三论坛,分享技术、讨论问题、交流心得的社区",
|
||
};
|
||
|
||
interface HomePageProps {
|
||
searchParams: Promise<{ q?: string; page?: string; sort?: string; board?: string }>;
|
||
}
|
||
|
||
function normalizeSort(v?: string): FeedSort {
|
||
return v === "hot" || v === "recommended" ? v : "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 === "recommended" ? "hot" : sort,
|
||
keyword,
|
||
sort === "recommended",
|
||
cookie
|
||
);
|
||
// 聚合面板失败不阻塞帖子流
|
||
const overviewPromise: Promise<OverviewResponse | null> = fetchOverview(cookie).catch(() => null);
|
||
|
||
const [me, postsData, overview] = await Promise.all([
|
||
mePromise,
|
||
postsPromise,
|
||
overviewPromise,
|
||
]);
|
||
const user = me.user;
|
||
const boards = overview?.boards ?? [];
|
||
const activeBoard = boards.find((b) => b.id === boardId) || null;
|
||
|
||
// 搜索态:单栏占满,无 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 === "hot"
|
||
? "热门讨论"
|
||
: sort === "recommended"
|
||
? "精华内容"
|
||
: "最新讨论";
|
||
const headingHint = activeBoard
|
||
? sort === "hot"
|
||
? "该板块按点赞与评论热度排序"
|
||
: sort === "recommended"
|
||
? "该板块管理员精选的优质内容"
|
||
: "该板块新发帖与新评论都会浮到顶部"
|
||
: sort === "hot"
|
||
? "按点赞与评论热度排序"
|
||
: sort === "recommended"
|
||
? "管理员精选的优质内容"
|
||
: "新发帖与新评论都会浮到顶部";
|
||
|
||
const pageQuery: Record<string, string> = {};
|
||
if (boardId > 0) pageQuery.board = String(boardId);
|
||
if (sort !== "latest") pageQuery.sort = sort;
|
||
|
||
return (
|
||
<div>
|
||
<HomeHero user={user} stats={overview?.stats ?? null} />
|
||
|
||
<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} />
|
||
|
||
{/* 中栏:帖子流(左栏切板块只替换这一列) */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-end justify-between gap-3 flex-wrap mb-4">
|
||
<div className="min-w-0">
|
||
<h1
|
||
className="text-[23px] sm:text-[27px] font-extrabold tracking-tight"
|
||
style={{ color: "var(--ink)" }}
|
||
>
|
||
{heading}
|
||
</h1>
|
||
<p className="meta mt-1.5">
|
||
共 {postsData.total} 篇 · {headingHint}
|
||
</p>
|
||
</div>
|
||
<FeedTabs current={sort} boardId={boardId} />
|
||
</div>
|
||
|
||
<div className="panel overflow-hidden">
|
||
{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>
|
||
|
||
{/* 右栏:社区面板 */}
|
||
{overview && (
|
||
<CommunityPanels hot={overview.hot} activeUsers={overview.active_users} />
|
||
)}
|
||
</div>
|
||
|
||
{/* 窄屏:面板下沉到帖子流之后 */}
|
||
{overview && (
|
||
<div className="mt-6">
|
||
<CommunityPanelsMobile hot={overview.hot} activeUsers={overview.active_users} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|