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

Vue项目里给Element UI的Quill富文本编辑器加上图片上传功能(附完整代码)

Vue项目中为Element UI的Quill富文本编辑器实现图片上传功能

在后台管理系统开发中,富文本编辑器是不可或缺的组件。Element UI作为Vue生态中流行的UI框架,常与Quill富文本编辑器结合使用。但官方文档对图片上传功能的实现着墨不多,这让不少开发者感到困惑。本文将手把手带你实现一个完整的图片上传解决方案。

1. 环境准备与基础配置

首先确保项目已正确安装Vue、Element UI和Quill相关依赖。推荐使用npm或yarn进行安装:

npm install vue@2 element-ui quill --save

Quill编辑器需要引入对应的CSS样式文件,通常在组件中直接导入:

import 'quill/dist/quill.core.css' import 'quill/dist/quill.snow.css' import 'quill/dist/quill.bubble.css'

创建一个基本的Editor组件框架:

<template> <div class="editor-container"> <div ref="editor" :style="{ height: editorHeight }"></div> </div> </template> <script> import Quill from 'quill' export default { props: { value: String, height: { type: String, default: '400px' } }, data() { return { editor: null, editorHeight: this.height } }, mounted() { this.initEditor() }, methods: { initEditor() { this.editor = new Quill(this.$refs.editor, { theme: 'snow', modules: { toolbar: [ ['bold', 'italic', 'underline', 'strike'], ['blockquote', 'code-block'], [{ 'header': 1 }, { 'header': 2 }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], [{ 'indent': '-1'}, { 'indent': '+1' }], [{ 'direction': 'rtl' }], [{ 'size': ['small', false, 'large', 'huge'] }], [{ 'header': [1, 2, 3, 4, 5, 6, false] }], [{ 'color': [] }, { 'background': [] }], [{ 'font': [] }], [{ 'align': [] }], ['clean'], ['link', 'image', 'video'] ] }, placeholder: '请输入内容...' }) } } } </script>

2. 图片上传功能实现

2.1 集成Element UI上传组件

我们需要在Quill编辑器中集成Element UI的Upload组件来处理图片上传:

<template> <div class="editor-container"> <el-upload class="upload-demo" :action="uploadUrl" :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError" :before-upload="beforeUpload" style="display: none" ref="imageUpload" > </el-upload> <div ref="editor"></div> </div> </template>

2.2 自定义图片处理逻辑

修改Quill的初始化代码,自定义图片处理逻辑:

initEditor() { this.editor = new Quill(this.$refs.editor, { // ...其他配置 }) // 自定义图片处理 const toolbar = this.editor.getModule('toolbar') toolbar.addHandler('image', () => { this.selectLocalImage() }) } selectLocalImage() { const input = document.createElement('input') input.setAttribute('type', 'file') input.setAttribute('accept', 'image/*') input.click() input.onchange = () => { const file = input.files[0] if (!file) return // 检查文件类型和大小 if (!this.checkImage(file)) return // 触发上传 this.$refs.imageUpload.$el.querySelector('input').files = input.files this.$refs.imageUpload.submit() } } checkImage(file) { const isImage = file.type.includes('image/') const isLt5M = file.size / 1024 / 1024 < 5 if (!isImage) { this.$message.error('只能上传图片文件!') return false } if (!isLt5M) { this.$message.error('图片大小不能超过5MB!') return false } return true }

3. 后端接口对接与回调处理

3.1 处理上传成功回调

handleUploadSuccess(response, file) { if (response.code === 200) { // 获取当前光标位置 const range = this.editor.getSelection() // 插入图片 this.editor.insertEmbed(range.index, 'image', response.data.url) // 移动光标到图片后面 this.editor.setSelection(range.index + 1) } else { this.$message.error(response.message || '图片上传失败') } }

3.2 错误处理

handleUploadError(err) { console.error('上传失败:', err) this.$message.error('图片上传失败,请重试') }

4. 高级功能与优化

4.1 支持多种上传方式

除了本地文件上传,还可以支持粘贴板图片上传:

initEditor() { // ...其他初始化代码 this.editor.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => { if (node.tagName === 'IMG') { const blob = this.dataURLtoBlob(node.src) if (blob) { this.uploadImageFromBlob(blob) return delta.compose(new Delta().retain(delta.length(), { image: '' })) } } return delta }) } dataURLtoBlob(dataurl) { const arr = dataurl.split(',') const mime = arr[0].match(/:(.*?);/)[1] const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) } uploadImageFromBlob(blob) { const file = new File([blob], 'pasted-image.png', { type: blob.type }) this.$refs.imageUpload.$el.querySelector('input').files = [file] this.$refs.imageUpload.submit() }

4.2 上传进度提示

添加上传进度提示提升用户体验:

<el-upload :on-progress="handleProgress" // ...其他属性 > </el-upload> // 在data中添加 data() { return { uploadProgress: 0, showProgress: false } } // 添加方法 handleProgress(event, file, fileList) { this.showProgress = true this.uploadProgress = Math.floor(event.percent) } handleUploadSuccess() { this.showProgress = false this.uploadProgress = 0 // ...其他成功处理 }

5. 完整组件代码

以下是整合了所有功能的完整组件代码:

<template> <div class="editor-wrapper"> <el-upload ref="imageUpload" :action="uploadUrl" :headers="headers" :show-file-list="false" :before-upload="beforeUpload" :on-success="handleUploadSuccess" :on-error="handleUploadError" :on-progress="handleProgress" style="display: none" > </el-upload> <el-progress v-if="showProgress" :percentage="uploadProgress" status="success" class="upload-progress" ></el-progress> <div ref="editor" :style="{ height: height }"></div> </div> </template> <script> import Quill from 'quill' import 'quill/dist/quill.core.css' import 'quill/dist/quill.snow.css' import 'quill/dist/quill.bubble.css' export default { name: 'QuillEditor', props: { value: { type: String, default: '' }, height: { type: String, default: '500px' }, uploadUrl: { type: String, required: true }, headers: { type: Object, default: () => ({}) }, maxSize: { type: Number, default: 5 // MB } }, data() { return { editor: null, showProgress: false, uploadProgress: 0 } }, mounted() { this.initEditor() }, methods: { initEditor() { this.editor = new Quill(this.$refs.editor, { theme: 'snow', modules: { toolbar: { container: [ ['bold', 'italic', 'underline', 'strike'], ['blockquote', 'code-block'], [{ 'header': 1 }, { 'header': 2 }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], [{ 'indent': '-1'}, { 'indent': '+1' }], [{ 'direction': 'rtl' }], [{ 'size': ['small', false, 'large', 'huge'] }], [{ 'header': [1, 2, 3, 4, 5, 6, false] }], [{ 'color': [] }, { 'background': [] }], [{ 'font': [] }], [{ 'align': [] }], ['clean'], ['link', 'image', 'video'] ], handlers: { image: this.imageHandler } } }, placeholder: '请输入内容...' }) this.editor.on('text-change', () => { this.$emit('input', this.$refs.editor.querySelector('.ql-editor').innerHTML) }) // 粘贴图片处理 this.editor.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => { if (node.tagName === 'IMG') { const blob = this.dataURLtoBlob(node.src) if (blob) { this.uploadImageFromBlob(blob) return delta.compose(new Delta().retain(delta.length(), { image: '' })) } } return delta }) }, imageHandler() { const input = document.createElement('input') input.setAttribute('type', 'file') input.setAttribute('accept', 'image/*') input.click() input.onchange = () => { const file = input.files[0] if (!file) return if (!this.beforeUpload(file)) return this.$refs.imageUpload.$el.querySelector('input').files = input.files this.$refs.imageUpload.submit() } }, beforeUpload(file) { const isImage = file.type.includes('image/') const isLt5M = file.size / 1024 / 1024 < this.maxSize if (!isImage) { this.$message.error('只能上传图片文件!') return false } if (!isLt5M) { this.$message.error(`图片大小不能超过${this.maxSize}MB!`) return false } return true }, handleUploadSuccess(response) { this.showProgress = false this.uploadProgress = 0 if (response.code === 200) { const range = this.editor.getSelection() this.editor.insertEmbed(range.index, 'image', response.data.url) this.editor.setSelection(range.index + 1) } else { this.$message.error(response.message || '图片上传失败') } }, handleUploadError(err) { this.showProgress = false this.uploadProgress = 0 console.error('上传失败:', err) this.$message.error('图片上传失败,请重试') }, handleProgress(event) { this.showProgress = true this.uploadProgress = Math.floor(event.percent) }, dataURLtoBlob(dataurl) { const arr = dataurl.split(',') const mime = arr[0].match(/:(.*?);/)[1] const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) }, uploadImageFromBlob(blob) { const file = new File([blob], 'pasted-image.png', { type: blob.type }) this.$refs.imageUpload.$el.querySelector('input').files = [file] this.$refs.imageUpload.submit() } }, watch: { value(val) { if (val !== this.editor.root.innerHTML) { this.editor.clipboard.dangerouslyPasteHTML(val) } } } } </script> <style scoped> .editor-wrapper { position: relative; } .upload-progress { position: absolute; top: 0; left: 0; right: 0; z-index: 10; } </style>

在实际项目中,这个组件可以这样使用:

<template> <div> <quill-editor v-model="content" :upload-url="uploadUrl" :headers="headers" ></quill-editor> </div> </template> <script> import QuillEditor from '@/components/QuillEditor' export default { components: { QuillEditor }, data() { return { content: '', uploadUrl: '/api/upload', headers: { Authorization: 'Bearer ' + this.$store.getters.token } } } } </script>
http://www.jsqmd.com/news/745724/

相关文章:

  • 10_从 React Hooks 本质看 useState
  • Unlock Music:浏览器端免费解密加密音乐文件的完整实践指南
  • 如何用DS4Windows实现PS手柄在Windows上的完美游戏体验:终极配置指南
  • Java 25 ZGC 2.0低延迟调优实战(生产环境0.8ms P99停顿实录)
  • 中小团队如何利用Taotoken统一管理多个AI模型的API调用成本
  • 5分钟快速完成Axure RP免费中文汉化:终极完整指南
  • League Akari:重新定义英雄联盟的游戏助手体验
  • Depth-Anything-V2:如何在5分钟内实现高精度单目深度估计
  • 如何在Windows系统上快速部署iperf3网络性能测试工具:终极实战指南
  • Allegro PCB布线小技巧:移动元件时,如何让导线乖乖跟着走?(Options选项详解)
  • 使用 TaoToken CLI 工具一键配置开发环境与写入密钥
  • ROS2参数管理避坑指南:为什么你的RCLPY节点没收到参数变更通知?
  • 如何在Windows上使用OpenSpeedy开源游戏变速工具:3分钟快速上手终极指南
  • 别再死记硬背CNN结构了!用PyTorch手把手搭建一个图像分类器(附完整代码)
  • 跨平台漫画阅读器JHenTai:5大核心功能深度解析与使用指南
  • League Akari终极指南:英雄联盟智能游戏管家完整配置与高效使用方案
  • 告别视频下载烦恼:bilibili-parse让你的B站视频获取如此简单
  • Anthropic推出Claude Security公开测试版:AI驱动代码漏洞扫描与自动修复工具
  • Battery Toolkit:为Apple Silicon Mac延长50%电池寿命的开源电源管理解决方案
  • 别再死记硬背了!用Protege手把手教你构建知识图谱的‘骨架’(本体建模实战)
  • 局域网内实现电脑间快速传输超大文件并支持断点续传的三种工具
  • 别再手动敲公式了!用IguanaTex插件,5分钟搞定PowerPoint里的LaTeX数学公式
  • 保姆级教程:在Ubuntu22.04上为ROS2 Humble搞定CH340串口驱动与权限问题
  • PPTist终极指南:3分钟掌握免费在线PPT制作,告别PowerPoint依赖
  • 告别数据灾难:Linux下flash_erase命令的‘锁’与‘备份’实操指南
  • 终极免费OCR解决方案:如何用Umi-OCR离线批量识别图片文字
  • Windows上直接安装Android应用的终极解决方案:APK Installer使用全指南
  • 163MusicLyrics:一键获取全网音乐歌词的终极解决方案
  • 5个理由告诉你为什么TouchGAL是Galgame爱好者的终极选择
  • 使用curl命令在无图形界面虚拟机中测试Taotoken API连通性