feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -16,7 +16,6 @@ import {
|
||||
fetchUserProfile,
|
||||
fetchSidebar,
|
||||
postDeleteTypeLabel,
|
||||
type UserProfile,
|
||||
type PostListItem,
|
||||
type CommentsResponse,
|
||||
type SidebarPublicPayload,
|
||||
@@ -39,6 +38,7 @@ import { publicMarkdownForSeo } from "@/lib/hideBlocks";
|
||||
import PostAttachments from "@/components/PostAttachments";
|
||||
import PostEditedMark from "@/components/PostEditedMark";
|
||||
import PostInteractPanels, { QuestionStatusCard } from "@/components/PostInteractPanels";
|
||||
import MobileRailDrawers from "@/components/MobileRailDrawers";
|
||||
import AcceptedAnswerCard from "@/components/AcceptedAnswerCard";
|
||||
import { canModerateBoard } from "@/lib/roles";
|
||||
import { IconComment, IconEye } from "@/components/Icons";
|
||||
@@ -122,7 +122,7 @@ function RelatedPosts({
|
||||
const titleId = `${uid}-title`;
|
||||
return (
|
||||
<section
|
||||
className="j13-module shrink-0 max-sm:-mx-4 max-sm:rounded-none max-sm:border-x-0 max-sm:shadow-none"
|
||||
className="j13-module shrink-0"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<p className="j13-module-title mb-1.5" id={titleId}>
|
||||
@@ -163,87 +163,9 @@ function RelatedPosts({
|
||||
);
|
||||
}
|
||||
|
||||
/* 移动端 / 窄屏作者卡(xl 以下内联在正文与评论之间,替代左栏作者模块) */
|
||||
function AuthorCardStacked({
|
||||
profile,
|
||||
allowComments,
|
||||
boardNames,
|
||||
boardColors,
|
||||
urlStyle,
|
||||
}: {
|
||||
profile: UserProfile;
|
||||
allowComments: boolean;
|
||||
/** 板块 id → 名称/配色:板块管理员徽章展示「{板块名}板块管理员」并对齐板块色 */
|
||||
boardNames?: Record<number, string>;
|
||||
boardColors?: Record<number, number>;
|
||||
urlStyle: UrlStyle;
|
||||
}) {
|
||||
const u = profile.user;
|
||||
const signature = (u.signature || "").trim();
|
||||
const { post_count, comment_count, points, streak } = profile.stats;
|
||||
return (
|
||||
<div className="panel p-4 sm:p-6 max-sm:-mx-4 max-sm:rounded-none max-sm:border-x-0 max-sm:shadow-none">
|
||||
<p
|
||||
className="text-[11.5px] font-medium tracking-[0.08em] uppercase mb-3.5"
|
||||
style={{ color: "var(--ink-3)" }}
|
||||
>
|
||||
关于作者
|
||||
</p>
|
||||
<div className="flex items-start gap-4 flex-wrap">
|
||||
<Link href={userPath(urlStyle, u.id)} className="shrink-0" aria-label={u.nickname}>
|
||||
<Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={52} />
|
||||
</Link>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Link
|
||||
href={userPath(urlStyle, u.id)}
|
||||
className="font-semibold text-[15px] hover:text-[var(--accent)] transition-colors"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{u.nickname}
|
||||
</Link>
|
||||
<LevelBadge level={u.level} />
|
||||
{(u.badges ?? []).slice(0, 2).map((ub) => (
|
||||
<BadgeChip key={ub.id} badge={ub.badge} />
|
||||
))}
|
||||
{u.role !== "user" && (
|
||||
<RoleBadge role={u.role} boardIds={u.board_ids} boardNames={boardNames} boardColors={boardColors} />
|
||||
)}
|
||||
</div>
|
||||
<p className="meta mt-1 flex items-center gap-1.5 flex-wrap">
|
||||
<span>@{u.username}</span>
|
||||
<span aria-hidden>·</span>
|
||||
<span>发帖 {post_count}</span>
|
||||
{allowComments ? (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<span>评论 {comment_count}</span>
|
||||
</>
|
||||
) : null}
|
||||
<span aria-hidden>·</span>
|
||||
<span>积分 {points}</span>
|
||||
{streak > 0 && (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<span className="inline-flex items-center gap-0.5 font-medium" style={{ color: "var(--gold)" }}>
|
||||
<Flame size={11} /> 连签 {streak} 天
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{signature ? (
|
||||
<p className="mt-2 text-[13px] leading-relaxed line-clamp-2" style={{ color: "var(--ink-2)" }}>
|
||||
{signature}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Link href={userPath(urlStyle, u.id)} className="btn btn-line btn-sm shrink-0 self-center">
|
||||
访问主页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/* ============================================================
|
||||
右栏模块(lg 起展示;移动端收进右抽屉)
|
||||
============================================================ */
|
||||
|
||||
export default async function PostDetailPage({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
@@ -374,6 +296,29 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
? "lg:grid-cols-[224px_minmax(0,1fr)] xl:grid-cols-[240px_minmax(0,1fr)]"
|
||||
: "lg:grid-cols-[minmax(0,1fr)_280px] xl:grid-cols-[minmax(0,1fr)_300px]";
|
||||
|
||||
// 移动端抽屉内容(lg 以下):左抽屉 = 左栏侧栏块,右抽屉 = 右栏侧栏块(含文章目录)+ 同板块热议;
|
||||
// 桌面常驻 aside 与抽屉共用渲染函数,uid 区分同屏两份 DOM 的 aria-labelledby
|
||||
const leftRailNode = hasLeftRail ? (
|
||||
<SidebarRenderer blocks={sidebarLists.left} widgets={sidebar.widgets} props={sidebarProps} />
|
||||
) : null;
|
||||
const hasTocBlock = sidebarLists.right.some((r) => r.key === "post_toc");
|
||||
const renderRightRail = (uid: string) => (
|
||||
<>
|
||||
{hasRightRail ? (
|
||||
<SidebarRenderer blocks={sidebarLists.right} widgets={sidebar.widgets} props={sidebarProps} />
|
||||
) : null}
|
||||
{related.length > 0 ? (
|
||||
<RelatedPosts
|
||||
posts={related}
|
||||
boardId={post.board_id}
|
||||
uid={uid}
|
||||
allowComments={allowComments}
|
||||
urlStyle={urlStyle}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
// 非公开帖(待审/未通过)仅作者本人与管理团队可看,顶部给出醒目状态
|
||||
const isPublished = post.status === "published";
|
||||
const postType = normalizePostType(post.post_type);
|
||||
@@ -487,6 +432,26 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
return (
|
||||
<>
|
||||
<JsonLd data={jsonLd} />
|
||||
{/* 移动端:吸顶工具栏 + 左右抽屉(导航 / 目录),lg 起隐藏;两侧皆无内容时组件自身不渲染 */}
|
||||
<MobileRailDrawers
|
||||
center={
|
||||
<>
|
||||
<p className="truncate text-[13px] font-bold leading-tight" style={{ color: "var(--ink)" }}>
|
||||
{post.title}
|
||||
</p>
|
||||
<p className="meta mt-0.5 text-[10.5px] leading-tight">约 {readingMinutes} 分钟读完</p>
|
||||
</>
|
||||
}
|
||||
left={leftRailNode ? { label: "导航", content: leftRailNode } : undefined}
|
||||
right={
|
||||
hasRightRail || related.length > 0
|
||||
? {
|
||||
label: hasTocBlock ? "目录" : "热议",
|
||||
content: renderRightRail("related-drawer"),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{/* 三栏阅读布局:宽度与首页工作台一致——lg = 左224 + 正文 + 右280,xl = 左240 + 正文 + 右300;以下单栏
|
||||
侧栏两列均可被站长清空,网格随之退化为两栏/单栏 */}
|
||||
<div
|
||||
@@ -500,7 +465,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
className="hidden lg:flex w-56 xl:w-60 shrink-0 sticky top-[var(--rail-top)] flex-col gap-4 max-h-[var(--rail-max-h)] overflow-hidden"
|
||||
aria-label="帖子导航"
|
||||
>
|
||||
<SidebarRenderer blocks={sidebarLists.left} widgets={sidebar.widgets} props={sidebarProps} />
|
||||
{leftRailNode}
|
||||
</aside>
|
||||
)}
|
||||
|
||||
@@ -697,18 +662,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 窄屏作者卡(lg 起由左栏承载,避免重复展示) */}
|
||||
{profile && (
|
||||
<div className="lg:hidden mt-4">
|
||||
<AuthorCardStacked
|
||||
profile={profile}
|
||||
allowComments={allowComments}
|
||||
boardNames={boardNames}
|
||||
boardColors={boardColors}
|
||||
urlStyle={urlStyle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 窄屏作者卡已移除:作者信息随左栏配置收进移动端左抽屉 */}
|
||||
|
||||
{/* 关评:前台仅提示不支持评论,不展示历史楼层;非公开帖不提及评论 */}
|
||||
{isPublished ? (
|
||||
@@ -756,17 +710,6 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
</p>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* 窄屏相关讨论(lg 起由右栏承载) */}
|
||||
<div className="lg:hidden mt-4">
|
||||
<RelatedPosts
|
||||
posts={related}
|
||||
boardId={post.board_id}
|
||||
uid="related-inline"
|
||||
allowComments={allowComments}
|
||||
urlStyle={urlStyle}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -776,8 +719,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
className="hidden lg:flex w-[280px] xl:w-[300px] shrink-0 sticky top-[var(--rail-top)] flex-col gap-4 max-h-[var(--rail-max-h)] overflow-hidden"
|
||||
aria-label="阅读辅助"
|
||||
>
|
||||
<SidebarRenderer blocks={sidebarLists.right} widgets={sidebar.widgets} props={sidebarProps} />
|
||||
<RelatedPosts posts={related} boardId={post.board_id} allowComments={allowComments} urlStyle={urlStyle} />
|
||||
{renderRightRail("related")}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user