feat: 站点单页、时间线导入与行级 BBCode 短代码
首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
109
frontend/app/announcements/page.tsx
Normal file
109
frontend/app/announcements/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { fetchAnnouncementsPage, type Announcement } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import Pagination from "@/components/Pagination";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "站点公告",
|
||||
description: "站点公告与通知列表",
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}
|
||||
|
||||
/** 粗略去掉 Markdown,取摘要 */
|
||||
function plainSummary(md: string, max = 120): string {
|
||||
const text = md
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, " ")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/[#>*_`~\-|=]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return text.length > max ? `${text.slice(0, max)}…` : text;
|
||||
}
|
||||
|
||||
function AnnouncementCard({ a }: { a: Announcement }) {
|
||||
const summary = plainSummary(a.content);
|
||||
return (
|
||||
<Link
|
||||
href={`/announcement/${a.id}`}
|
||||
className="panel block p-5 sm:p-6 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{a.pinned ? <span className="badge-pin">置顶</span> : null}
|
||||
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
|
||||
<time className="meta text-[12px] tabular-nums ml-auto" dateTime={a.created_at}>
|
||||
{formatDate(a.created_at)}
|
||||
</time>
|
||||
</div>
|
||||
<h2
|
||||
className="mt-2.5 text-[17px] sm:text-[18px] font-semibold leading-snug tracking-tight"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{a.title}
|
||||
</h2>
|
||||
{summary ? (
|
||||
<p className="mt-2 text-[13.5px] leading-relaxed line-clamp-2" style={{ color: "var(--ink-2)" }}>
|
||||
{summary}
|
||||
</p>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AnnouncementsListPage({ searchParams }: PageProps) {
|
||||
const sp = await searchParams;
|
||||
const page = Math.max(1, parseInt(sp.page || "1", 10) || 1);
|
||||
const size = 20;
|
||||
|
||||
let announcements: Announcement[] = [];
|
||||
let total = 0;
|
||||
let error = "";
|
||||
try {
|
||||
const res = await fetchAnnouncementsPage(page, size);
|
||||
announcements = res.announcements ?? [];
|
||||
total = res.total ?? 0;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "加载公告失败";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<header className="mb-6">
|
||||
<h1
|
||||
className="text-[24px] sm:text-[28px] font-extrabold tracking-tight flex items-center gap-2"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
<Megaphone size={26} style={{ color: "var(--accent)" }} aria-hidden />
|
||||
站点公告
|
||||
</h1>
|
||||
<p className="meta mt-2">站点通知与运营公告</p>
|
||||
</header>
|
||||
|
||||
{error ? (
|
||||
<p className="alert-error">{error}</p>
|
||||
) : announcements.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="flex flex-col gap-3">
|
||||
{announcements.map((a) => (
|
||||
<AnnouncementCard key={a.id} a={a} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error ? (
|
||||
<Pagination total={total} page={page} size={size} basePath="/announcements" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user