实现博客内容解析
This commit is contained in:
@@ -3,13 +3,11 @@
|
|||||||
import { BlogApi } from "@/lib/api";
|
import { BlogApi } from "@/lib/api";
|
||||||
import { base62 } from "@/lib/utils";
|
import { base62 } from "@/lib/utils";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { unified } from 'unified';
|
import ReactMarkdown from 'react-markdown'
|
||||||
import remarkParse from 'remark-parse';
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkRehype from 'remark-rehype';
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import 'highlight.js/styles/github.css'
|
||||||
import rehypeStringify from 'rehype-stringify';
|
|
||||||
|
|
||||||
export default function Blog() {
|
export default function Blog() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -22,46 +20,36 @@ export default function Blog() {
|
|||||||
hex.slice(20, 32)
|
hex.slice(20, 32)
|
||||||
].join('-');
|
].join('-');
|
||||||
|
|
||||||
const [markdownHTML, setMarkdownHTML] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWR(
|
const { data, error, isLoading } = useSWR(
|
||||||
`/api/blog/${id}`,
|
`/api/blog/${id}`,
|
||||||
() => BlogApi.get(id),
|
() => BlogApi.get(id),
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.content) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const processed = unified()
|
|
||||||
.use(remarkParse) // 解析 Markdown
|
|
||||||
.use(remarkRehype) // 转换为 HTML AST
|
|
||||||
.use(rehypeHighlight) // 高亮代码块
|
|
||||||
.use(rehypeStringify); // 输出 HTML 字符串
|
|
||||||
|
|
||||||
const result = await processed.process(data.content);
|
|
||||||
setMarkdownHTML(result.toString());
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to parse markdown", err);
|
|
||||||
setMarkdownHTML("<p>无法加载内容</p>");
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full overflow-x-hidden">
|
||||||
{data && (
|
{data && (
|
||||||
<div className="w-full flex flex-col items-center">
|
<div className="max-w-200 mx-auto px-5 overflow-x-hidden mb-10">
|
||||||
<h1 className="text-center text-2xl font-semibold mt-5">{data.title}</h1>
|
<h1 className="text-center text-3xl font-bold mt-10">{data.title}</h1>
|
||||||
{/* 渲染 Markdown HTML */}
|
<p className="text-sm text-zinc-500 text-center my-5">发布于:{new Date(data.createdAt).toLocaleString()}</p>
|
||||||
{markdownHTML && (
|
<ReactMarkdown
|
||||||
<div
|
remarkPlugins={[remarkGfm]}
|
||||||
className="prose dark:prose-invert max-w-200 mt-8"
|
rehypePlugins={[rehypeHighlight]}
|
||||||
dangerouslySetInnerHTML={{ __html: markdownHTML }}
|
children={data.content}
|
||||||
/>
|
components={{
|
||||||
)}
|
h1: ({ node, ...props }) => <h1 className="text-3xl font-bold py-1" {...props} />,
|
||||||
<h2 className="text-sm text-zinc-500 text-center mt-1">发布于:{new Date(data.createdAt).toLocaleString()}</h2>
|
h2: ({ node, ...props }) => <h2 className="text-2xl font-bold py-0.5" {...props} />,
|
||||||
|
h3: ({ node, ...props }) => <h3 className="text-xl font-bold" {...props} />,
|
||||||
|
h4: ({ node, ...props }) => <h4 className="text-lg font-bold" {...props} />,
|
||||||
|
h5: ({ node, ...props }) => <h5 className="text-md font-bold" {...props} />,
|
||||||
|
p: ({ node, ...props }) => <p className="py-2 text-zinc-700" {...props} />,
|
||||||
|
img: ({ node, ...props }) => <img className="w-full shadow" {...props} />,
|
||||||
|
th: ({ node, ...props }) => <th className="text-ellipsis text-nowrap border border-zinc-300 p-2" {...props} />,
|
||||||
|
td: ({ node, ...props }) => <td className="border border-zinc-300 p-1" {...props} />,
|
||||||
|
table: ({ node, ...props }) => <div className="overflow-x-auto"><table {...props} /></div>,
|
||||||
|
pre: ({ node, ...props }) => <pre className="rounded-sm overflow-hidden shadow" {...props} />,
|
||||||
|
blockquote: ({ node, ...props }) => <blockquote className="pl-3 border-l-5" {...props} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dialog": "^0.3.1",
|
"dialog": "^0.3.1",
|
||||||
"drawer": "^0.0.2",
|
"drawer": "^0.0.2",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
@@ -39,6 +40,9 @@
|
|||||||
"popover": "^2.4.1",
|
"popover": "^2.4.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"select": "^1.1.2",
|
"select": "^1.1.2",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
|
|||||||
925
tone-page-web/pnpm-lock.yaml
generated
925
tone-page-web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user