diff --git a/frontend/components/common/markdown/ContentRender.tsx b/frontend/components/common/markdown/ContentRender.tsx index 0c6be11..a5d3993 100644 --- a/frontend/components/common/markdown/ContentRender.tsx +++ b/frontend/components/common/markdown/ContentRender.tsx @@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import {copyToClipboard} from '@/lib/utils'; -import {ReactNode} from 'react'; +import {ComponentPropsWithoutRef, ReactNode, useState} from 'react'; import type {Components} from 'react-markdown'; /** @@ -33,6 +33,57 @@ const getCodeContent = (node: ReactNode): string => { return ''; }; +/** + * Markdown 图片组件 + */ +const MarkdownImage = ({src, alt, title}: ComponentPropsWithoutRef<'img'>) => { + const [hasError, setHasError] = useState(false); + const fallbackText = alt || (typeof src === 'string' ? src : ''); + + return ( + <> + {hasError ? ( +
+ + + +
图片加载失败
+
+ {fallbackText} +
+
+ ) : ( + <> + {/** + * 此处使用原生 img 元素而非 Next.js Image 组件。 + * Markdown 图片通常来自外部源,使用 Next.js Image 会增加域名白名单配置复杂性。 + */} + {/* eslint-disable-next-line @next/next/no-img-element */} + {alt setHasError(true)} + /> + + )} + {(alt || title) && ( + + {title || alt} + + )} + + ); +}; + /** * Markdown 组件配置映射 */ @@ -254,44 +305,7 @@ const markdownComponents: Components = { hr: () => (
), - img: ({src, alt, title}) => { - return ( - <> - {/* - * 注意:此组件中使用原生 img 元素而非 Next.js Image 组件。 - * Markdown 内容中的图片 URL 通常来自外部源,使用 Next.js Image 组件会导致跨域问题和域名白名单配置复杂性。 - */} - {/* eslint-disable-next-line @next/next/no-img-element */} - {alt { - const target = e.target as HTMLImageElement; - const errorDiv = document.createElement('div'); - errorDiv.className = 'bg-muted border border-border rounded-lg p-4 text-center text-muted-foreground my-6'; - errorDiv.innerHTML = ` - - - -
图片加载失败
-
${alt || src}
- `; - target.parentNode?.replaceChild(errorDiv, target); - }} - /> - {(alt || title) && ( - - {title || alt} - - )} - - ); - }, + img: MarkdownImage, table: ({children}) => (