feat: 代码片段功能与 Markdown 内嵌片段渲染、文档截图等

This commit is contained in:
2026-10-09 18:53:38 +08:00
parent 5598e1cbb7
commit da72843c70
64 changed files with 7047 additions and 248 deletions

View File

@@ -0,0 +1,124 @@
import Link from "next/link";
import { CodeXml, Lock, Copy, Eye, MessageSquare, GitFork } from "lucide-react";
import { fetchSnippetById } from "@/lib/api";
import { postViewerCookieHeader } from "@/lib/cookies";
import { getPublicSettingsCached } from "@/lib/serverData";
import { cookies } from "next/headers";
import { snippetPath } from "@/lib/urlStyle";
import Avatar from "@/components/Avatar";
/**
* [snippet id=N] 嵌入卡片(SSR)。
* 私有/已删除/不存在时渲染降级占位,不暴露内容。
*/
export default async function MdSnippet({ id }: { id: number }) {
const cookieHeader = postViewerCookieHeader(await cookies());
const [data, settings] = await Promise.all([
fetchSnippetById(id, cookieHeader).catch(() => null),
getPublicSettingsCached(),
]);
const urlStyle = settings.url_style;
// 降级占位:不存在 / 私有且无权限 / 已删除
if (!data) {
return (
<div
className="my-4 rounded-xl px-4 py-4 flex items-center gap-3"
style={{ background: "var(--panel-2)", border: "1px solid var(--line)" }}
>
<span
className="w-9 h-9 flex items-center justify-center rounded-lg shrink-0"
style={{ background: "var(--panel)", color: "var(--ink-3)" }}
>
<Lock size={16} />
</span>
<div className="min-w-0">
<p className="text-[13px] font-medium" style={{ color: "var(--ink-2)" }}>
代码片段 #{id}
</p>
<p className="meta text-[11.5px]">片段不存在或无权查看</p>
</div>
</div>
);
}
const s = data.snippet;
const ownerName = s.user ? s.user.nickname || s.user.username : "";
const previewLines = s.code.split("\n").slice(0, 5);
const href = snippetPath(urlStyle, s.id);
return (
<Link href={href} className="group block my-4">
<div
className="rounded-xl overflow-hidden transition-[box-shadow,transform] duration-200 group-hover:-translate-y-0.5 group-hover:shadow-[var(--shadow-md)]"
style={{ border: "1px solid var(--line)", background: "var(--panel)" }}
>
{/* 头部:语言 + 标题 + 作者 */}
<div className="flex items-center gap-2.5 px-4 py-3" style={{ borderBottom: "1px solid var(--line)" }}>
<span
className="w-8 h-8 flex items-center justify-center rounded-lg shrink-0"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<CodeXml size={15} />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span
className="text-[10px] font-semibold px-1.5 py-0.5 rounded uppercase tracking-wide shrink-0"
style={{
color: "var(--accent)",
background: "var(--accent-soft)",
border: "1px solid color-mix(in srgb, var(--accent) 30%, transparent)",
}}
>
{s.language}
</span>
<p className="text-[14px] font-semibold truncate" style={{ color: "var(--ink)" }}>
{s.title}
</p>
</div>
{s.user && (
<span className="flex items-center gap-1.5 mt-0.5">
<Avatar size={14} name={ownerName} src={s.user.avatar} />
<span className="meta text-[11px] truncate">{ownerName}</span>
</span>
)}
</div>
</div>
{/* 代码预览 */}
<div className="px-4 py-3" style={{ background: "var(--panel-2)" }}>
<pre
className="text-[12px] leading-[1.6] overflow-hidden"
style={{ color: "var(--ink-2)", fontFamily: "var(--font-mono, ui-monospace, monospace)" }}
>
{previewLines.map((line, i) => (
<div key={i} className="flex gap-2 min-w-0">
<span className="shrink-0 w-4 text-right select-none" style={{ color: "var(--ink-3)", opacity: 0.5 }}>
{i + 1}
</span>
<span className="truncate">{line || " "}</span>
</div>
))}
</pre>
</div>
{/* 底部统计 */}
<div className="flex items-center gap-3 px-4 py-2.5 meta text-[11px]">
<span className="inline-flex items-center gap-1">
<Copy size={11} /> {s.copy_count}
</span>
<span className="inline-flex items-center gap-1">
<Eye size={11} /> {s.view_count}
</span>
<span className="inline-flex items-center gap-1">
<MessageSquare size={11} /> {s.comment_count}
</span>
<span className="inline-flex items-center gap-1">
<GitFork size={11} /> {s.fork_count}
</span>
</div>
</div>
</Link>
);
}