文章编辑器问题
This commit is contained in:
@@ -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() || ''),
|
||||
|
||||
Reference in New Issue
Block a user