本次提交完成了多个核心功能迭代: 1. 新增用户每日签到系统,包含签到状态查询、连续天数统计、积分累计逻辑 2. 新增"新帖子"排序选项,替换原有的latest排序为新评论排序 3. 重构首页信息流布局,优化移动端与桌面端展示样式 4. 新增最新评论、最新注册用户模块,完善社区面板数据展示 5. 新增静态友情链接配置模块 6. 提取通用时间格式化工具函数,统一全站相对时间展示 7. 补充数据库签到模型与后端API接口,完善前后端交互链路
215 lines
7.2 KiB
TypeScript
215 lines
7.2 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";
|
||
|
||
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;
|
||
// 兼容旧的 ?sort=hot 链接:落到最接近的推荐帖
|
||
if (v === "hot") return "recommended";
|
||
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 === "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 === "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>
|
||
{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
|
||
stats={overview.stats}
|
||
hot={overview.hot}
|
||
activeUsers={overview.active_users}
|
||
latestComments={overview.latest_comments}
|
||
newUsers={overview.new_users}
|
||
checkin={overview.checkin ?? undefined}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* 窄屏:面板下沉到帖子流之后 */}
|
||
{overview && (
|
||
<div className="mt-6">
|
||
<CommunityPanelsMobile
|
||
hot={overview.hot}
|
||
activeUsers={overview.active_users}
|
||
latestComments={overview.latest_comments}
|
||
newUsers={overview.new_users}
|
||
checkin={overview.checkin ?? undefined}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|