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>
|
||||
);
|
||||
}
|
||||
@@ -14,14 +14,36 @@ export const SHIKI_LANG_IDS = [
|
||||
"tsx",
|
||||
"jsx",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"ini",
|
||||
"bash",
|
||||
"sql",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"diff",
|
||||
"rust",
|
||||
"ruby",
|
||||
"powershell",
|
||||
"nginx",
|
||||
"dockerfile",
|
||||
"kotlin",
|
||||
"java",
|
||||
"c",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"php",
|
||||
"swift",
|
||||
"dart",
|
||||
"lua",
|
||||
"vue",
|
||||
"graphql",
|
||||
"xml",
|
||||
"regex",
|
||||
"makefile",
|
||||
"prisma",
|
||||
"plaintext",
|
||||
] as const;
|
||||
|
||||
@@ -38,8 +60,44 @@ export const SHIKI_LANG_ALIAS: Record<string, string> = {
|
||||
yml: "yaml",
|
||||
text: "plaintext",
|
||||
txt: "plaintext",
|
||||
golang: "go",
|
||||
rb: "ruby",
|
||||
ps1: "powershell",
|
||||
pwsh: "powershell",
|
||||
"c++": "cpp",
|
||||
cs: "csharp",
|
||||
"c#": "csharp",
|
||||
regexp: "regex",
|
||||
docker: "dockerfile",
|
||||
};
|
||||
|
||||
/**
|
||||
* 白名单 id → @shikijs/langs grammar 内部注册名。
|
||||
* 部分语言的 dist 文件只是别名重导出(如 bash.mjs = shellscript.mjs),
|
||||
* HighlighterCore 按内部名注册,codeToHtml(lang:"bash") 会静默降级 plaintext。
|
||||
*/
|
||||
const INTERNAL_LANG_ALIAS: Record<string, string> = {
|
||||
bash: "shellscript",
|
||||
sh: "shellscript",
|
||||
shell: "shellscript",
|
||||
zsh: "shellscript",
|
||||
dockerfile: "docker",
|
||||
regex: "regexp",
|
||||
makefile: "make",
|
||||
};
|
||||
|
||||
/**
|
||||
* createHighlighterCore 的 langAlias:输入别名 + 白名单 id 都要能落到 grammar 内部名。
|
||||
* 注意 core 会校验别名链无环,且 docker/regexp 本身就是内部注册名,
|
||||
* 不能再作为 alias 键(会与 dockerfile→docker、regex→regexp 互指成环)。
|
||||
*/
|
||||
function buildCoreLangAlias(): Record<string, string> {
|
||||
const merged = { ...SHIKI_LANG_ALIAS, ...INTERNAL_LANG_ALIAS };
|
||||
delete merged["docker"];
|
||||
delete merged["regexp"];
|
||||
return merged;
|
||||
}
|
||||
|
||||
export const SHIKI_THEMES = {
|
||||
light: "github-light",
|
||||
dark: "github-dark-dimmed",
|
||||
@@ -84,10 +142,30 @@ export function getSiteHighlighter(): Promise<HighlighterCore> {
|
||||
import("@shikijs/langs/css"),
|
||||
import("@shikijs/langs/scss"),
|
||||
import("@shikijs/langs/diff"),
|
||||
import("@shikijs/langs/rust"),
|
||||
import("@shikijs/langs/ruby"),
|
||||
import("@shikijs/langs/powershell"),
|
||||
import("@shikijs/langs/nginx"),
|
||||
import("@shikijs/langs/dockerfile"),
|
||||
import("@shikijs/langs/kotlin"),
|
||||
import("@shikijs/langs/java"),
|
||||
import("@shikijs/langs/c"),
|
||||
import("@shikijs/langs/cpp"),
|
||||
import("@shikijs/langs/csharp"),
|
||||
import("@shikijs/langs/php"),
|
||||
import("@shikijs/langs/swift"),
|
||||
import("@shikijs/langs/dart"),
|
||||
import("@shikijs/langs/lua"),
|
||||
import("@shikijs/langs/vue"),
|
||||
import("@shikijs/langs/graphql"),
|
||||
import("@shikijs/langs/xml"),
|
||||
import("@shikijs/langs/regex"),
|
||||
import("@shikijs/langs/makefile"),
|
||||
import("@shikijs/langs/prisma"),
|
||||
// plaintext / text / txt 为 Shiki 特殊语言,无需 grammar
|
||||
aardioGrammar,
|
||||
],
|
||||
langAlias: SHIKI_LANG_ALIAS,
|
||||
langAlias: buildCoreLangAlias(),
|
||||
engine: createOnigurumaEngine(import("shiki/wasm")),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user