'use client';
import { memo, useMemo } 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;
/** 图片链接点击回调,用于在灯箱中打开图片 */
onImageLinkClick?: (url: string) => void;
}
/**
* 判断 URL 是否为图片链接
*/
function isImageUrl(url: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.ico'];
const urlLower = url.toLowerCase();
// 检查扩展名(忽略查询参数)
const urlWithoutQuery = urlLower.split('?')[0];
return imageExtensions.some(ext => urlWithoutQuery.endsWith(ext));
}
/**
* 创建 Markdown 组件配置
* 使用工厂函数以支持传入回调
*/
function createMarkdownComponents(onImageLinkClick?: (url: string) => void) {
return {
// 代码块
code({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
const match = /language-(\w+)/.exec(className || '');
const isInline = !match && !className;
if (isInline) {
// 行内代码 - 无特殊样式,仅等宽字体
return (
{children}
);
}
// 代码块
return (
);
},
// 段落
p({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 标题 - 使用相对单位保持与全局字体的比例
h1({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
h2({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
h3({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
h4({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 列表
ul({ children }: { children?: React.ReactNode }) {
return (
);
},
ol({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
li({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 链接 - 支持图片链接在灯箱中打开
a({ href, children }: { href?: string; children?: React.ReactNode }) {
// 如果是图片链接且有回调,则拦截点击事件
if (href && onImageLinkClick && isImageUrl(href)) {
return (
{
e.preventDefault();
onImageLinkClick(href);
}}
className="text-[var(--color-primary)] hover:underline cursor-pointer"
>
{children}
);
}
// 非图片链接保持原有行为
return (
{children}
);
},
// 粗体
strong({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 斜体
em({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 引用
blockquote({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
// 分割线
hr() {
return (
);
},
// 表格
table({ children }: { children?: React.ReactNode }) {
return (
);
},
thead({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
tbody({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
tr({ children }: { children?: React.ReactNode }) {
return (
{children}
);
},
th({ children }: { children?: React.ReactNode }) {
return (
{children}
|
);
},
td({ children }: { children?: React.ReactNode }) {
return (
{children}
|
);
},
// 图片
img(props: React.ImgHTMLAttributes) {
return (
);
},
};
}
// 使用 memo 包裹组件,避免不必要的重渲染
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick }: MarkdownRendererProps) {
// 使用 useMemo 缓存 components 配置,仅在 onImageLinkClick 变化时重新创建
const components = useMemo(
() => createMarkdownComponents(onImageLinkClick),
[onImageLinkClick]
);
return (
{content}
);
});