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 --saveQuill编辑器需要引入对应的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>