文章编辑器问题

This commit is contained in:
Alex-larget
2026-03-14 23:27:22 +08:00
parent d82ef6d8e4
commit 7ece8f52ff
24 changed files with 642 additions and 217 deletions

View File

@@ -232,6 +232,61 @@ const MentionSuggestion = (persons: PersonItem[]): any => ({
},
})
/** 从剪贴板提取图片文件(粘贴截图、复制图片时) */
function getImageFilesFromClipboard(event: ClipboardEvent): File[] {
const files: File[] = []
const items = event.clipboardData?.items
if (!items) return files
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile()
if (file) files.push(file)
}
}
return files
}
const BASE64_IMG_RE = /src=["'](data:image\/([^;"']+);base64,([A-Za-z0-9+/=]+))["']/gi
/** 将 base64 转为 File */
function base64ToFile(b64: string, mime: string): File {
const ext = { png: '.png', jpeg: '.jpg', jpg: '.jpg', gif: '.gif', webp: '.webp' }[mime.toLowerCase()] || '.png'
const bin = atob(b64)
const arr = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i)
return new File([new Blob([arr], { type: `image/${mime}` })], `image${ext}`, { type: `image/${mime}` })
}
/** 将 HTML 中的 base64 图片替换为上传后的 URL支持多个、去重同图只传一次 */
async function replaceBase64ImagesInHtml(
html: string,
onImageUpload: (file: File) => Promise<string>,
): Promise<string> {
const matches = [...html.matchAll(BASE64_IMG_RE)]
if (matches.length === 0) return html
const urlCache = new Map<string, string>() // full dataURL -> 上传后的 url同图只传一次
let result = html
for (const m of matches) {
const full = m[1]
const mime = m[2]
const b64 = m[3]
let url = urlCache.get(full)
if (!url) {
try {
const file = base64ToFile(b64, mime)
url = await onImageUpload(file)
urlCache.set(full, url)
} catch (e) {
console.error('base64 图片上传失败', e)
continue
}
}
// 替换该 base64 的所有出现(同一张图可能被引用多次)
result = result.split(`src="${full}"`).join(`src="${url}"`).split(`src='${full}'`).join(`src="${url}"`)
}
return result
}
const RichEditor = forwardRef<RichEditorRef, RichEditorProps>(({
content,
onChange,
@@ -242,10 +297,54 @@ const RichEditor = forwardRef<RichEditorRef, RichEditorProps>(({
className,
}, ref) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const editorRef = useRef<Editor | null>(null)
const [linkUrl, setLinkUrl] = useState('')
const [showLinkInput, setShowLinkInput] = useState(false)
const initialContent = useRef(markdownToHtml(content))
const handlePaste = useCallback(
(_view: unknown, event: ClipboardEvent) => {
const ed = editorRef.current
if (!ed || !onImageUpload) return false
// 1. 粘贴截图/复制图片:剪贴板直接有 image 文件
const imageFiles = getImageFilesFromClipboard(event)
if (imageFiles.length > 0) {
event.preventDefault()
;(async () => {
for (const file of imageFiles) {
try {
const url = await onImageUpload(file)
if (url) ed.chain().focus().setImage({ src: url }).run()
} catch (e) {
console.error('粘贴图片上传失败', e)
}
}
})()
return true
}
// 2. 粘贴 HTML含 base64 图片):复制编辑器内容再粘贴时,自动上传 base64 转为 URL
const html = event.clipboardData?.getData('text/html')
if (html && /data:image\/[^;"']+;base64,/i.test(html)) {
event.preventDefault()
const { from, to } = ed.state.selection
;(async () => {
try {
const processed = await replaceBase64ImagesInHtml(html, onImageUpload)
ed.chain().focus().insertContentAt({ from, to }, processed).run()
} catch (e) {
console.error('粘贴 HTML 内 base64 转换失败', e)
}
})()
return true
}
return false
},
[onImageUpload],
)
const editor = useEditor({
extensions: [
StarterKit.configure({
@@ -267,9 +366,14 @@ const RichEditor = forwardRef<RichEditorRef, RichEditorProps>(({
},
editorProps: {
attributes: { class: 'rich-editor-content' },
handlePaste,
},
})
useEffect(() => {
editorRef.current = editor ?? null
}, [editor])
useImperativeHandle(ref, () => ({
getHTML: () => editor?.getHTML() || '',
getMarkdown: () => htmlToMarkdown(editor?.getHTML() || ''),