"use client"; import { useMemo, useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { Search, X, Copy, Eye, MessageSquare, GitFork, Plus, Lock, 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 ( ); })}
)}
{/* 结果统计:纯文本行,切换范围时数据原地更新,无任何加载指示 */}
${" "} {scope === "mine" ? "ls ~/snippets --count" : "ls --count"} 共 {total} 个片段
{/* 列表 / 空态:请求期间保留现有内容,新数据返回后原地替换,不闪空态 */} {items.length === 0 ? ( 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}
)}
); }