feat(ui): 添加 Markdown 渲染和代码高亮功能

- 新增 CodeBlock 组件,使用 Shiki 语法高亮
- 新增 Markdown 组件,支持 GFM 语法
- AI 消息自动渲染 Markdown,用户消息保持原样
- 代码块支持一键复制和语言标签显示
This commit is contained in:
2025-12-12 17:32:25 +08:00
parent cbbe9c7af1
commit f561687307
6 changed files with 1318 additions and 7 deletions
+170
View File
@@ -0,0 +1,170 @@
/**
* Markdown Component
*
* Markdown 渲染组件,支持 GFM 语法和代码高亮
*/
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { CodeBlock, InlineCode } from './CodeBlock';
import { cn } from '../utils/cn';
interface MarkdownProps {
content: string;
className?: string;
}
export function Markdown({ content, className }: MarkdownProps) {
return (
<div className={cn('markdown-content', className)}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 代码块和内联代码
code({ className, children, node, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const codeContent = String(children).replace(/\n$/, '');
// 检查是否为内联代码:如果父节点不是 pre,则为内联代码
const isInline = node?.position?.start.line === node?.position?.end.line &&
!codeContent.includes('\n');
if (isInline && !language) {
return <InlineCode {...props}>{children}</InlineCode>;
}
return (
<CodeBlock
code={codeContent}
language={language || 'text'}
/>
);
},
// 段落
p({ children }) {
return <p className="mb-4 last:mb-0">{children}</p>;
},
// 标题
h1({ children }) {
return <h1 className="text-2xl font-bold mb-4 mt-6 first:mt-0">{children}</h1>;
},
h2({ children }) {
return <h2 className="text-xl font-bold mb-3 mt-5 first:mt-0">{children}</h2>;
},
h3({ children }) {
return <h3 className="text-lg font-bold mb-2 mt-4 first:mt-0">{children}</h3>;
},
h4({ children }) {
return <h4 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h4>;
},
// 列表
ul({ children }) {
return <ul className="list-disc list-inside mb-4 space-y-1">{children}</ul>;
},
ol({ children }) {
return <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>;
},
li({ children }) {
return <li className="text-gray-200">{children}</li>;
},
// 引用
blockquote({ children }) {
return (
<blockquote className="border-l-4 border-gray-600 pl-4 my-4 text-gray-400 italic">
{children}
</blockquote>
);
},
// 链接
a({ href, children }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary-400 hover:text-primary-300 underline underline-offset-2"
>
{children}
</a>
);
},
// 强调
strong({ children }) {
return <strong className="font-bold text-gray-100">{children}</strong>;
},
em({ children }) {
return <em className="italic">{children}</em>;
},
// 分割线
hr() {
return <hr className="my-6 border-gray-700" />;
},
// 表格
table({ children }) {
return (
<div className="overflow-x-auto my-4">
<table className="min-w-full border-collapse border border-gray-700">
{children}
</table>
</div>
);
},
thead({ children }) {
return <thead className="bg-gray-800">{children}</thead>;
},
tbody({ children }) {
return <tbody>{children}</tbody>;
},
tr({ children }) {
return <tr className="border-b border-gray-700">{children}</tr>;
},
th({ children }) {
return (
<th className="px-4 py-2 text-left text-sm font-semibold text-gray-200 border-r border-gray-700 last:border-r-0">
{children}
</th>
);
},
td({ children }) {
return (
<td className="px-4 py-2 text-sm text-gray-300 border-r border-gray-700 last:border-r-0">
{children}
</td>
);
},
// 图片
img({ src, alt }) {
return (
<img
src={src}
alt={alt || ''}
className="max-w-full h-auto rounded-lg my-4"
/>
);
},
// 预格式化文本(非代码块的 pre)
pre({ children }) {
return (
<pre className="bg-gray-900 p-4 rounded-lg overflow-x-auto my-4 text-sm">
{children}
</pre>
);
},
}}
>
{content}
</ReactMarkdown>
</div>
);
}