feat: 全站评论开关,并统一后台表单控件

关闭评论后前台隐藏评论能力;外观/设置/公告改用 AdminField 与 Stepper。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 01:22:00 +08:00
parent 442af625f6
commit d93635c961
27 changed files with 989 additions and 479 deletions

View File

@@ -22,6 +22,7 @@ import {
type UserProfile,
type PostListItem,
type Board,
type CommentsResponse,
} from "@/lib/api";
import { authCookieHeader } from "@/lib/cookies";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
@@ -152,13 +153,19 @@ function BoardCard({ board }: { board: Board }) {
);
}
function AuthorRailCard({ profile }: { profile: UserProfile }) {
function AuthorRailCard({
profile,
allowComments,
}: {
profile: UserProfile;
allowComments: boolean;
}) {
const u = profile.user;
const signature = (u.signature || "").trim();
const { post_count, comment_count, points, streak } = profile.stats;
const stats = [
{ label: "发帖", value: post_count },
{ label: "评论", value: comment_count },
...(allowComments ? [{ label: "评论", value: comment_count }] : []),
{ label: "积分", value: points },
];
return (
@@ -197,7 +204,7 @@ function AuthorRailCard({ profile }: { profile: UserProfile }) {
</p>
) : null}
<div
className="mt-3 grid grid-cols-3 border-t"
className={`mt-3 grid border-t ${stats.length === 3 ? "grid-cols-3" : "grid-cols-2"}`}
style={{ borderColor: "var(--line)" }}
>
{stats.map((s, i) => (
@@ -271,11 +278,13 @@ function RelatedPosts({
posts,
boardId,
uid = "related",
allowComments,
}: {
posts: PostListItem[];
boardId: number;
// 右栏与移动端内联实例同时存在,id 必须区分以免 aria-labelledby 指向隐藏副本
uid?: string;
allowComments: boolean;
}) {
if (posts.length === 0) return null;
const titleId = `${uid}-title`;
@@ -294,9 +303,11 @@ function RelatedPosts({
{p.title}
</span>
<span className="mt-1 flex items-center gap-2.5 meta text-[11.5px]">
<span className="inline-flex items-center gap-1">
<MessageCircle size={11.5} /> {p.comment_count}
</span>
{allowComments ? (
<span className="inline-flex items-center gap-1">
<MessageCircle size={11.5} /> {p.comment_count}
</span>
) : null}
<span className="inline-flex items-center gap-1">
<Heart size={11.5} /> {p.like_count}
</span>
@@ -318,7 +329,13 @@ function RelatedPosts({
}
/* 移动端 / 窄屏作者卡(xl 以下内联在正文与评论之间,替代左栏作者模块) */
function AuthorCardStacked({ profile }: { profile: UserProfile }) {
function AuthorCardStacked({
profile,
allowComments,
}: {
profile: UserProfile;
allowComments: boolean;
}) {
const u = profile.user;
const signature = (u.signature || "").trim();
const { post_count, comment_count, points, streak } = profile.stats;
@@ -349,8 +366,12 @@ function AuthorCardStacked({ profile }: { profile: UserProfile }) {
<span>@{u.username}</span>
<span aria-hidden>·</span>
<span>发帖 {post_count}</span>
<span aria-hidden>·</span>
<span>评论 {comment_count}</span>
{allowComments ? (
<>
<span aria-hidden>·</span>
<span>评论 {comment_count}</span>
</>
) : null}
<span aria-hidden>·</span>
<span>积分 {points}</span>
{streak > 0 && (
@@ -441,11 +462,18 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
);
}
// 评论 / 当前用户 / 站点设置 / 作者资料 / 同板块热议并行拉取;侧栏数据失败一律静默降级,不阻塞正文
const [commentsData, me, siteSettings, profile, relatedData] = await Promise.all([
fetchComments(id, commentPage, cookie),
// 站点设置已在 layout 缓存;关评时不拉评论树,避免首屏 HTML 泄露历史评论
const allowComments = (await getPublicSettingsCached()).allow_comments !== false;
const emptyComments: CommentsResponse = {
comments: [],
total: 0,
total_comments: 0,
page: 1,
size: 20,
};
const [commentsData, me, profile, relatedData] = await Promise.all([
allowComments ? fetchComments(id, commentPage, cookie) : Promise.resolve(emptyComments),
getMeCached(cookie || undefined),
getPublicSettingsCached(),
fetchUserProfile(String(post.user.id), 1, cookie).catch(() => null),
fetchPosts(1, 8, post.board_id, "recommended", "", false, cookie).catch(() => null),
]);
@@ -504,13 +532,14 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
datePublished: post.created_at,
dateModified: post.updated_at,
author: { "@type": "Person", name: post.user.nickname },
commentCount: post.comment_count,
...(allowComments ? { commentCount: post.comment_count } : {}),
interactionStatistic: {
"@type": "InteractionCounter",
interactionType: { "@type": "ReadAction" },
userInteractionCount: post.view_count,
},
...(acceptedAnswer &&
...(allowComments &&
acceptedAnswer &&
!acceptedAnswer.deleted &&
!post.content_locked && {
acceptedAnswer: {
@@ -546,7 +575,12 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
{/* 移动端浮动操作条:长文阅读中途可点赞 / 跳评论 / 回顶部 */}
<MobilePostBar postId={id} liked={post.liked} likeCount={post.like_count} />
<MobilePostBar
postId={id}
liked={post.liked}
likeCount={post.like_count}
allowComments={allowComments}
/>
{/* 三栏阅读布局:宽度与首页工作台一致——lg = 左224 + 正文 + 右280,xl = 左240 + 正文 + 右300;以下单栏 */}
<div className="grid grid-cols-1 items-start lg:grid-cols-[224px_minmax(0,1fr)_280px] xl:grid-cols-[240px_minmax(0,1fr)_300px] gap-x-6 xl:gap-x-7">
@@ -558,7 +592,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
aria-label="帖子导航"
>
<BoardCard board={post.board} />
{profile && <AuthorRailCard profile={profile} />}
{profile && <AuthorRailCard profile={profile} allowComments={allowComments} />}
{profile && <AuthorPosts posts={authorPosts} />}
</aside>
@@ -656,9 +690,11 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
<span className="inline-flex items-center gap-1.5" aria-label={`浏览 ${post.view_count} 次`}>
<IconEye size={14} /> {post.view_count} 浏览
</span>
<span className="inline-flex items-center gap-1.5" aria-label={`评论 ${post.comment_count} 条`}>
<IconComment size={14} /> {post.comment_count} 评论
</span>
{allowComments ? (
<span className="inline-flex items-center gap-1.5" aria-label={`评论 ${post.comment_count} 条`}>
<IconComment size={14} /> {post.comment_count} 评论
</span>
) : null}
<span className="inline-flex items-center gap-1.5" aria-label={`阅读约需 ${readingMinutes} 分钟`}>
<Clock size={14} /> 约 {readingMinutes} 分钟读完
</span>
@@ -707,7 +743,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
lottery={post.lottery}
loggedIn={!!me.user}
/>
{!post.content_locked && acceptedAnswer ? (
{!post.content_locked && allowComments && acceptedAnswer ? (
<AcceptedAnswerCard
answer={acceptedAnswer}
variant={acceptedAnswerVariant}
@@ -738,11 +774,11 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
{/* 窄屏作者卡(lg 起由左栏承载,避免重复展示) */}
{profile && (
<div className="lg:hidden mt-6">
<AuthorCardStacked profile={profile} />
<AuthorCardStacked profile={profile} allowComments={allowComments} />
</div>
)}
{/* 非公开帖不开放评论(后端同样拒绝),给出状态提示 */}
{/* 关评:前台仅提示不支持评论,不展示历史楼层;非公开帖不提及评论 */}
{isPublished ? (
<CommentSection
postId={id}
@@ -754,7 +790,6 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
page={commentsData.page}
size={commentsData.size}
user={me.user}
allowComments={siteSettings.allow_comments}
acceptMode={acceptMode}
acceptedCommentId={
post.question?.accepted_comment_id ||
@@ -765,11 +800,11 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
post.lottery?.drawn
? (post.lottery.winners || [])
.map((w) => w.comment_id || 0)
.filter((id) => id > 0)
.filter((cid) => cid > 0)
: []
}
/>
) : (
) : allowComments ? (
<section className="panel p-6 sm:p-8 mt-6 text-center" aria-label="评论区">
<p className="text-[14px]" style={{ color: "var(--ink-2)" }}>
{post.status === "pending" ? "帖子审核通过后开放评论" : "帖子未通过审核,评论已关闭"}
@@ -782,11 +817,16 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
: "审核通过后评论区将自动开放"}
</p>
</section>
)}
) : null}
{/* 窄屏相关讨论(lg 起由右栏承载) */}
<div className="lg:hidden mt-6">
<RelatedPosts posts={related} boardId={post.board_id} uid="related-inline" />
<RelatedPosts
posts={related}
boardId={post.board_id}
uid="related-inline"
allowComments={allowComments}
/>
</div>
</article>
</div>
@@ -797,7 +837,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
aria-label="阅读辅助"
>
{toc.length > 0 && <PostToc items={toc} />}
<RelatedPosts posts={related} boardId={post.board_id} />
<RelatedPosts posts={related} boardId={post.board_id} allowComments={allowComments} />
</aside>
</div>