创赢未来评分系统 - 初始化提交(移除大文件)
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
This commit is contained in:
146
miniprogram/src/components/MarkdownReader/index.tsx
Normal file
146
miniprogram/src/components/MarkdownReader/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { View, RichText, Video } from '@tarojs/components'
|
||||
import { marked, Renderer } from 'marked'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
content: string
|
||||
themeColor?: string
|
||||
}
|
||||
|
||||
const MarkdownReader: React.FC<Props> = ({ content, themeColor = '#00b96b' }) => {
|
||||
const elements = useMemo(() => {
|
||||
if (!content) return []
|
||||
|
||||
const tokens = marked.lexer(content)
|
||||
const result: React.ReactNode[] = []
|
||||
let currentTokens: any[] = []
|
||||
|
||||
// Configure renderer
|
||||
const renderer = new Renderer()
|
||||
|
||||
renderer.table = (header, body) => {
|
||||
return `<div style="overflow-x: auto; width: 100%; -webkit-overflow-scrolling: touch;">
|
||||
<table style="width: 100%; min-width: 600px; border-collapse: collapse; margin: 16px 0; font-size: 16px;">
|
||||
<thead>${header}</thead>
|
||||
<tbody>${body}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
}
|
||||
|
||||
renderer.tablecell = (content, flags) => {
|
||||
const type = flags.header ? 'th' : 'td'
|
||||
const style = [
|
||||
'border: 1px solid rgba(255,255,255,0.1)',
|
||||
'padding: 10px',
|
||||
flags.header ? 'background-color: rgba(255,255,255,0.05); font-weight: 700; color: #fff;' : 'color: #ddd;',
|
||||
flags.align ? `text-align: ${flags.align}` : 'text-align: left'
|
||||
].join(';')
|
||||
return `<${type} style="${style}">${content}</${type}>`
|
||||
}
|
||||
|
||||
renderer.image = (href, title, text) => {
|
||||
return `<img src="${href}" style="max-width:100%;border-radius:8px;margin:10px 0;box-shadow: 0 4px 12px rgba(0,0,0,0.3);" title="${title || ''}" alt="${text || ''}" />`
|
||||
}
|
||||
|
||||
renderer.link = (href, title, text) => {
|
||||
return `<a href="${href}" style="color: ${themeColor}; text-decoration: none;">${text}</a>`
|
||||
}
|
||||
|
||||
// Process tokens
|
||||
tokens.forEach((token, index) => {
|
||||
if (token.type === 'code') {
|
||||
// Skip css blocks that look like the video component styles
|
||||
if ((token.lang === 'css' || !token.lang) && token.text.includes('.simple-tech-video')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Flush accumulated tokens
|
||||
if (currentTokens.length > 0) {
|
||||
// preserve links if any
|
||||
(currentTokens as any).links = (tokens as any).links
|
||||
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
||||
result.push(<RichText key={`rt-${index}`} nodes={html} className='markdown-text' />)
|
||||
currentTokens = []
|
||||
}
|
||||
|
||||
// Add code block
|
||||
result.push(
|
||||
<View key={`cb-${index}`} className='code-block-wrapper'>
|
||||
<CodeBlock
|
||||
code={token.text}
|
||||
language={token.lang}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
} else if (token.type === 'html') {
|
||||
// Check for video tag
|
||||
const videoRegex = /<video[^>]*>[\s\S]*?<source[^>]*src=["'](.*?)["'][^>]*>[\s\S]*?<\/video>/i
|
||||
const simpleVideoRegex = /<video[^>]*src=["'](.*?)["'][^>]*>/i
|
||||
|
||||
const match = token.text.match(videoRegex) || token.text.match(simpleVideoRegex)
|
||||
|
||||
if (match) {
|
||||
// Flush accumulated tokens
|
||||
if (currentTokens.length > 0) {
|
||||
(currentTokens as any).links = (tokens as any).links
|
||||
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
||||
result.push(<RichText key={`rt-${index}`} nodes={html} className='markdown-text' />)
|
||||
currentTokens = []
|
||||
}
|
||||
|
||||
const src = match[1]
|
||||
// Try to extract caption
|
||||
const captionRegex = /class=["']video-caption["'][^>]*>(.*?)<\/div>/i
|
||||
const captionMatch = token.text.match(captionRegex)
|
||||
const caption = captionMatch ? captionMatch[1] : null
|
||||
|
||||
result.push(
|
||||
<View key={`video-${index}`} className='markdown-video-container'>
|
||||
<Video
|
||||
src={src}
|
||||
className='markdown-video'
|
||||
controls
|
||||
autoplay={false}
|
||||
objectFit='contain'
|
||||
showFullscreenBtn
|
||||
showPlayBtn
|
||||
showCenterPlayBtn
|
||||
enablePlayGesture
|
||||
/>
|
||||
{caption && <View className='markdown-video-caption'>{caption}</View>}
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
// Filter out style tags for video component if they are parsed as HTML
|
||||
if (token.text.includes('<style>') && token.text.includes('.simple-tech-video')) {
|
||||
// If it's JUST the style tag, ignore it. If it's mixed with other content, we might need to be careful.
|
||||
// But usually marked parses block HTML separately.
|
||||
// Let's verify if we can just skip it.
|
||||
// If the token is ONLY the style block, we skip it.
|
||||
// If it contains other content, we might need to strip the style.
|
||||
// For now, let's assume it's a block HTML token.
|
||||
return
|
||||
}
|
||||
currentTokens.push(token)
|
||||
}
|
||||
} else {
|
||||
currentTokens.push(token)
|
||||
}
|
||||
})
|
||||
|
||||
// Flush remaining tokens
|
||||
if (currentTokens.length > 0) {
|
||||
(currentTokens as any).links = (tokens as any).links
|
||||
const html = marked.parser(currentTokens as any, { renderer, breaks: true })
|
||||
result.push(<RichText key={`rt-end`} nodes={html} className='markdown-text' />)
|
||||
}
|
||||
|
||||
return result
|
||||
}, [content, themeColor])
|
||||
|
||||
return <View className='markdown-reader'>{elements}</View>
|
||||
}
|
||||
|
||||
export default MarkdownReader
|
||||
Reference in New Issue
Block a user