feat: 新增站点公告系统与多项体验优化

此提交完成了以下核心功能与优化:
1. 新增站点公告全流程支持:包含公告模型、CRUD接口、管理后台页面、公告详情页与首页展示
2. 实现评论楼层化与回复嵌套:重构评论系统为树状结构,支持回复评论与层级限制
3. 添加用户在线状态追踪:异步更新用户最后活跃时间并做SQL限频
4. 优化用户体验:移动端浮动操作条、统一板块标签样式、用户页签到积分展示、注册即登录优化
5. 其他细节优化:SEO站点地图新增公告页、评论分页与统计、登录/注册表单增强等
This commit is contained in:
2026-09-13 21:38:01 +08:00
parent 9bae6f2513
commit 996a870015
41 changed files with 2846 additions and 786 deletions

View File

@@ -0,0 +1,165 @@
import Link from "next/link";
import { FileText, MessageCircle, AlertCircle } from "lucide-react";
import Pagination from "@/components/Pagination";
import { IconComment, IconEye, IconHeart } from "@/components/Icons";
import type { UserProfile, UserCommentsResponse } from "@/lib/api";
/**
* 用户主页内容区(纯服务端渲染):
* tab 与分页全部走 URL(?tab=comments&page=N),刷新/后退/分享链接均保持一致。
*/
export default function UserProfileContent({
userId,
profile,
tab,
commentData,
commentError,
page,
}: {
userId: string;
profile: UserProfile;
tab: "posts" | "comments";
commentData: UserCommentsResponse | null;
commentError: boolean;
page: number;
}) {
const tabLink = (t: "posts" | "comments", label: string, count: number, icon: React.ReactNode) => {
const active = tab === t;
const href = t === "posts" ? `/u/${userId}` : `/u/${userId}?tab=comments`;
return (
<Link
href={href}
aria-current={active ? "page" : undefined}
className={`inline-flex items-center justify-center gap-1.5 px-4 py-2 rounded-full text-sm font-semibold transition-all flex-1 sm:flex-none ${
active ? "" : "hover:text-[var(--ink)]"
}`}
style={{
background: active ? "var(--panel)" : "transparent",
color: active ? "var(--accent)" : "var(--ink-3)",
boxShadow: active ? "var(--shadow-sm)" : "none",
}}
>
{icon}
{label}
<span
className="text-[11px] font-bold px-1.5 py-px rounded-full tabular-nums"
style={{
background: active ? "var(--accent-soft)" : "var(--tint)",
color: active ? "var(--accent)" : "var(--ink-3)",
}}
>
{count}
</span>
</Link>
);
};
return (
<div>
<div
className="inline-flex w-full sm:w-auto items-center gap-1 mb-6 p-1 rounded-full"
style={{ background: "var(--panel-2)" }}
>
{tabLink("posts", "发帖", profile.stats.post_count, <FileText size={15} />)}
{tabLink("comments", "评论", profile.stats.comment_count, <MessageCircle size={15} />)}
</div>
{tab === "posts" ? (
<div>
{profile.posts.length > 0 ? (
<div className="panel overflow-hidden p-0">
{profile.posts.map((post, i) => (
<Link
key={post.id}
href={`/post/${post.id}`}
className="group block px-4 sm:px-5 py-4 transition-colors hover:bg-[var(--panel-2)]"
style={{ borderTop: i === 0 ? "none" : "1px solid var(--line)" }}
>
<div className="flex items-center gap-2 mb-2 flex-wrap">
<span className="j13-board-tag">{post.board.name}</span>
{post.pinned > 0 && <span className="badge-pin">置顶</span>}
{post.recommended && <span className="badge-rec">推荐</span>}
</div>
<h2
className="font-semibold mb-2 leading-snug transition-colors group-hover:text-[var(--accent)]"
style={{ color: "var(--ink)" }}
>
{post.title}
</h2>
<div className="flex items-center gap-4 meta text-[12px]">
<span className="tabular-nums">{new Date(post.created_at).toLocaleDateString("zh-CN")}</span>
<span className="inline-flex items-center gap-1">
<IconComment size={12} /> {post.comment_count}
</span>
<span className="inline-flex items-center gap-1">
<IconEye size={12} /> {post.view_count}
</span>
<span className="inline-flex items-center gap-1">
<IconHeart size={12} /> {post.like_count}
</span>
</div>
</Link>
))}
</div>
) : (
<div className="panel text-center py-16 px-6">
<FileText size={26} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} />
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}>暂无发帖</p>
</div>
)}
<Pagination
total={profile.posts_total}
page={page}
size={20}
basePath={`/u/${userId}`}
/>
</div>
) : commentError ? (
<div className="panel text-center py-16 px-6" role="alert">
<AlertCircle size={26} className="mx-auto mb-3" style={{ color: "var(--danger)" }} />
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}>评论加载失败</p>
<p className="meta mt-1.5 mb-4">网络异常,请稍后重试</p>
<Link href={`/u/${userId}?tab=comments`} className="btn btn-line btn-sm">重新加载</Link>
</div>
) : commentData && commentData.comments.length > 0 ? (
<div>
<div className="panel overflow-hidden p-0">
{commentData.comments.map((c, i) => (
<Link
key={c.id}
href={`/post/${c.post_id}`}
className="block px-4 sm:px-5 py-4 transition-colors hover:bg-[var(--panel-2)]"
style={{ borderTop: i === 0 ? "none" : "1px solid var(--line)" }}
>
<div className="meta text-xs mb-2">
评论于《<span style={{ color: "var(--accent)" }}>{c.post_title}</span>》
</div>
<p
className="text-[15px] leading-relaxed line-clamp-2"
style={{ color: "var(--ink)" }}
>
{c.content}
</p>
<div className="meta text-xs mt-2 tabular-nums">
{new Date(c.created_at).toLocaleString("zh-CN")}
</div>
</Link>
))}
</div>
<Pagination
total={commentData.total}
page={commentData.page}
size={20}
basePath={`/u/${userId}`}
query={{ tab: "comments" }}
/>
</div>
) : (
<div className="panel text-center py-16 px-6">
<MessageCircle size={26} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} />
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}>暂无评论</p>
</div>
)}
</div>
);
}