feat: 关于本站对齐站点公告,增加全部与管理入口
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
156
frontend/app/about/page.tsx
Normal file
156
frontend/app/about/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Info } from "lucide-react";
|
||||
import {
|
||||
fetchPublishedSitePages,
|
||||
fetchOverview,
|
||||
type SitePageSummary,
|
||||
} from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { isAdminOrAbove } from "@/lib/roles";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { pageGlyph } from "@/lib/pageGlyph";
|
||||
import {
|
||||
SiteDocShell,
|
||||
SiteDocIdentity,
|
||||
SiteDocAnnoNav,
|
||||
SiteDocBreadcrumb,
|
||||
} from "@/components/site-doc";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "关于本站",
|
||||
description: "站点说明、政策与帮助文档",
|
||||
};
|
||||
|
||||
function PageRow({
|
||||
p,
|
||||
showDivider,
|
||||
}: {
|
||||
p: SitePageSummary;
|
||||
showDivider: boolean;
|
||||
}) {
|
||||
const Icon = pageGlyph(p.slug);
|
||||
return (
|
||||
<Link
|
||||
href={`/p/${p.slug}`}
|
||||
className="block px-5 sm:px-6 py-4 sm:py-5 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
|
||||
style={showDivider ? { borderTop: "1px solid var(--line)" } : undefined}
|
||||
>
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<span
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 mt-0.5"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
aria-hidden
|
||||
>
|
||||
<Icon size={16} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 flex-wrap">
|
||||
<h2
|
||||
className="text-[16px] sm:text-[17px] font-semibold leading-snug tracking-tight"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{p.title}
|
||||
</h2>
|
||||
<time className="meta text-[12px] tabular-nums ml-auto" dateTime={p.updated_at}>
|
||||
{formatDate(p.updated_at || p.created_at)}
|
||||
</time>
|
||||
</span>
|
||||
{p.excerpt ? (
|
||||
<p className="mt-1.5 text-[13.5px] leading-relaxed line-clamp-2" style={{ color: "var(--ink-2)" }}>
|
||||
{p.excerpt}
|
||||
</p>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AboutIndexPage() {
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
let pages: SitePageSummary[] = [];
|
||||
let error = "";
|
||||
try {
|
||||
const res = await fetchPublishedSitePages();
|
||||
pages = res.pages ?? [];
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "加载页面失败";
|
||||
}
|
||||
|
||||
const [overview, settings, me] = await Promise.all([
|
||||
fetchOverview().catch(() => null),
|
||||
getPublicSettingsCached(),
|
||||
getMeCached(cookie || undefined),
|
||||
]);
|
||||
const announcements = overview?.announcements ?? [];
|
||||
const siteLead = settings.site_description?.trim() || undefined;
|
||||
const canManagePages = !!me.user && isAdminOrAbove(me.user.role);
|
||||
|
||||
const left = <SiteDocIdentity kind="about" lead={siteLead} />;
|
||||
const right = <SiteDocAnnoNav items={announcements} />;
|
||||
|
||||
return (
|
||||
<SiteDocShell
|
||||
left={left}
|
||||
right={right}
|
||||
breadcrumb={
|
||||
<SiteDocBreadcrumb
|
||||
items={[
|
||||
{ href: "/", label: "首页" },
|
||||
{ label: "关于本站" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
mobileExtras={
|
||||
announcements.length > 0 ? <SiteDocAnnoNav items={announcements} /> : null
|
||||
}
|
||||
>
|
||||
<header className="mb-5 flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1
|
||||
className="text-[24px] sm:text-[28px] font-extrabold tracking-tight flex items-center gap-2"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
<Info size={26} style={{ color: "var(--accent)" }} aria-hidden />
|
||||
关于本站
|
||||
</h1>
|
||||
<p className="meta mt-2">
|
||||
{error
|
||||
? "加载失败"
|
||||
: pages.length > 0
|
||||
? `共 ${pages.length} 篇站点文档`
|
||||
: "站点说明与政策文档"}
|
||||
</p>
|
||||
</div>
|
||||
{canManagePages ? (
|
||||
<Link
|
||||
href="/admin/pages"
|
||||
className="meta text-[13px] shrink-0 mb-0.5 transition-colors hover:text-[var(--accent)]"
|
||||
>
|
||||
管理
|
||||
</Link>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{error ? (
|
||||
<p className="alert-error">{error}</p>
|
||||
) : pages.length === 0 ? (
|
||||
<div className="panel p-10 text-center">
|
||||
<p className="font-medium" style={{ color: "var(--ink)" }}>
|
||||
暂无介绍
|
||||
</p>
|
||||
<p className="meta mt-1.5">有新文档时会显示在这里</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel overflow-hidden">
|
||||
{pages.map((p, i) => (
|
||||
<PageRow key={p.id} p={p} showDivider={i > 0} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SiteDocShell>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Megaphone } from "lucide-react";
|
||||
import { fetchAnnouncementDetail, fetchOverview } from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { isAdminOrAbove } from "@/lib/roles";
|
||||
import { extractToc, estimateReadingMinutes, markdownHeadingToc } from "@/lib/toc";
|
||||
import MarkdownBody from "@/components/MarkdownBody";
|
||||
import PostToc from "@/components/PostToc";
|
||||
@@ -53,6 +54,7 @@ export default async function AnnouncementPage({ params }: PageProps) {
|
||||
const loginHref = `/login?redirect=${encodeURIComponent(`/announcement/${id}`)}`;
|
||||
const sidebarPages = overview?.sidebar_pages ?? [];
|
||||
const announcements = overview?.announcements ?? [];
|
||||
const canManagePages = !!me.user && isAdminOrAbove(me.user.role);
|
||||
const siteLead = settings.site_description?.trim() || undefined;
|
||||
const toc = extractToc(a.content);
|
||||
const headingToc = markdownHeadingToc(toc);
|
||||
@@ -89,7 +91,7 @@ export default async function AnnouncementPage({ params }: PageProps) {
|
||||
const right = (
|
||||
<>
|
||||
{toc.length > 0 ? <PostToc items={toc} /> : null}
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" />
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" canManage={canManagePages} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -116,7 +118,7 @@ export default async function AnnouncementPage({ params }: PageProps) {
|
||||
<>
|
||||
{toc.length > 0 ? <PostToc items={toc} /> : null}
|
||||
<SiteDocAnnoNav items={announcements} currentId={annId} />
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" />
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" canManage={canManagePages} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { fetchAnnouncementsPage, fetchOverview, type Announcement } from "@/lib/api";
|
||||
import { getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { isAdminOrAbove } from "@/lib/roles";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import {
|
||||
@@ -114,19 +117,24 @@ export default async function AnnouncementsListPage({ searchParams }: PageProps)
|
||||
error = e instanceof Error ? e.message : "加载公告失败";
|
||||
}
|
||||
|
||||
const [overview, settings] = await Promise.all([
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const [overview, settings, me] = await Promise.all([
|
||||
fetchOverview().catch(() => null),
|
||||
getPublicSettingsCached(),
|
||||
getMeCached(cookie || undefined),
|
||||
]);
|
||||
const sidebarPages = overview?.sidebar_pages ?? [];
|
||||
const siteLead = settings.site_description?.trim() || undefined;
|
||||
const canManagePages = !!me.user && isAdminOrAbove(me.user.role);
|
||||
|
||||
// 首页置顶条目作头图;其余走列表行(置顶已在头图时不再重复)
|
||||
const featured = page === 1 ? announcements.find((a) => a.pinned) : undefined;
|
||||
const rest = featured ? announcements.filter((a) => a.id !== featured.id) : announcements;
|
||||
|
||||
const left = <SiteDocIdentity kind="announcements" lead={siteLead} />;
|
||||
const right = <SiteDocPageNav pages={sidebarPages} title="关于本站" />;
|
||||
const right = (
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" canManage={canManagePages} />
|
||||
);
|
||||
|
||||
return (
|
||||
<SiteDocShell
|
||||
@@ -140,7 +148,9 @@ export default async function AnnouncementsListPage({ searchParams }: PageProps)
|
||||
]}
|
||||
/>
|
||||
}
|
||||
mobileExtras={<SiteDocPageNav pages={sidebarPages} title="关于本站" />}
|
||||
mobileExtras={
|
||||
<SiteDocPageNav pages={sidebarPages} title="关于本站" canManage={canManagePages} />
|
||||
}
|
||||
>
|
||||
<header className="mb-5">
|
||||
<h1
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { fetchSitePageBySlug, fetchOverview } from "@/lib/api";
|
||||
import { getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { isAdminOrAbove } from "@/lib/roles";
|
||||
import { extractToc, estimateReadingMinutes, markdownHeadingToc } from "@/lib/toc";
|
||||
import MarkdownBody from "@/components/MarkdownBody";
|
||||
import PostToc from "@/components/PostToc";
|
||||
@@ -38,16 +41,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
|
||||
export default async function SitePagePublic({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const [data, overview, settings] = await Promise.all([
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const [data, overview, settings, me] = await Promise.all([
|
||||
fetchSitePageBySlug(slug).catch(() => null),
|
||||
fetchOverview().catch(() => null),
|
||||
getPublicSettingsCached(),
|
||||
getMeCached(cookie || undefined),
|
||||
]);
|
||||
if (!data) notFound();
|
||||
|
||||
const p = data.page;
|
||||
const sidebarPages = overview?.sidebar_pages ?? [];
|
||||
const announcements = overview?.announcements ?? [];
|
||||
const canManagePages = !!me.user && isAdminOrAbove(me.user.role);
|
||||
const siteLead = settings.site_description?.trim() || undefined;
|
||||
const toc = extractToc(p.content);
|
||||
const headingToc = markdownHeadingToc(toc);
|
||||
@@ -76,7 +82,11 @@ export default async function SitePagePublic({ params }: PageProps) {
|
||||
const left = (
|
||||
<>
|
||||
<SiteDocIdentity kind="about" lead={siteLead} />
|
||||
<SiteDocPageNav pages={sidebarPages} currentSlug={slug} />
|
||||
<SiteDocPageNav
|
||||
pages={sidebarPages}
|
||||
currentSlug={slug}
|
||||
canManage={canManagePages}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -101,7 +111,7 @@ export default async function SitePagePublic({ params }: PageProps) {
|
||||
<SiteDocBreadcrumb
|
||||
items={[
|
||||
{ href: "/", label: "首页" },
|
||||
{ label: "关于本站" },
|
||||
{ href: "/about", label: "关于本站" },
|
||||
{ label: p.title },
|
||||
]}
|
||||
/>
|
||||
@@ -109,7 +119,12 @@ export default async function SitePagePublic({ params }: PageProps) {
|
||||
mobileExtras={
|
||||
<>
|
||||
{toc.length > 0 ? <PostToc items={toc} /> : null}
|
||||
<SiteDocPageNav pages={sidebarPages} currentSlug={slug} title="文档目录" />
|
||||
<SiteDocPageNav
|
||||
pages={sidebarPages}
|
||||
currentSlug={slug}
|
||||
title="文档目录"
|
||||
canManage={canManagePages}
|
||||
/>
|
||||
{toc.length === 0 && announcements.length > 0 ? (
|
||||
<SiteDocAnnoNav items={announcements} />
|
||||
) : null}
|
||||
|
||||
@@ -114,6 +114,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
const user = me.user;
|
||||
const boards = overview?.boards ?? [];
|
||||
const canManageAnnouncements = !!user && isAdminOrAbove(user.role);
|
||||
const canManagePages = canManageAnnouncements;
|
||||
const sidebarPages = overview?.sidebar_pages ?? [];
|
||||
const activeBoard = boards.find((b) => b.id === boardId) || boardSidebar?.board || null;
|
||||
const showBoardPanels = boardId > 0 && !isSearch;
|
||||
@@ -251,6 +252,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
checkin={overview.checkin ?? undefined}
|
||||
allowComments={allowComments}
|
||||
canManageAnnouncements={canManageAnnouncements}
|
||||
canManagePages={canManagePages}
|
||||
sidebarPages={sidebarPages}
|
||||
siteDescription={publicSettings.site_description}
|
||||
/>
|
||||
@@ -285,6 +287,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
checkin={overview.checkin ?? undefined}
|
||||
allowComments={allowComments}
|
||||
canManageAnnouncements={canManageAnnouncements}
|
||||
canManagePages={canManagePages}
|
||||
sidebarPages={sidebarPages}
|
||||
siteDescription={publicSettings.site_description}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { fetchPosts, fetchAnnouncementsPage, fetchOverview } from "@/lib/api";
|
||||
import {
|
||||
fetchPosts,
|
||||
fetchAnnouncementsPage,
|
||||
fetchPublishedSitePages,
|
||||
} from "@/lib/api";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = "http://localhost:3000";
|
||||
@@ -11,6 +15,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
changeFrequency: "daily",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/about`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -41,13 +51,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
/* 忽略 */
|
||||
}
|
||||
|
||||
// 公开单页列表 API 尚未提供:用 overview 侧栏页近似收录已发布且展示中的页面
|
||||
try {
|
||||
const overview = await fetchOverview();
|
||||
for (const p of overview.sidebar_pages ?? []) {
|
||||
const { pages } = await fetchPublishedSitePages();
|
||||
for (const p of pages) {
|
||||
entries.push({
|
||||
url: `${baseUrl}/p/${p.slug}`,
|
||||
lastModified: new Date(),
|
||||
lastModified: new Date(p.updated_at || p.created_at),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.4,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user