- 移除 ChatInput 和 MessageBubble 中的固定字体大小类 - MarkdownRenderer 标题和内容使用 em 相对单位 - 表格字体大小改为相对单位保持比例 - 聊天输入框添加 z-20 层级避免被代码块遮挡
223 lines
5.3 KiB
TypeScript
223 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { memo } from 'react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import { CodeBlock } from './CodeBlock';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface MarkdownRendererProps {
|
|
content: string;
|
|
className?: string;
|
|
}
|
|
|
|
// 将 components 配置提取到组件外部,避免每次渲染时创建新对象
|
|
const markdownComponents = {
|
|
// 代码块
|
|
code({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
|
|
const match = /language-(\w+)/.exec(className || '');
|
|
const isInline = !match && !className;
|
|
|
|
if (isInline) {
|
|
// 行内代码 - 无特殊样式,仅等宽字体
|
|
return (
|
|
<code
|
|
className="font-mono"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</code>
|
|
);
|
|
}
|
|
|
|
// 代码块
|
|
return (
|
|
<CodeBlock
|
|
code={String(children).replace(/\n$/, '')}
|
|
language={match ? match[1] : 'text'}
|
|
/>
|
|
);
|
|
},
|
|
|
|
// 段落
|
|
p({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<p className="mb-3 last:mb-0 leading-[1.7]">
|
|
{children}
|
|
</p>
|
|
);
|
|
},
|
|
|
|
// 标题 - 使用相对单位保持与全局字体的比例
|
|
h1({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<h1 className="text-[1.25em] font-bold mt-5 mb-3 text-[var(--color-text-primary)]">
|
|
{children}
|
|
</h1>
|
|
);
|
|
},
|
|
h2({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<h2 className="text-[1.125em] font-bold mt-4 mb-2 text-[var(--color-text-primary)]">
|
|
{children}
|
|
</h2>
|
|
);
|
|
},
|
|
h3({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<h3 className="text-[1em] font-semibold mt-3 mb-2 text-[var(--color-text-primary)]">
|
|
{children}
|
|
</h3>
|
|
);
|
|
},
|
|
h4({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<h4 className="text-[1em] font-semibold mt-2 mb-1 text-[var(--color-text-primary)]">
|
|
{children}
|
|
</h4>
|
|
);
|
|
},
|
|
|
|
// 列表
|
|
ul({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<ul className="list-disc pl-5 my-2 space-y-1 marker:text-[#E06B3E]">
|
|
{children}
|
|
</ul>
|
|
);
|
|
},
|
|
ol({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<ol className="list-decimal pl-5 my-2 space-y-1 marker:text-[#E06B3E]">
|
|
{children}
|
|
</ol>
|
|
);
|
|
},
|
|
li({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<li className="leading-relaxed">
|
|
{children}
|
|
</li>
|
|
);
|
|
},
|
|
|
|
// 链接
|
|
a({ href, children }: { href?: string; children?: React.ReactNode }) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-[var(--color-primary)] hover:underline"
|
|
>
|
|
{children}
|
|
</a>
|
|
);
|
|
},
|
|
|
|
// 粗体
|
|
strong({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<strong className="font-semibold">
|
|
{children}
|
|
</strong>
|
|
);
|
|
},
|
|
|
|
// 斜体
|
|
em({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<em className="italic">
|
|
{children}
|
|
</em>
|
|
);
|
|
},
|
|
|
|
// 引用
|
|
blockquote({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<blockquote className="border-l-3 border-[var(--color-primary)] pl-3 my-3 italic text-[var(--color-text-secondary)]">
|
|
{children}
|
|
</blockquote>
|
|
);
|
|
},
|
|
|
|
// 分割线
|
|
hr() {
|
|
return (
|
|
<hr className="my-4 border-[var(--color-border)]" />
|
|
);
|
|
},
|
|
|
|
// 表格
|
|
table({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<div className="overflow-x-auto my-3">
|
|
<table className="min-w-full border border-[var(--color-border)] rounded-lg overflow-hidden text-[0.9em]">
|
|
{children}
|
|
</table>
|
|
</div>
|
|
);
|
|
},
|
|
thead({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<thead className="bg-[var(--color-bg-tertiary)]">
|
|
{children}
|
|
</thead>
|
|
);
|
|
},
|
|
tbody({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<tbody className="divide-y divide-[var(--color-border)]">
|
|
{children}
|
|
</tbody>
|
|
);
|
|
},
|
|
tr({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<tr className="hover:bg-[var(--color-bg-hover)] transition-colors">
|
|
{children}
|
|
</tr>
|
|
);
|
|
},
|
|
th({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<th className="px-3 py-1.5 text-left text-[0.85em] font-semibold text-[var(--color-text-primary)]">
|
|
{children}
|
|
</th>
|
|
);
|
|
},
|
|
td({ children }: { children?: React.ReactNode }) {
|
|
return (
|
|
<td className="px-3 py-1.5 text-[0.85em] text-[var(--color-text-secondary)]">
|
|
{children}
|
|
</td>
|
|
);
|
|
},
|
|
|
|
// 图片
|
|
img(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
|
return (
|
|
<img
|
|
{...props}
|
|
alt={props.alt || ''}
|
|
className="max-w-full h-auto rounded-lg my-3"
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// 使用 memo 包裹组件,避免不必要的重渲染
|
|
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
|
|
return (
|
|
<div className={cn('markdown-content', className)}>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
components={markdownComponents}
|
|
>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
);
|
|
});
|