feat(blog): Markdown 渲染组件(react-markdown + 暖色编辑风)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-27 16:52:07 +08:00
parent d25c65ef81
commit f89345b6fa
3 changed files with 1540 additions and 7 deletions

1476
Client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,9 @@
"@fontsource/hanken-grotesk": "^5.2.8", "@fontsource/hanken-grotesk": "^5.2.8",
"framer-motion": "^12.40.0", "framer-motion": "^12.40.0",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6" "react-dom": "^19.2.6",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",

View File

@ -0,0 +1,67 @@
import ReactMarkdown, { type Components } from 'react-markdown'
import remarkGfm from 'remark-gfm'
// 把 Markdown 渲染套上站点暖色编辑风。只渲染自有可信内容;
// react-markdown 默认不解析原始 HTML未引 rehype-raw天然防注入。
const components: Components = {
h1: ({ children }) => (
<h1 className="mt-8 mb-4 font-display text-3xl font-medium text-text">{children}</h1>
),
h2: ({ children }) => (
<h2 className="mt-7 mb-3 font-display text-2xl font-medium text-text">{children}</h2>
),
h3: ({ children }) => (
<h3 className="mt-6 mb-2 font-display text-xl font-medium text-text">{children}</h3>
),
p: ({ children }) => <p className="my-4 leading-relaxed text-muted">{children}</p>,
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noreferrer"
className="text-accent underline-offset-4 hover:underline"
>
{children}
</a>
),
ul: ({ children }) => (
<ul className="my-4 list-disc pl-6 text-muted marker:text-accent/60">{children}</ul>
),
ol: ({ children }) => (
<ol className="my-4 list-decimal pl-6 text-muted marker:text-accent/60">{children}</ol>
),
li: ({ children }) => <li className="my-1 leading-relaxed">{children}</li>,
blockquote: ({ children }) => (
<blockquote className="my-4 border-l-2 border-accent pl-4 italic text-muted/90">
{children}
</blockquote>
),
pre: ({ children }) => <pre className="my-4">{children}</pre>,
code: ({ className, children }) => {
const isBlock = /language-/.test(className ?? '')
return isBlock ? (
<code className="block overflow-x-auto rounded-lg border border-line bg-surface px-4 py-3 font-mono text-sm text-text">
{children}
</code>
) : (
<code className="rounded bg-surface px-1.5 py-0.5 font-mono text-[0.85em] text-accent-soft">
{children}
</code>
)
},
img: ({ src, alt }) => (
<img src={typeof src === 'string' ? src : ''} alt={alt ?? ''} className="my-4 max-w-full rounded-lg" />
),
hr: () => <hr className="my-8 border-line" />,
}
export function Markdown({ children }: { children: string }) {
// data-selectable全站默认禁选正文区放开以便复制。
return (
<div data-selectable>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
{children}
</ReactMarkdown>
</div>
)
}