"use client"; import { useMemo, useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { Search, X, Copy, Eye, MessageSquare, GitFork, Plus, Lock, Loader2, ArrowLeft, ArrowRight, RotateCcw, } from "lucide-react"; import type { SnippetListItem, SnippetSort, SnippetStats } from "@/lib/api"; import { fetchSnippets, apiMySnippets } from "@/lib/api"; import { snippetPath, type UrlStyle } from "@/lib/urlStyle"; import { formatRelative } from "@/lib/format"; import Avatar from "@/components/Avatar"; import { toast } from "@/lib/toast"; import { siteCodeToHtml } from "@/components/markdown/shikiHighlighter"; import SnippetLangSelect from "@/components/SnippetLangSelect"; import SnippetHero from "@/components/SnippetHero"; import { snippetLangColor } from "@/lib/snippetLangColors"; const MONO = "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)"; function plainExcerpt(md: string): string { return md .replace(/!\[[^\]]*\]\([^)]*\)/g, "") .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") .replace(/[*_~`#>]+/g, "") .replace(/\s+/g, " ") .trim(); } /** 编辑器窗格的三色圆点(灰阶令牌,明暗主题都不穿帮) */ function WindowDots() { return ( ); } /** 语言标识:小圆点 + 等宽语言名 */ function LangTag({ lang }: { lang: string }) { const c = snippetLangColor(lang); return ( {lang} ); } /** * 从 shiki 输出提取高亮信息: * - 双主题 CSS 变量定义在
上(--shiki-light/--shiki-dark …),必须保留
* - 内部按行拆分自绘 gutter
*/
function extractShiki(html: string): { vars: Record; lines: string[] } | null {
const codeM = /]*>([\s\S]*?)<\/code>/i.exec(html);
if (!codeM) return null;
const vars: Record = {};
const preM = /]*\sstyle="([^"]*)"[^>]*>/i.exec(html);
if (preM) {
for (const m of preM[1].matchAll(/(--[\w-]+)\s*:\s*([^;]+)/g)) {
vars[m[1]] = m[2].trim();
}
}
return { vars, lines: codeM[1].split("\n") };
}
const PREVIEW_LINES = 5;
/** 代码预览:shiki 客户端高亮 + 自绘行号 gutter;未着色前回退纯文本 */
function SnippetPreview({ code, language }: { code: string; language: string }) {
const [hl, setHl] = useState<{ vars: Record; lines: string[] } | null>(null);
const plainLines = useMemo(() => code.split("\n").slice(0, PREVIEW_LINES), [code]);
const longPreview = code.split("\n").length > PREVIEW_LINES;
useEffect(() => {
let cancelled = false;
const head = code.split("\n").slice(0, PREVIEW_LINES).join("\n");
siteCodeToHtml(head, language)
.then((html) => {
if (cancelled) return;
const parsed = extractShiki(html);
// shiki 末行通常带尾换行产生空串,截到预览行数
if (parsed && parsed.lines.length > 0) {
setHl({ vars: parsed.vars, lines: parsed.lines.slice(0, PREVIEW_LINES) });
}
})
.catch(() => {
/* 保持纯文本 */
});
return () => {
cancelled = true;
};
}, [code, language]);
const rows = hl?.lines ?? plainLines;
const highlighted = hl !== null;
return (
{rows.map((ln, i) => (
{i + 1}
{highlighted ? (
) : (
{ln || " "}
)}
))}
{longPreview ? (
) : null}
);
}
/** 自定义语言下拉:见共享组件 SnippetLangSelect */
function Stat({ icon, value, label }: { icon: React.ReactNode; value: number; label: string }) {
return (
{icon}
{value}
);
}
function SnippetCard({ item, urlStyle }: { item: SnippetListItem; urlStyle: UrlStyle }) {
const ownerName = item.user ? item.user.nickname || item.user.username : "";
const excerpt = plainExcerpt(item.description ?? "");
const href = snippetPath(urlStyle, item.id);
const langColor = snippetLangColor(item.language);
return (
{/* 卡头:语言 + 标题 + 徽标 */}
{item.visibility === "private" && (
私有
)}
{item.forked_from_id ? (
Fork
) : null}
{item.title}
{excerpt ? (
{excerpt}
) : null}
{/* 代码窗格:标题栏 + gutter + shiki 高亮 */}
snippet.{item.language === "plaintext" ? "txt" : item.language}
{item.language}
{/* 卡脚:统计 + 作者 */}
} value={item.copy_count} label="复制数" />
} value={item.view_count} label="浏览数" />
} value={item.comment_count} label="评论数" />
} value={item.fork_count} label="Fork 数" />
{item.user ? (
<>
{ownerName}
·
>
) : null}
{formatRelative(item.created_at)}
);
}
const SORT_OPTIONS: { value: SnippetSort; label: string }[] = [
{ value: "latest", label: "最新" },
{ value: "copy", label: "复制" },
{ value: "comment", label: "评论" },
{ value: "fork", label: "Fork" },
];
export default function SnippetExplorer({
initialItems,
initialTotal,
languages,
urlStyle,
stats,
isLoggedIn,
}: {
initialItems: SnippetListItem[];
initialTotal: number;
languages: string[];
urlStyle: UrlStyle;
stats: SnippetStats | null;
isLoggedIn: boolean;
}) {
const [items, setItems] = useState(initialItems);
const [total, setTotal] = useState(initialTotal);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState("");
const [lang, setLang] = useState("");
const [sort, setSort] = useState("latest");
const [scope, setScope] = useState<"public" | "mine">("public");
const pageSize = 12;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const filtering = !!query || (scope === "public" && !!lang);
const load = useCallback(
async (p: number) => {
setLoading(true);
try {
const res =
scope === "mine"
? await apiMySnippets(query, p, pageSize)
: await fetchSnippets({ q: query, lang, sort, page: p, size: pageSize });
setItems(res.items);
setTotal(res.total);
setPage(p);
} catch (e) {
toast(e instanceof Error ? e.message : "加载失败");
} finally {
setLoading(false);
}
},
[scope, query, lang, sort]
);
// 筛选条件变化时回到第一页
useEffect(() => {
const t = setTimeout(() => load(1), 300);
return () => clearTimeout(t);
}, [load]);
const switchScope = (next: "public" | "mine") => {
if (next === scope) return;
setScope(next);
setQuery("");
setLang("");
setSort("latest");
};
return (
{/* 未来科技感 Hero:网格 + 扫描线 + 统计矩阵 */}
{/* 工具台:IDE 风格软底工具条,控件浮于其上(无框中框) */}
{isLoggedIn && (
{(
[
{ value: "public", label: "广场" },
{ value: "mine", label: "我的" },
] as const
).map((opt) => {
const active = scope === opt.value;
return (
);
})}
)}
setQuery(e.target.value)}
placeholder={scope === "mine" ? "搜索我的片段…" : "搜索标题、描述或代码…"}
aria-label="搜索片段"
className="j13-sn-search w-full h-9 rounded-lg pr-8 text-[13px]"
style={{ background: "var(--panel)", border: "1px solid transparent", color: "var(--ink)", paddingLeft: 34 }}
/>
{query ? (
) : null}
{scope === "public" && (
)}
{scope === "public" && (
{SORT_OPTIONS.map((opt) => {
const active = sort === opt.value;
return (
);
})}
)}
{/* 结果统计 */}
{loading ? (
) : (
${" "}
{scope === "mine" ? "ls ~/snippets --count" : "ls --count"}
)}
共 {total} 个片段
{loading ? ",加载中…" : ""}
{/* 列表 / 空态 */}
{items.length === 0 && loading ? null : items.length === 0 && !loading ? (
filtering ? (
没有找到匹配的片段
) : (
snippets — zsh
${" "}
{scope === "mine" ? "ls ~/snippets" : "snippets list --public"}
# {scope === "mine" ? "你还没有片段,新建一个或从广场 Fork" : "还没有公开片段"}
▍
{scope === "mine" ? "Fork 的副本会默认设为私有,只在这里可见" : "成为第一个分享代码片段的人"}
{scope === "mine" ? (
<>
新建片段
>
) : isLoggedIn ? (
新建片段
) : (
登录后分享
)}
)
) : (
{items.map((item) => (
))}
)}
{/* 分页 */}
{totalPages > 1 && (
{page} / {totalPages}
)}
);
}