feat: 代码片段功能与 Markdown 内嵌片段渲染、文档截图等
This commit is contained in:
124
frontend/components/markdown/MdSnippet.tsx
Normal file
124
frontend/components/markdown/MdSnippet.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user