imetting/frontend/src/components/MarkdownEditor.jsx

161 lines
6.2 KiB
JavaScript

import React, { useState, useRef, useMemo } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { EditorView } from '@codemirror/view';
import {
Space, Button, Tooltip, Card,
Divider, Dropdown, Typography
} from 'antd';
import {
BoldOutlined, ItalicOutlined, FontSizeOutlined,
MessageOutlined, CodeOutlined, LinkOutlined,
TableOutlined, PictureOutlined, OrderedListOutlined,
UnorderedListOutlined, LineOutlined, EyeOutlined,
EditOutlined
} from '@ant-design/icons';
import MarkdownRenderer from './MarkdownRenderer';
const { Text } = Typography;
const MarkdownEditor = ({
value,
onChange,
onImageUpload,
placeholder = '在这里编写内容...',
height = 400,
showImageUpload = true
}) => {
const editorRef = useRef(null);
const imageInputRef = useRef(null);
const [showPreview, setShowPreview] = useState(false);
const editorExtensions = useMemo(() => [
markdown({ base: markdownLanguage }),
EditorView.lineWrapping,
EditorView.theme({
"&": {
fontSize: "14px",
border: "1px solid #d9d9d9",
borderRadius: "0 0 8px 8px",
borderTop: "none",
},
".cm-content": {
fontFamily: "var(--ant-font-family-code), monospace",
padding: "16px",
minHeight: `${height}px`,
},
"&.cm-focused": {
outline: "none",
borderColor: "#1677ff",
boxShadow: "0 0 0 2px rgba(22, 119, 255, 0.1)",
}
})
], [height]);
const insertMarkdown = (before, after = '', placeholder = '') => {
if (!editorRef.current?.view) return;
const view = editorRef.current.view;
const selection = view.state.selection.main;
const selectedText = view.state.doc.sliceString(selection.from, selection.to);
const text = selectedText || placeholder;
const newText = `${before}${text}${after}`;
view.dispatch({
changes: { from: selection.from, to: selection.to, insert: newText },
selection: { anchor: selection.from + before.length, head: selection.from + before.length + text.length }
});
view.focus();
};
const toolbarActions = {
bold: () => insertMarkdown('**', '**', '粗体'),
italic: () => insertMarkdown('*', '*', '斜体'),
heading: (level) => insertMarkdown('#'.repeat(level) + ' ', '', '标题'),
quote: () => insertMarkdown('> ', '', '引用'),
code: () => insertMarkdown('`', '`', '代码'),
link: () => insertMarkdown('[', '](url)', '链接'),
unorderedList: () => insertMarkdown('- ', '', '列表项'),
orderedList: () => insertMarkdown('1. ', '', '列表项'),
table: () => insertMarkdown('\n| 列1 | 列2 |\n| --- | --- |\n| 单元格 | 单元格 |\n', '', ''),
hr: () => insertMarkdown('\n---\n', '', ''),
image: () => imageInputRef.current?.click(),
};
const headingMenu = {
items: [1, 2, 3, 4, 5, 6].map(level => ({
key: level,
label: `标题 ${level}`,
onClick: () => toolbarActions.heading(level)
}))
};
return (
<div className="markdown-editor-modern">
<Card
size="small"
bodyStyle={{ padding: '4px 8px', background: '#f5f5f5', borderBottom: '1px solid #d9d9d9', borderRadius: '8px 8px 0 0' }}
bordered={false}
>
<Space split={<Divider type="vertical" />} size={4}>
<Space size={2}>
<Tooltip title="粗体"><Button type="text" size="small" icon={<BoldOutlined />} onClick={toolbarActions.bold} /></Tooltip>
<Tooltip title="斜体"><Button type="text" size="small" icon={<ItalicOutlined />} onClick={toolbarActions.italic} /></Tooltip>
<Dropdown menu={headingMenu} placement="bottomLeft">
<Button type="text" size="small" icon={<FontSizeOutlined />} />
</Dropdown>
</Space>
<Space size={2}>
<Tooltip title="引用"><Button type="text" size="small" icon={<MessageOutlined />} onClick={toolbarActions.quote} /></Tooltip>
<Tooltip title="代码"><Button type="text" size="small" icon={<CodeOutlined />} onClick={toolbarActions.code} /></Tooltip>
<Tooltip title="链接"><Button type="text" size="small" icon={<LinkOutlined />} onClick={toolbarActions.link} /></Tooltip>
<Tooltip title="表格"><Button type="text" size="small" icon={<TableOutlined />} onClick={toolbarActions.table} /></Tooltip>
{showImageUpload && (
<Tooltip title="图片"><Button type="text" size="small" icon={<PictureOutlined />} onClick={toolbarActions.image} /></Tooltip>
)}
</Space>
<Space size={2}>
<Tooltip title="无序列表"><Button type="text" size="small" icon={<UnorderedListOutlined />} onClick={toolbarActions.unorderedList} /></Tooltip>
<Tooltip title="有序列表"><Button type="text" size="small" icon={<OrderedListOutlined />} onClick={toolbarActions.orderedList} /></Tooltip>
<Tooltip title="分隔线"><Button type="text" size="small" icon={<LineOutlined />} onClick={toolbarActions.hr} /></Tooltip>
</Space>
<Button
type={showPreview ? "primary" : "text"}
size="small"
icon={showPreview ? <EditOutlined /> : <EyeOutlined />}
onClick={() => setShowPreview(!showPreview)}
>
{showPreview ? "编辑" : "预览"}
</Button>
</Space>
</Card>
{showPreview ? (
<Card bordered bodyStyle={{ padding: 16, minHeight: height, overflowY: 'auto' }} style={{ borderRadius: '0 0 8px 8px' }}>
<MarkdownRenderer content={value} />
</Card>
) : (
<CodeMirror
ref={editorRef}
value={value}
onChange={onChange}
extensions={editorExtensions}
placeholder={placeholder}
basicSetup={{ lineNumbers: false, foldGutter: false }}
/>
)}
<input ref={imageInputRef} type="file" accept="image/*" onChange={(e) => {
const file = e.target.files[0];
if (file && onImageUpload) {
onImageUpload(file).then(url => url && insertMarkdown(`![${file.name}](${url})`, '', ''));
}
e.target.value = '';
}} style={{ display: 'none' }} />
</div>
);
};
export default MarkdownEditor;