Files
MemoryBear/web/src/components/Markdown/index.tsx
yujiangping d6b1c2effb refactor(markdown): simplify editing logic and remove unused components
- Remove unused imports (Button, EditOutlined, SaveOutlined, CloseOutlined)
- Remove onSave callback prop and related save/cancel handlers
- Simplify editing state management by using editable prop directly instead of isEditing state
- Remove edit toolbar with save/cancel buttons from edit mode
- Remove edit button that appeared on hover in preview mode
- Remove unused props spread in button component renderer
- Simplify textarea rows to always use 10 rows instead of conditional logic
- Remove group hover styling from preview container
- Streamline component to rely solely on editable prop for mode switching
2026-01-06 19:46:38 +08:00

195 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
import ReactMarkdown from 'react-markdown'
import RemarkGfm from 'remark-gfm'
import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RehypeRaw from 'rehype-raw'
import type { FC } from 'react'
import { useState, useRef, useEffect } from 'react'
import Code from './Code'
import VideoBlock from './VideoBlock'
import AudioBlock from './AudioBlock'
import Link from './Link'
import RbButton from './RbButton'
interface RbMarkdownProps {
content: string;
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false隐藏
editable?: boolean; // 是否可编辑,默认为 false
onContentChange?: (content: string) => void; // 内容变化回调
}
const components = {
h1: ({ children, ...props }: any) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2" {...props}>{children}</h1>,
h2: ({ children, ...props }: any) => <h2 className="rb:text-xl rb:font-bold rb:mb-2" {...props}>{children}</h2>,
h3: ({ children, ...props }: any) => <h3 className="rb:text-lg rb:font-bold rb:mb-2" {...props}>{children}</h3>,
h4: ({ children, ...props }: any) => <h4 className="rb:text-md rb:font-bold rb:mb-2" {...props}>{children}</h4>,
h5: ({ children, ...props }: any) => <h5 className="rb:text-sm rb:font-bold rb:mb-2" {...props}>{children}</h5>,
h6: ({ children, ...props }: any) => <h6 className="rb:text-xs rb:font-bold rb:mb-2" {...props}>{children}</h6>,
ul: ({ children, ...props }: any) => <ul className="rb:list-disc rb:ml-6 rb:mb-2" {...props}>{children}</ul>,
ol: ({ children, ...props }: any) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2" {...props}>{children}</ol>,
li: ({ children, ...props }: any) => <li className="rb:mb-1" {...props}>{children}</li>,
blockquote: ({ children, ...props }: any) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2" {...props}>{children}</blockquote>,
p: ({ children, ...props }: any) => <p className="rb:mb-2" {...props}>{children}</p>,
strong: ({ children, ...props }: any) => <strong className="rb:font-bold" {...props}>{children}</strong>,
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
del: ({ children, ...props }: any) => <del className="rb:line-through" {...props}>{children}</del>,
span: ({ children, style, ...restProps }: any) => {
// 如果是 HTML 注释的 span应用特殊样式
if (style?.color === '#999') {
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
}
return <span style={style} {...restProps}>{children}</span>
},
code: ({ children, className, ...props }: any) => <Code children={String(children)} className={className || ''} {...props} />,
img: ({ src, alt, ...props }: any) => <Image src={src} alt={alt} {...props} />,
video: ({ src, ...props }: any) => <VideoBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
button: ({ children }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
table: ({ children, ...props }: any) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table>,
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
input: ({ children, ...props }: any) => {
switch (props.type) {
case 'color':
return <ColorPicker {...props} />
case 'time':
return <TimePicker {...props} />
case 'date':
return <DatePicker {...props} />
case 'datetime':
case 'datetime-local':
return <DatePicker showTime={true} {...props} />
case 'week':
return <DatePicker picker="week" {...props} />
case 'month':
return <DatePicker picker="month" {...props} />
case 'number':
return <InputNumber {...props} />
case 'search':
return <Input.Search {...props} />
case 'range':
return <Slider {...props} />
case 'submit':
case 'button':
return <RbButton node={{ children: props.value || children }}>{[props.value || children]}</RbButton>
case 'checkbox':
return <Checkbox {...props}>{children}</Checkbox>
case 'password':
return <Input.Password {...props} />
case 'radio':
return <Radio {...props}>{children}</Radio>
default:
return <Input value={children} {...props} />
}
},
select: ({ children, ...props }: any) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
textarea: ({ children, ...props }: any) => <Input.TextArea {...props}>{children}</Input.TextArea>,
form: ({ children, ...props }: any) => <Form {...props}>{children}</Form>,
}
const RbMarkdown: FC<RbMarkdownProps> = ({
content,
showHtmlComments = false,
editable = false,
onContentChange,
}) => {
const [editContent, setEditContent] = useState(content)
const textareaRef = useRef<any>(null)
// 当外部 content 变化时,同步更新编辑内容
useEffect(() => {
setEditContent(content)
}, [content])
// 处理 textarea 内容变化
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newContent = e.target.value
setEditContent(newContent)
// 实时回调内容变化
onContentChange?.(newContent)
}
// 根据参数决定是否将 HTML 注释转换为可见文本
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
const processedContent = showHtmlComments
? (editable ? editContent : content).replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
const escaped = commentContent.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')
return `<span class="html-comment">&lt;!-- ${escaped} --&gt;</span>`
})
: (editable ? editContent : content)
// 如果是编辑模式,显示 textarea
if (editable) {
return (
<div className="rb:relative">
<style>{`
.html-comment {
color: #999;
font-size: 0.9em;
}
`}</style>
{/* 编辑区域 */}
<Input.TextArea
ref={textareaRef}
value={editContent}
onChange={handleTextareaChange}
rows={10}
className="rb:font-mono rb:text-sm"
placeholder="请输入 Markdown 内容..."
style={{ resize: 'vertical' }}
/>
</div>
)
}
// 预览模式
return (
<div className="rb:relative">
<style>{`
.html-comment {
color: #999;
font-size: 0.9em;
}
`}</style>
<ReactMarkdown
// allowElement={[]}
// allowedElements={[]}
components={components as any}
disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
rehypePlugins={[
RehypeKatex,
RehypeRaw,
// The Rehype plug-in is used to remove the ref attribute of an element
// () => {
// return (tree) => {
// const iterate = (node: any) => {
// if (node.type === 'element' && !node.properties?.src && node.properties?.ref && node.properties.ref.startsWith('{') && node.properties.ref.endsWith('}'))
// delete node.properties.ref
// if (node.children)
// node.children.forEach(iterate)
// }
// tree.children.forEach(iterate)
// }
// },
]}
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
remarkRehypeOptions={{
allowDangerousHtml: true,
}}
>
{processedContent}
</ReactMarkdown>
</div>
)
}
export default RbMarkdown