当前位置: 首页 > news >正文

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]);

关键点

  1. addEventListener('paste', …, true) → 使用捕获阶段,否则 Quill 内部可能已经 stopPropagation,导致事件没执行。

  2. e.preventDefault() → 阻止 Quill 默认插入 base64。

  3. 只处理图片文件,粘贴文本或 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);};

关键点

  1. e.preventDefault() + e.stopPropagation():必须同时阻止默认和冒泡,否则 Quill 仍然会插入 base64。

  2. 事件绑定在捕获阶段(true:确保拦截 Quill 内部的 drop 处理。

  3. 确保没有其他 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;

http://www.dtcms.com/a/338850.html

相关文章:

  • 叉车结构设计cad+三维图+设计说明书
  • 浅看架构理论(一)
  • Parallels Desktop 26 技术预览版免激活下载适配Tahoe 26
  • 【撸靶笔记】第七关:GET - Dump into outfile - String
  • LeetCode算法日记 - Day 16: 连续数组、矩阵区域和
  • [系统架构设计师]信息系统架构设计理论与实践(十二)
  • 第八十四章:实战篇:图 → 视频:基于 AnimateDiff 的视频合成链路——让你的图片“活”起来,瞬间拥有“电影感”!
  • C++排序算法学习笔记
  • Java第十三课 异常(超简单)
  • 基于zephyr使用stm32的LTDC点亮ARGB8888LCD触摸屏
  • Kubernetes 简介
  • 代码随想录刷题——字符串篇(七)
  • 字符分类函数与字符转换函数
  • 【LeetCode 热题 100】279. 完全平方数——(解法一)记忆化搜索
  • kkfileview预览Excel文件去掉左上角的跳转HTM预览、打印按钮
  • Python爬虫第一课:爬取HTML静态网页小说章节
  • 国产!全志T113-i 双核Cortex-A7@1.2GHz 工业开发板—ARM + FPGA通信案例
  • PicoShare 文件共享教程:cpolar 内网穿透服务实现跨设备极速传输
  • Simulink库文件创建及使用方法
  • 4.Kotlin 集合 Map 所有方法
  • CVPR 2025 | 具身智能 | HOLODECK:一句话召唤3D世界,智能体的“元宇宙练功房”来了
  • 库卡机器人tag焊接保护气体流量控制系统
  • (第五篇)spring cloud之Ribbon负载均衡
  • 主流 3D 模型格式(FBX/OBJ/DAE/GLTF)材质支持与转换操作指南
  • 云存储的高效安全助手:阿里云国际站 OSS
  • ICCV 2025 | 首个3D动作游戏专用VLA模型,打黑神话只狼超越人类玩家
  • iOS 性能监控实践,如何构建从开发到运维的持续优化体系
  • 面试题储备-MQ篇 3-说说你对Kafka的理解
  • 如何用给各种IDE配置R语言环境
  • Halcon联合C# 添加工具类让winform自动根据窗体大小自适应缩放所有控件