1
- import { Typography , Card , Empty } from 'antd' ;
1
+ import { Typography , Card , Empty , Image } from 'antd' ;
2
2
import { Challenge } from '../../types/challenge' ;
3
3
import ReactMarkdown from 'react-markdown' ;
4
4
import rehypeRaw from 'rehype-raw' ;
5
5
import '../../styles/markdown.css' ;
6
6
import { useTranslation } from 'react-i18next' ;
7
+ import { useState } from 'react' ;
7
8
8
9
const { Title } = Typography ;
9
10
@@ -15,27 +16,43 @@ interface ChallengeDescriptionProps {
15
16
const FALLBACK_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' ;
16
17
17
18
// 图片组件
18
- const MarkdownImage = ( props : any ) => {
19
+ const MarkdownImage = ( { node } : { node : any } ) => {
19
20
// 使用传入的src或回退到默认图片
20
- const imageSrc = props . src || FALLBACK_IMAGE ;
21
+ const imageSrc = node . properties ? .src || FALLBACK_IMAGE ;
21
22
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组件,支持点击预览
22
37
return (
23
- < img
24
- { ...props }
25
- src = { imageSrc }
26
- alt = { props . alt || '图片' }
38
+ < Image
39
+ src = { fullImageSrc }
40
+ alt = { node . properties ?. alt || '图片' }
27
41
style = { {
28
42
maxWidth : '100%' ,
29
- boxShadow : '0 2px 8px rgba(0, 0, 0, 0.1)' ,
30
43
borderRadius : '4px' ,
31
44
margin : '16px 0' ,
32
45
display : 'block' ,
33
- ...props . style
34
46
} }
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
+ )
39
56
} }
40
57
/>
41
58
) ;
@@ -73,6 +90,176 @@ const ChallengeDescription: React.FC<ChallengeDescriptionProps> = ({ challenge }
73
90
? challenge . descriptionMarkdownEN
74
91
: challenge . descriptionMarkdown ;
75
92
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
76
263
return (
77
264
< div >
78
265
< Title level = { 3 } > { t ( 'challenge.detail.description' ) } </ Title >
0 commit comments