"use client"; import { useCallback, useMemo, useState } from "react"; import Link from "next/link"; import { Plus, Pencil, Trash2, Loader2, ExternalLink, Megaphone, Pin, } from "lucide-react"; import { apiAdminListAnnouncements, apiAdminDeleteAnnouncement, apiAdminToggleAnnouncementPin, type Announcement, } from "@/lib/api"; import { formatRelative } from "@/lib/format"; import { toast } from "@/lib/toast"; import ConfirmDialog from "@/components/ConfirmDialog"; import { AdminEmpty, AdminIconButton, AdminPageHeader, AdminSegmented, AdminStatusChip, } from "@/components/admin"; type StatusFilter = "all" | "published" | "draft"; /** 粗略去掉 Markdown,取一行摘要 */ function plainSummary(md: string, max = 96): 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; } export default function AnnouncementAdmin({ initial, initError, }: { initial: Announcement[]; initError: string; }) { const [list, setList] = useState(initial); const [loadError, setLoadError] = useState(initError); const [statusFilter, setStatusFilter] = useState("all"); const [pinningId, setPinningId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const publishedCount = useMemo(() => list.filter((a) => a.published).length, [list]); const draftCount = useMemo(() => list.filter((a) => !a.published).length, [list]); const filtered = useMemo(() => { if (statusFilter === "published") return list.filter((a) => a.published); if (statusFilter === "draft") return list.filter((a) => !a.published); return list; }, [list, statusFilter]); const load = useCallback(async () => { try { const res = await apiAdminListAnnouncements(); setList(res.announcements); setLoadError(""); } catch (e) { setLoadError(e instanceof Error ? e.message : "加载公告失败"); } }, []); async function togglePin(a: Announcement) { if (pinningId != null) return; setPinningId(a.id); try { const res = await apiAdminToggleAnnouncementPin(a.id); setList((prev) => prev.map((x) => (x.id === a.id ? res.announcement : x))); toast(res.announcement.pinned ? "已置顶" : "已取消置顶", "ok"); // 置顶后列表顺序可能变化,重新拉一次对齐后端排序 await load(); } catch (e) { toast(e instanceof Error ? e.message : "置顶失败"); } finally { setPinningId(null); } } async function remove(a: Announcement) { setDeletingId(a.id); try { await apiAdminDeleteAnnouncement(a.id); setList((l) => l.filter((x) => x.id !== a.id)); toast("公告已删除", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "删除失败"); } finally { setDeletingId(null); setDeleteTarget(null); } } return (
新建公告 } />
ariaLabel="公告状态筛选" size="sm" value={statusFilter} onChange={setStatusFilter} options={[ { key: "all", label: "全部", count: list.length }, { key: "published", label: "已发布", count: publishedCount }, { key: "draft", label: "草稿", count: draftCount }, ]} />
公告列表 共 {filtered.length} 条
{loadError ? (

{loadError}

) : filtered.length === 0 ? ( } title={list.length === 0 ? "还没有公告" : "该状态下暂无公告"} description={ list.length === 0 ? "发布第一条站点公告,会出现在首页右栏" : "试试切换筛选条件" } /> ) : ( filtered.map((a, idx) => { const summary = plainSummary(a.content); return (
{a.pinned ? ( 置顶 ) : null}

{a.title}

{a.tag} {a.published ? "已发布" : "草稿"} {formatRelative(a.updated_at || a.created_at)}
{summary ? (

{summary}

) : null}
void togglePin(a)} > {pinningId === a.id ? ( ) : ( )} {a.published ? ( ) : null} setDeleteTarget(a)} > {deletingId === a.id ? ( ) : ( )}
); }) )} {list.length === 0 && !loadError ? (
新建第一条公告
) : null}
deletingId == null && setDeleteTarget(null)} onConfirm={() => { if (deleteTarget) void remove(deleteTarget); }} />
); }