| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- import React, { memo, useMemo } from 'react';
- import ReactMarkdown from 'react-markdown';
- import { copyToClipboard } from '../utils/clipboard';
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
- import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
- import remarkGfm from 'remark-gfm';
- import remarkMath from 'remark-math';
- import rehypeKatex from 'rehype-katex';
- import 'katex/dist/katex.min.css'; // KaTeX 样式
- interface MarkdownRendererProps {
- content: string;
- className?: string;
- isStreaming?: boolean; // 是否正在流式输出
- }
- const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(({ content, className = '', isStreaming = false }) => {
- // 流式输出时使用简化渲染,完成后才做完整 Markdown 渲染
- const remarkPlugins = useMemo(() => isStreaming ? [] : [remarkGfm, remarkMath], [isStreaming]);
- const rehypePlugins = useMemo(() => isStreaming ? [] : [rehypeKatex], [isStreaming]);
-
- // 缓存 components 配置,避免每次渲染都创建新对象
- const components = useMemo(() => ({
- // 代码块渲染 - 流式时简化处理
- code({ inline, className, children, ...props }: any) {
- const match = /language-(\w+)/.exec(className || '');
- const language = match ? match[1] : '';
-
- // 流式输出时使用简单代码块,避免频繁高亮计算
- if (isStreaming && !inline && language) {
- return (
- <pre className="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto my-2">
- <code className={`language-${language}`}>{children}</code>
- </pre>
- );
- }
-
- return !inline && language ? (
- <div className="relative group">
- <SyntaxHighlighter
- style={oneDark as any}
- language={language}
- PreTag="div"
- className="rounded-lg !mt-2 !mb-4"
- showLineNumbers={true}
- wrapLines={true}
- {...props}
- >
- {String(children).replace(/\n$/, '')}
- </SyntaxHighlighter>
- <button
- onClick={() => {
- copyToClipboard(String(children));
- }}
- className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-gray-700 hover:bg-gray-600 text-white text-xs px-2 py-1 rounded"
- >
- 复制
- </button>
- </div>
- ) : (
- <code
- className="bg-gray-100 text-red-600 px-1.5 py-0.5 rounded text-sm font-mono"
- {...props}
- >
- {children}
- </code>
- );
- },
-
- // 表格样式
- table({ children }: any) {
- return (
- <div className="overflow-x-auto my-4">
- <table className="min-w-full border-collapse border border-gray-300 rounded-lg overflow-hidden">
- {children}
- </table>
- </div>
- );
- },
-
- thead({ children }: any) {
- return (
- <thead className="bg-gray-50">
- {children}
- </thead>
- );
- },
-
- th({ children }: any) {
- return (
- <th className="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-700">
- {children}
- </th>
- );
- },
-
- td({ children }: any) {
- return (
- <td className="border border-gray-300 px-4 py-2 text-gray-600">
- {children}
- </td>
- );
- },
-
- // 标题样式
- h1({ children }: any) {
- return (
- <h1 className="text-2xl font-bold text-gray-900 mt-6 mb-4 pb-2 border-b border-gray-200">
- {children}
- </h1>
- );
- },
-
- h2({ children }: any) {
- return (
- <h2 className="text-xl font-bold text-gray-900 mt-5 mb-3">
- {children}
- </h2>
- );
- },
-
- h3({ children }: any) {
- return (
- <h3 className="text-lg font-semibold text-gray-900 mt-4 mb-2">
- {children}
- </h3>
- );
- },
-
- h4({ children }: any) {
- return (
- <h4 className="text-base font-semibold text-gray-900 mt-3 mb-2">
- {children}
- </h4>
- );
- },
-
- // 段落样式
- p({ children }: any) {
- return (
- <p className="mb-4 leading-relaxed text-gray-700">
- {children}
- </p>
- );
- },
-
- // 列表样式
- ul({ children }: any) {
- return (
- <ul className="mb-4 ml-6 space-y-1 list-disc">
- {children}
- </ul>
- );
- },
-
- ol({ children }: any) {
- return (
- <ol className="mb-4 ml-6 space-y-1 list-decimal">
- {children}
- </ol>
- );
- },
-
- li({ children }: any) {
- return (
- <li className="text-gray-700 leading-relaxed">
- {children}
- </li>
- );
- },
-
- // 引用样式
- blockquote({ children }: any) {
- return (
- <blockquote className="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-blue-50 text-gray-700 italic">
- {children}
- </blockquote>
- );
- },
-
- // 链接样式
- a({ href, children }: any) {
- return (
- <a
- href={href}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-600 hover:text-blue-800 underline"
- >
- {children}
- </a>
- );
- },
-
- // 强调样式
- strong({ children }: any) {
- return (
- <strong className="font-semibold text-gray-900">
- {children}
- </strong>
- );
- },
-
- em({ children }: any) {
- return (
- <em className="italic text-gray-700">
- {children}
- </em>
- );
- },
-
- // 分割线
- hr() {
- return (
- <hr className="my-6 border-t border-gray-300" />
- );
- },
- }), [isStreaming]);
- return (
- <div className={`markdown-content ${className}`}>
- <ReactMarkdown
- remarkPlugins={remarkPlugins}
- rehypePlugins={rehypePlugins}
- components={components}
- >
- {content}
- </ReactMarkdown>
- </div>
- );
- });
- export default MarkdownRenderer;
|