1- import { Typography , Card , Empty } from 'antd' ;
1+ import { Typography , Card , Empty , Image } from 'antd' ;
22import { Challenge } from '../../types/challenge' ;
33import ReactMarkdown from 'react-markdown' ;
44import rehypeRaw from 'rehype-raw' ;
55import '../../styles/markdown.css' ;
66import { useTranslation } from 'react-i18next' ;
7+ import { useState } from 'react' ;
78
89const { Title } = Typography ;
910
@@ -15,27 +16,43 @@ interface ChallengeDescriptionProps {
1516const FALLBACK_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' ;
1617
1718// 图片组件
18- const MarkdownImage = ( props : any ) => {
19+ const MarkdownImage = ( { node } : { node : any } ) => {
1920 // 使用传入的src或回退到默认图片
20- const imageSrc = props . src || FALLBACK_IMAGE ;
21+ const imageSrc = node . properties ? .src || FALLBACK_IMAGE ;
2122
23+ // 如果是data:image类型的图片,直接使用原始src
24+ const isDataImage = typeof imageSrc === 'string' && imageSrc . startsWith ( 'data:image' ) ;
25+
26+ // 检查图片源是否完整 (data:image格式但很短,可能被截断)
27+ const isTruncatedBase64 = isDataImage && imageSrc . length < 100 ;
28+
29+ // 解决方案:如果检测到是被截断的data:image,尝试从node属性中提取完整的图片数据
30+ // 这是处理React-Markdown可能截断长字符串的情况
31+ let fullImageSrc = imageSrc ;
32+ if ( isTruncatedBase64 && node && node . properties && node . properties . src ) {
33+ fullImageSrc = node . properties . src ;
34+ }
35+
36+ // 使用Ant Design的Image组件,支持点击预览
2237 return (
23- < img
24- { ...props }
25- src = { imageSrc }
26- alt = { props . alt || '图片' }
38+ < Image
39+ src = { fullImageSrc }
40+ alt = { node . properties ?. alt || '图片' }
2741 style = { {
2842 maxWidth : '100%' ,
29- boxShadow : '0 2px 8px rgba(0, 0, 0, 0.1)' ,
3043 borderRadius : '4px' ,
3144 margin : '16px 0' ,
3245 display : 'block' ,
33- ...props . style
3446 } }
35- onError = { ( e ) => {
36- const imgElement = e . currentTarget as HTMLImageElement ;
37- imgElement . onerror = null ; // 防止循环错误
38- imgElement . src = FALLBACK_IMAGE ;
47+ preview = { {
48+ mask : < div className = "image-preview-mask" > 点击查看大图</ div > ,
49+ maskClassName : "image-preview-mask" ,
50+ rootClassName : "custom-image-preview" ,
51+ toolbarRender : ( ) => (
52+ < div className = "image-preview-tip" >
53+ 点击图片外区域关闭 | 滚轮缩放 | 左键拖动
54+ </ div >
55+ )
3956 } }
4057 />
4158 ) ;
@@ -73,6 +90,176 @@ const ChallengeDescription: React.FC<ChallengeDescriptionProps> = ({ challenge }
7390 ? challenge . descriptionMarkdownEN
7491 : challenge . descriptionMarkdown ;
7592
93+ // 检查Markdown中是否包含base64图片
94+ const hasBase64Image = displayDescription . includes ( 'data:image/' ) || displayDescription . includes ( '
111+
112+ // 直接尝试通过正则表达式提取文本中的图片
113+ try {
114+ // 检查是否包含完整的图片标记
115+ const firstTextPart = content . split ( '![' ) [ 0 ] || '' ;
116+ // 获取描述-markdown字段后的内容
117+ const markdownRegex = / d e s c r i p t i o n ( - | \s ) m a r k d o w n : [ \s ] * \| ( [ \s \S ] * ?) ( \n \s * \w + : | $ ) / i;
118+ const markdownMatch = challenge . descriptionMarkdown . match ( markdownRegex ) ;
119+
120+ if ( markdownMatch && markdownMatch [ 2 ] ) {
121+ const fullMarkdown = markdownMatch [ 2 ] . trim ( ) ;
122+
123+ // 尝试匹配图片标记
124+ const imgRegex = / ! \[ ( .+ ?) \] \( ( d a t a : i m a g e \/ [ ^ ) ] + ) \) / ;
125+ const imgMatch = fullMarkdown . match ( imgRegex ) ;
126+
127+ if ( imgMatch && imgMatch [ 2 ] ) {
128+ // 提取图片URL以供Image组件使用
129+ extractedImageUrl = imgMatch [ 2 ] ;
130+
131+ // 构建HTML
132+ htmlContent = `
133+ <div>
134+ <p>${ firstTextPart } </p>
135+ </div>
136+ ` ;
137+ }
138+ }
139+ } catch ( error ) {
140+ console . error ( '处理Markdown图片时出错:' , error ) ;
141+ }
142+ }
143+
144+ // 如果上面的方法没有找到图片,尝试直接解析Markdown
145+ if ( ! extractedImageUrl ) {
146+ // 尝试从文本中提取完整的base64图片
147+ // 模式1: 
148+ const imgMatches = content . match ( / ! \[ ( .+ ?) \] \( ( d a t a : i m a g e \/ .+ ?b a s e 6 4 , ) ( [ ^ ) ] + ) \) / ) ;
149+
150+ if ( imgMatches ) {
151+ const [ fullMatch , alt , prefix , base64Data ] = imgMatches ;
152+ extractedImageUrl = prefix + base64Data ;
153+
154+ htmlContent = `
155+ <div>
156+ <p>${ content . split ( fullMatch ) [ 0 ] } </p>
157+ <p>${ content . split ( fullMatch ) [ 1 ] || '' } </p>
158+ </div>
159+ ` ;
160+ } else {
161+ // 模式2: 尝试直接匹配被截断的base64链接
162+ const truncatedMatch = content . match ( / ! \[ ( .+ ?) \] \( ( d a t a : i m a g e \/ [ ^ ) ] * ) \) / ) ;
163+
164+ if ( truncatedMatch ) {
165+ const [ fullMatch , alt ] = truncatedMatch ;
166+
167+ // 从YAML源中查找描述字段中的base64编码
168+ const rawYaml = challenge . descriptionMarkdown ;
169+ const base64Match = rawYaml . match ( / d a t a : i m a g e \/ [ ^ ) ] + / ) ;
170+
171+ if ( base64Match ) {
172+ extractedImageUrl = base64Match [ 0 ] ;
173+
174+ htmlContent = `
175+ <div>
176+ <p>${ content . split ( fullMatch ) [ 0 ] } </p>
177+ <p>${ content . split ( fullMatch ) [ 1 ] || '' } </p>
178+ </div>
179+ ` ;
180+ }
181+ }
182+ }
183+ }
184+
185+ // 最终解决方案:直接提取并创建图片元素
186+ if ( ! extractedImageUrl && challenge . descriptionMarkdown ) {
187+ // 从截图看,图片的base64数据被错误地当作文本显示
188+ // 直接使用这些文本内容作为图片源
189+ const text = challenge . descriptionMarkdown ;
190+ const textParts = displayDescription . split ( '\n' ) ;
191+
192+ // 找出包含爱给网站音频播放链接加密的那一行,作为第一部分
193+ const firstPart = textParts [ 0 ] || '' ;
194+
195+ // 检查原始文本是否包含data:image部分
196+ if ( text . includes ( 'data:image/png;base64,' ) ) {
197+ // 截取data:image开始的部分直到结束
198+ const dataImageIndex = text . indexOf ( 'data:image/png;base64,' ) ;
199+ if ( dataImageIndex !== - 1 ) {
200+ let endIndex = text . indexOf ( '"' , dataImageIndex ) ;
201+ if ( endIndex === - 1 ) endIndex = text . indexOf ( "'" , dataImageIndex ) ;
202+ if ( endIndex === - 1 ) endIndex = text . indexOf ( ')' , dataImageIndex ) ;
203+ if ( endIndex === - 1 ) endIndex = text . length ;
204+
205+ extractedImageUrl = text . substring ( dataImageIndex , endIndex ) ;
206+
207+ htmlContent = `
208+ <div>
209+ <p>${ firstPart } </p>
210+ </div>
211+ ` ;
212+ }
213+ }
214+ }
215+
216+ // 如果成功提取了图片URL,使用Ant Design的Image组件显示
217+ if ( extractedImageUrl ) {
218+ return (
219+ < div >
220+ < Title level = { 3 } > { t ( 'challenge.detail.description' ) } </ Title >
221+
222+ < Card
223+ bordered = { false }
224+ style = { {
225+ marginBottom : 24 ,
226+ wordWrap : 'break-word' ,
227+ overflowWrap : 'break-word'
228+ } }
229+ >
230+ < div className = "markdown-content" >
231+ { htmlContent && (
232+ < div dangerouslySetInnerHTML = { { __html : htmlContent } } />
233+ ) }
234+ < Image
235+ src = { extractedImageUrl }
236+ alt = "挑战图片"
237+ style = { {
238+ maxWidth : '100%' ,
239+ borderRadius : '4px' ,
240+ margin : '16px 0' ,
241+ display : 'block'
242+ } }
243+ preview = { {
244+ mask : '点击查看大图' ,
245+ maskClassName : 'image-preview-mask' ,
246+ toolbarRender : ( ) => (
247+ < div className = "image-preview-tip" >
248+ 点击图片外区域关闭 | 滚轮缩放 | 左键拖动
249+ </ div >
250+ ) ,
251+ rootClassName : 'custom-image-preview'
252+ } }
253+ fallback = { FALLBACK_IMAGE }
254+ />
255+ </ div >
256+ </ Card >
257+ </ div >
258+ ) ;
259+ }
260+ }
261+
262+ // 默认渲染方式 - 使用ReactMarkdown
76263 return (
77264 < div >
78265 < Title level = { 3 } > { t ( 'challenge.detail.description' ) } </ Title >
0 commit comments