This commit is contained in:
42
miniprogram/src/components/MarkdownReader/CodeBlock.tsx
Normal file
42
miniprogram/src/components/MarkdownReader/CodeBlock.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { AtIcon } from 'taro-ui'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
code: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<Props> = ({ code, language }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = (e) => {
|
||||
e.stopPropagation()
|
||||
Taro.setClipboardData({
|
||||
data: code,
|
||||
success: () => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='markdown-code-block'>
|
||||
<View className='code-header'>
|
||||
<Text className='language'>{language || 'text'}</Text>
|
||||
<View className='copy-btn' onClick={handleCopy}>
|
||||
<AtIcon value='copy' size='14' color='#ccc' />
|
||||
<Text className='copy-text'>{copied ? '已复制' : '复制'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView scrollX scrollY className='code-content'>
|
||||
<Text userSelect className='code-text'>{code}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeBlock
|
||||
75
miniprogram/src/components/MarkdownReader/index.scss
Normal file
75
miniprogram/src/components/MarkdownReader/index.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
.markdown-reader {
|
||||
.markdown-text {
|
||||
/* Inherit font styles and color from parent */
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
|
||||
/* Ensure rich text images are responsive */
|
||||
image {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-code-block {
|
||||
margin: 16px 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
.language {
|
||||
color: #9cdcfe;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.copy-text {
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-content {
|
||||
padding: 12px;
|
||||
background-color: #1e1e1e;
|
||||
max-height: 400px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.code-text {
|
||||
color: #d4d4d4;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
display: block;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
miniprogram/src/components/MarkdownReader/index.tsx
Normal file
90
miniprogram/src/components/MarkdownReader/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { View, RichText } 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: 14px;">
|
||||
<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') {
|
||||
// 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 {
|
||||
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