当前位置: 首页 > 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.xdnf.cn/news/1327951.html

相关文章:

  • LeetCode算法日记 - Day 16: 连续数组、矩阵区域和
  • 第4章 React状态管理基础
  • 算法训练营day56 图论⑥ 108. 109.冗余连接系列
  • 项目过程管理的重点是什么
  • Ansible 角色管理
  • 点大餐饮独立版系统源码v1.0.3+uniapp前端+搭建教程
  • GStreamer无线图传:树莓派到计算机的WiFi图传方案
  • GEO 优化专家孟庆涛:技术破壁者重构 AI 时代搜索逻辑
  • RESTful API 开发实践:淘宝商品详情页数据采集方案
  • Apache IoTDB:大数据时代时序数据库选型的技术突围与实践指南
  • 从0到1认识Rust通道
  • Redis-缓存-击穿-分布式锁
  • 无人机场景 - 目标检测数据集 - 山林野火烟雾检测数据集下载「包含VOC、COCO、YOLO三种格式」
  • 国产!全志T113-i 双核Cortex-A7@1.2GHz 工业开发板—ARM + FPGA通信案例
  • 如何免费给视频加字幕
  • Linux的ALSA音频框架学习笔记
  • Spring AOP 和 Spring 拦截器
  • LeetCode 100 -- Day2
  • JVM垃圾收集器
  • ts 引入类型 type 可以省略吗
  • sfc_os!SfcValidateDLL函数分析之cache文件版本
  • python的社区互助养老系统
  • 【实时Linux实战系列】实时平台下的图像识别技术
  • 微软AD国产化替换倒计时——不是选择题,而是生存题
  • 初识线段树
  • 电影购票+票房预测系统 - 后端项目介绍(附源码)
  • 114. 二叉树展开为链表
  • 华为云之开发者空间云主机使用体验【玩转华为云】
  • RH134 运行容器知识点
  • 【QT入门到晋级】进程间通信(IPC)-socket(包含性能优化案例)