"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { CodeXml, Copy, Eye, EyeOff, GitFork, Loader2, MessagesSquare, Trash2 } from "lucide-react"; import { AdminEmpty, AdminPageHeader, AdminPager, AdminSearchField, AdminSegmented, } from "@/components/admin"; import ConfirmDialog from "@/components/ConfirmDialog"; import Avatar from "@/components/Avatar"; import { apiAdminDeleteSnippet, apiAdminSetSnippetVisibility, apiAdminSnippets, type SnippetListItem, } from "@/lib/api"; import { formatRelative } from "@/lib/format"; import { toast } from "@/lib/toast"; import { snippetPath } from "@/lib/urlStyle"; import { useUrlStyle } from "@/components/SiteBrandProvider"; type VisibilityFilter = "all" | "public" | "private"; const PAGE_SIZE = 20; /** 片段管理:全量列表(含私有),支持搜索、可见性筛选、下架/上架与删除。 */ export default function SnippetsAdmin() { const urlStyle = useUrlStyle(); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [vis, setVis] = useState("all"); const [keyword, setKeyword] = useState(""); const [q, setQ] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [confirm, setConfirm] = useState< | { kind: "delete"; item: SnippetListItem } | { kind: "hide" | "show"; item: SnippetListItem } | null >(null); const [busy, setBusy] = useState(false); const load = useCallback(async (p: number, v: VisibilityFilter, kw: string) => { setLoading(true); setError(""); try { const res = await apiAdminSnippets({ q: kw || undefined, visibility: v === "all" ? undefined : v, page: p, size: PAGE_SIZE, }); setItems(res.items); setTotal(res.total); setPage(p); } catch (e) { setError(e instanceof Error ? e.message : "加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { void load(1, "all", ""); }, [load]); const runConfirm = async () => { if (!confirm) return; setBusy(true); try { if (confirm.kind === "delete") { await apiAdminDeleteSnippet(confirm.item.id); toast("片段已删除", "ok"); } else { const next = confirm.kind === "hide" ? "private" : "public"; await apiAdminSetSnippetVisibility(confirm.item.id, next); toast(next === "private" ? "已下架为私有" : "已恢复为公开", "ok"); } setConfirm(null); await load(page, vis, q); } catch (e) { toast(e instanceof Error ? e.message : "操作失败"); } finally { setBusy(false); } }; return (
ariaLabel="可见性筛选" size="sm" value={vis} onChange={(v) => void load(1, v, q)} options={[ { key: "all", label: "全部" }, { key: "public", label: "公开" }, { key: "private", label: "私有" }, ]} /> { setQ(keyword.trim()); void load(1, vis, keyword.trim()); }} onClear={() => { setKeyword(""); setQ(""); void load(1, vis, ""); }} placeholder="搜索标题或描述…" ariaLabel="搜索片段" />
{error ?

{error}

: null} {loading && items.length === 0 ? (
) : items.length === 0 ? ( } title="暂无片段" description={q ? "没有匹配搜索条件的片段。" : "还没有用户发布代码片段。"} /> ) : (
    {items.map((it) => (
  • {it.title} {it.language} {it.visibility === "private" ? ( 私有 ) : ( 公开 )}
    {it.user ? ( {it.user.nickname} ) : ( 用户 #{it.user_id} )} {it.copy_count} {it.view_count} {it.comment_count} {it.fork_count} {formatRelative(it.created_at)}
    {it.visibility === "public" ? ( ) : ( )}
  • ))}
)} void load(p, vis, q)} />
!busy && setConfirm(null)} onConfirm={() => void runConfirm()} />
); }