66 lines
1.8 KiB
TypeScript
66 lines
1.8 KiB
TypeScript
import React from 'react'
|
||
import ReactMarkdown from 'react-markdown'
|
||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||
import remarkGfm from 'remark-gfm'
|
||
import { cn } from '@/utils/cn'
|
||
|
||
export interface MarkdownPreviewProps {
|
||
/** Markdown 文件的 URL */
|
||
url: string
|
||
}
|
||
|
||
export interface MarkdownContentProps {
|
||
/** 直接传入 Markdown 字符串 */
|
||
content: string
|
||
className?: string
|
||
}
|
||
|
||
/**
|
||
* 内联 Markdown 渲染组件 - 接收字符串内容直接渲染
|
||
*/
|
||
export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
||
return (
|
||
<div className={cn('prose prose-sm max-w-none dark:prose-invert break-words', className)}>
|
||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Markdown 预览组件 - 接收 URL,fetch 内容后渲染
|
||
*/
|
||
export function MarkdownPreview({ url }: MarkdownPreviewProps) {
|
||
const [content, setContent] = React.useState<string>('')
|
||
const [loading, setLoading] = React.useState(true)
|
||
|
||
React.useEffect(() => {
|
||
if (!url) return
|
||
|
||
setLoading(true)
|
||
fetch(url)
|
||
.then(res => res.text())
|
||
.then(text => setContent(text))
|
||
.catch(() => setContent('加载失败'))
|
||
.finally(() => setLoading(false))
|
||
}, [url])
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex-1 flex flex-col items-center justify-center p-8 text-muted-foreground h-full">
|
||
<div className="w-8 h-8 border-2 border-muted border-t-primary rounded-full animate-spin" />
|
||
<span className="text-sm mt-2">加载中...</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<ScrollArea className="h-full">
|
||
<div className="p-6 pt-14 prose prose-sm max-w-none dark:prose-invert">
|
||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||
</div>
|
||
</ScrollArea>
|
||
)
|
||
}
|
||
|
||
export default MarkdownPreview
|