'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 (
    {children}
    ); }, 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 ( {props.alt ); }, }; } // 使用 memo 包裹组件,避免不必要的重渲染 export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick }: MarkdownRendererProps) { // 使用 useMemo 缓存 components 配置,仅在 onImageLinkClick 变化时重新创建 const components = useMemo( () => createMarkdownComponents(onImageLinkClick), [onImageLinkClick] ); return (
    {content}
    ); });