diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx
index 12e4235d..fdaec143 100644
--- a/web/src/components/Markdown/index.tsx
+++ b/web/src/components/Markdown/index.tsx
@@ -1,4 +1,5 @@
-import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
+import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider, Button } from 'antd'
+import { EditOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown'
import RemarkGfm from 'remark-gfm'
import RemarkMath from 'remark-math'
@@ -6,6 +7,7 @@ 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'
@@ -16,42 +18,45 @@ import RbButton from './RbButton'
interface RbMarkdownProps {
content: string;
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏)
+ editable?: boolean; // 是否可编辑,默认为 false
+ onContentChange?: (content: string) => void; // 内容变化回调
+ onSave?: (content: string) => void; // 保存回调
}
const components = {
- h1: ({ children }: { children: string }) =>
{children}
,
- h2: ({ children }: { children: string }) => {children}
,
- h3: ({ children }: { children: string }) => {children}
,
- h4: ({ children }: { children: string }) => {children}
,
- h5: ({ children }: { children: string }) => {children}
,
- h6: ({ children }: { children: string }) => {children}
,
- ul: ({ children }: { children: string }) => ,
- ol: ({ children }: { children: string }) => {children}
,
- li: ({ children }: { children: string }) => {children},
- blockquote: ({ children }: { children: string }) => {children}
,
- p: ({ children }: { children: string }) => {children}
,
- strong: ({ children }: { children: string }) => {children},
- em: ({ children }: { children: string }) => {children},
- del: ({ children }: { children: string }) => {children},
- span: ({ children, ...props }: any) => {
+ h1: ({ children, ...props }: any) => {children}
,
+ h2: ({ children, ...props }: any) => {children}
,
+ h3: ({ children, ...props }: any) => {children}
,
+ h4: ({ children, ...props }: any) => {children}
,
+ h5: ({ children, ...props }: any) => {children}
,
+ h6: ({ children, ...props }: any) => {children}
,
+ ul: ({ children, ...props }: any) => ,
+ ol: ({ children, ...props }: any) => {children}
,
+ li: ({ children, ...props }: any) => {children},
+ blockquote: ({ children, ...props }: any) => {children}
,
+ p: ({ children, ...props }: any) => {children}
,
+ strong: ({ children, ...props }: any) => {children},
+ em: ({ children, ...props }: any) => {children},
+ del: ({ children, ...props }: any) => {children},
+ span: ({ children, style, ...restProps }: any) => {
// 如果是 HTML 注释的 span,应用特殊样式
- if (props.style?.color === '#999') {
+ if (style?.color === '#999') {
return {children}
}
- return {children}
+ return {children}
},
- code: Code,
- img: Image,
- video: VideoBlock,
- audio: AudioBlock,
- a: Link,
- button: RbButton,
- table: ({ children }: { children: string }) => ,
- tr: ({ children }: { children: string }) => {children}
,
- th: ({ children }: { children: string }) => {children} | ,
- td: ({ children }: { children: string }) => {children} | ,
- input: ({ children, ...props }: { children: string }) => {
+ code: ({ children, className, ...props }: any) => ,
+ img: ({ src, alt, ...props }: any) => ,
+ video: ({ src, ...props }: any) => ,
+ audio: ({ src, ...props }: any) => ,
+ a: ({ href, children, ...props }: any) => {children},
+ button: ({ children, ...props }: any) => {[children]},
+ table: ({ children, ...props }: any) => ,
+ tr: ({ children, ...props }: any) => {children}
,
+ th: ({ children, ...props }: any) => {children} | ,
+ td: ({ children, ...props }: any) => {children} | ,
+ input: ({ children, ...props }: any) => {
switch (props.type) {
case 'color':
return
@@ -74,7 +79,7 @@ const components = {
return
case 'submit':
case 'button':
- return {props.value}
+ return {[props.value || children]}
case 'checkbox':
return {children}
case 'password':
@@ -85,37 +90,158 @@ const components = {
return
}
},
- select: ({ children, ...props }: { children: string }) => ,
- textarea: ({ children, ...props }: { children: string }) => {children},
- form: ({ children }: { children: string }) => ,
+ select: ({ children, ...props }: any) => ,
+ textarea: ({ children, ...props }: any) => {children},
+ form: ({ children, ...props }: any) => ,
}
const RbMarkdown: FC = ({
content,
showHtmlComments = false,
+ editable = false,
+ onContentChange,
+ onSave,
}) => {
+ const [isEditing, setIsEditing] = useState(editable) // 如果可编辑,默认进入编辑模式
+ const [editContent, setEditContent] = useState(content)
+ const textareaRef = useRef(null)
+
+ // 当外部 content 变化时,同步更新编辑内容
+ useEffect(() => {
+ setEditContent(content)
+ }, [content])
+
+ // 当editable变化时,自动切换编辑状态
+ useEffect(() => {
+ if (editable) {
+ setIsEditing(true)
+ // 延迟聚焦,确保 textarea 已渲染
+ setTimeout(() => {
+ textareaRef.current?.focus()
+ }, 100)
+ }
+ }, [editable])
+
+ // 进入编辑模式
+ const handleEdit = () => {
+ setIsEditing(true)
+ setEditContent(content)
+ // 延迟聚焦,确保 textarea 已渲染
+ setTimeout(() => {
+ textareaRef.current?.focus()
+ }, 100)
+ }
+
+ // 保存编辑
+ const handleSave = () => {
+ onContentChange?.(editContent)
+ onSave?.(editContent)
+ if (!editable) {
+ setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
+ }
+ }
+
+ // 取消编辑
+ const handleCancel = () => {
+ setEditContent(content) // 恢复原内容
+ if (!editable) {
+ setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
+ }
+ }
+
+ // 处理 textarea 内容变化
+ const handleTextareaChange = (e: React.ChangeEvent) => {
+ const newContent = e.target.value
+ setEditContent(newContent)
+ // 实时回调内容变化
+ onContentChange?.(newContent)
+ }
+
// 根据参数决定是否将 HTML 注释转换为可见文本
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
const processedContent = showHtmlComments
- ? content.replace(//g, (_match, commentContent) => {
+ ? (isEditing ? editContent : content).replace(//g, (_match, commentContent) => {
// 转换为带样式的文本,使用