react-quill-new富文本编辑器工具栏上传、粘贴截图、拖拽图片将base64改上传服务器再显示
目录
一、处理工具栏上传图片的方法
二、粘贴图片上传的方法
三、拖拽图片上传
react-quill-new是React的现代化Quill组件,这是react-quill的分支,它将其QuillJS依赖从1.3.7更新到>=2.0.2,并试图保持依赖更新和问题,因为原来的维护者不再活跃。它是用TypeScript编写的,支持React 16+,运行Quill ^2.0.2。对于目前项目中的react19有很好的支持。
但是,使用react-quill-new富文本编辑器无论是工具栏上传、粘贴、拖拽一张本地图片会直接以base64格式展示,因为eact-quill-new编辑器默认的处理是将图片转成base64保存。这样的话,提交后端时请求数据太大,导致数据库不好存储。如果文章被删,那么图片也不好找回,为了解决这个问题,我么希望在粘贴图片之后将图片上传到服务器,拿到线上地址进行回显友好解决此类问题。
一、处理工具栏上传图片的方法
useEffect(() => {if (!quillRef.current) return;const editor = quillRef.current.getEditor();const toolbar = editor.getModule('toolbar') as any;console.log('🚀 ~ App ~ toolbar:', toolbar);if (toolbar) {toolbar.handlers.image = () => {const input = document.createElement('input');input.type = 'file';input.accept = 'image/*';input.click();input.onchange = async () => {const file = input.files?.[0];if (file) handleImageUpload(file);};};}}, [handleImageUpload]);
二、粘贴图片上传的方法
useEffect(() => {if (!quillRef.current) return;const editor = quillRef.current.getEditor();// 粘贴文件 / 截图处理const handlePaste = (e: ClipboardEvent) => {if (!e.clipboardData) return;const items = e.clipboardData.items;for (let i = 0; i < items.length; i++) {const item = items[i];if (item.type.startsWith('image/')) {e.preventDefault(); // 阻止 Quill 默认插入 base64const file = item.getAsFile();if (file) handleImageUpload(file);}}};// 注意要用 capture 阶段,确保拦截 Quill 内部事件editor.root.addEventListener('paste', handlePaste, true);return () => {editor.root.removeEventListener('paste', handlePaste, true);};}, [handleImageUpload]);
关键点
-
addEventListener('paste', …, true)
→ 使用捕获阶段,否则 Quill 内部可能已经 stopPropagation,导致事件没执行。 -
e.preventDefault()
→ 阻止 Quill 默认插入 base64。 -
只处理图片文件,粘贴文本或 HTML 不受影响。
三、拖拽图片上传
// 拖拽处理useEffect(() => {if (!quillRef.current) return;const editor = quillRef.current.getEditor();const handleDrop = (e: DragEvent) => {if (!e.dataTransfer) return;const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'));if (files.length === 0) return;e.preventDefault(); // 阻止默认 base64 插入e.stopPropagation(); // 阻止事件冒泡files.forEach((file) => handleImageUpload(file)); // 上传并插入 URL};// use capture phase,确保在 Quill 内部事件之前拦截editor.root.addEventListener('drop', handleDrop, true);return () => {editor.root.removeEventListener('drop', handleDrop, true);};
关键点
-
e.preventDefault()
+e.stopPropagation()
:必须同时阻止默认和冒泡,否则 Quill 仍然会插入 base64。 -
事件绑定在捕获阶段(
true
):确保拦截 Quill 内部的 drop 处理。 -
确保没有其他 paste 或 drop 的监听重复调用
handleImageUpload
,否则还是会重复插入。
三、附上完整处理方案
import React, { useCallback, useEffect, useRef, useState } from 'react';
import QuillResizeImage from 'quill-resize-image';
import ReactQuill, { Quill } from 'react-quill-new';import 'react-quill-new/dist/quill.snow.css';import { Flex, message, Spin, Typography } from 'antd';
import { useTranslation } from 'react-i18next';import { imageUpload } from '@/plugins/http/api';interface RichTextEditorProps {value?: string;onChange?: (value: string) => void;maxLength?: number;
}// 注册图片缩放模块
Quill.register('modules/resize', QuillResizeImage);const toolbarOptions = [['bold', 'italic', 'underline', 'strike'],['blockquote', 'code-block'],['link', 'image'],[{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],[{ indent: '-1' }, { indent: '+1' }],[{ direction: 'rtl' }],[{ size: ['small', false, 'large', 'huge'] }],[{ header: [1, 2, 3, 4, 5, 6, false] }],[{ color: [] }, { background: [] }],[{ align: [] }],['clean']
];const { Text } = Typography;const uploadImage = async (file: File) => {try {const res = await imageUpload();const response = await fetch(res.data.url, {method: 'PUT',headers: { 'Content-Type': file.type },body: file});if (response.ok) {const fileUrl = res.data.host + res.data.path;message.success('Image uploaded successfully');return fileUrl;} else {message.error('Image upload failed');return null;}} catch (err) {console.error('Image upload failed', err);message.error('Image upload failed');return null;}
};const ReactQuillEditor: React.FC<RichTextEditorProps> = ({ value, onChange, maxLength = 200 }) => {const { t } = useTranslation();const quillRef = useRef<ReactQuill>(null);const [uploading, setUploading] = useState(false);const [count, setCount] = useState(0);// 文字计数const handleChange = (content: string) => {const div = document.createElement('div');div.innerHTML = content || '';setCount(div.innerText.trim().length);onChange?.(content);};// 图片上传统一处理const handleImageUpload = useCallback(async (file: File) => {setUploading(true);try {const imageUrl = await uploadImage(file);if (imageUrl && quillRef.current) {const editor = quillRef.current.getEditor();const range = editor.getSelection(true);editor.insertEmbed(range.index, 'image', imageUrl);editor.setSelection(range.index + 1);}} finally {setUploading(false);}}, []);// 初始化 Quill 模块 & 事件处理useEffect(() => {if (!quillRef.current) return;const editor = quillRef.current.getEditor();// 工具栏上传const toolbar = editor.getModule('toolbar') as any;if (toolbar) {toolbar.handlers.image = () => {const input = document.createElement('input');input.type = 'file';input.accept = 'image/*';input.click();input.onchange = async () => {const file = input.files?.[0];if (file) handleImageUpload(file);};};}// 粘贴处理const handlePaste = (e: ClipboardEvent) => {if (!e.clipboardData) return;const items = e.clipboardData.items;for (let i = 0; i < items.length; i++) {const item = items[i];if (item.type.startsWith('image/')) {e.preventDefault(); // 阻止 base64 插入const file = item.getAsFile();if (file) handleImageUpload(file);}}};// 拖拽处理const handleDrop = (e: DragEvent) => {if (!e.dataTransfer) return;const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'));if (files.length === 0) return;e.preventDefault();e.stopPropagation();files.forEach((file) => handleImageUpload(file));};editor.root.addEventListener('paste', handlePaste, true);editor.root.addEventListener('drop', handleDrop, true);return () => {editor.root.removeEventListener('paste', handlePaste, true);editor.root.removeEventListener('drop', handleDrop, true);};}, [handleImageUpload]);return (<div className="rounded-sm border-1 border-[#C5C5C5]"><Spin spinning={uploading}><ReactQuillref={quillRef}theme="snow"value={value}onChange={handleChange}placeholder={t('pleaseInputContent')}modules={{toolbar: toolbarOptions,resize: { locale: {} }}}className="react-quill"style={{ height: 260, border: 'none' }}/></Spin><Flex justify="end" className="mt-1 text-sm"><Text>{count}</Text>/<Text type="secondary">{maxLength}</Text></Flex></div>);
};export default ReactQuillEditor;