vue3+js+elementPlus使用富文本编辑器@vueup/vue-quill详细教程
作者:FixUpSth
前言
本篇文章是基于vue3、js、elementPlus框架进行的,
主要是核心涉及的包是以下三个
"vue": "^3.2.47", "@vueup/vue-quill": "^1.0.0-alpha.40", "element-plus": "^2.3.6",
如果有问题的先看下版本是否一致。因为我每次找解决方案的时候,发现了好多问题都是版本不一致导致的。
本篇文章使用到的编辑器包含以下几个功能:
- 输入文本
- 插入图片(以img标签的形式插入,并且还能在标签上插入文件id)
- 工具栏显示为中文
- 工具栏hover后有中文提示
- 已插入的图片文件的删除
暂时没有完善的:
- 图片的大小尺寸修改和支持拖拽图片(后面如果解决了会更新,当前日期是2023年7月4日)
源码
共涉及两个文件,一个Editor/index.vue,一个Editor/quill.js
Editor/index.vue
详细目录是src/components/Editor/index.vue
<template> <el-upload :action="uploadUrl" :before-upload="handleBeforeUpload" :on-success="handleUploadSuccess" name="richTextFile" :on-error="handleUploadError" :show-file-list="false" class="editor-img-uploader" accept=".jpeg,.jpg,.png"> <i ref="uploadRef" class="Plus editor-img-uploader"></i> </el-upload> <div class="editor"> <QuillEditor id="editorId" ref="myQuillEditor" v-model:content="editorContent" contentType="html" @update:content="onContentChange" :options="options" /> </div> </template> <script setup> import { QuillEditor, Quill } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css'; import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted } from "vue"; // 引入插入图片标签自定义的类 import './quill' // 注册图片拖拽和大小修改插件(不起效果暂时屏蔽) // import { ImageDrop } from 'quill-image-drop-module'; // import {ImageResize} from 'quill-image-resize-module'; // Quill.register('modules/ImageDrop', ImageDrop); // Quill.register('modules/imageResize', ImageResize); const { proxy } = getCurrentInstance(); const emit = defineEmits(['update:content', 'getFileId', 'handleRichTextContentChange']) const props = defineProps({ /* 编辑器的内容 */ content: { type: String, default: '', }, /* 只读 */ readOnly: { type: Boolean, default: false, }, // 上传文件大小限制(MB) fileSize: { type: Number, default: 10, }, }) const editorContent = computed({ get: () => props.content, set: (val) => { emit('update:content', val) } }); const myQuillEditor = ref(null) const uploadUrl = ref(import.meta.env.VITE_BASEURL + '/sysFiles/upload') // 上传的图片服务器地址 const oldContent = ref('') const options = reactive({ theme: 'snow', debug: 'warn', modules: { // 工具栏配置 toolbar: { container: [ ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线 ['blockquote', 'code-block'], // 引用 代码块 [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表 [{ indent: '-1' }, { indent: '+1' }], // 缩进 [{ size: ['small', false, 'large', 'huge'] }], // 字体大小 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 [{ align: [] }], // 对齐方式 ['clean'], // 清除文本格式 ['link', 'image'], // 链接、图片、视频 ], handlers: { // 重写图片上传事件 image: function (value) { if (value) { //调用图片上传 proxy.$refs.uploadRef.click() } else { Quill.format("image", true); } }, }, // ImageDrop: true,//支持图片拖拽 // imageResize: { //支持图片大小尺寸修改 // displayStyles: { // backgroundColor: 'black', // border: 'none', // color: 'white' // }, // modules: ['Resize', 'DisplaySize','Toolbar'] // } } }, placeholder: '请输入公告内容...', readOnly: props.readOnly, clipboard: { matchers: [ ['img', (node, delta) => { const src = node.getAttribute('src'); const id = node.getAttribute('id'); delta.insert({ image: { src, 'id': id } }); }], ], }, }) // toolbar标题(此项是用来增加hover标题) const titleConfig = ref([ { Choice: '.ql-insertMetric', title: '跳转配置' }, { Choice: '.ql-bold', title: '加粗' }, { Choice: '.ql-italic', title: '斜体' }, { Choice: '.ql-underline', title: '下划线' }, { Choice: '.ql-header', title: '段落格式' }, { Choice: '.ql-strike', title: '删除线' }, { Choice: '.ql-blockquote', title: '块引用' }, { Choice: '.ql-code', title: '插入代码' }, { Choice: '.ql-code-block', title: '插入代码段' }, { Choice: '.ql-font', title: '字体' }, { Choice: '.ql-size', title: '字体大小' }, { Choice: '.ql-list[value="ordered"]', title: '编号列表' }, { Choice: '.ql-list[value="bullet"]', title: '项目列表' }, { Choice: '.ql-direction', title: '文本方向' }, { Choice: '.ql-header[value="1"]', title: 'h1' }, { Choice: '.ql-header[value="2"]', title: 'h2' }, { Choice: '.ql-align', title: '对齐方式' }, { Choice: '.ql-color', title: '字体颜色' }, { Choice: '.ql-background', title: '背景颜色' }, { Choice: '.ql-image', title: '图像' }, { Choice: '.ql-video', title: '视频' }, { Choice: '.ql-link', title: '添加链接' }, { Choice: '.ql-formula', title: '插入公式' }, { Choice: '.ql-clean', title: '清除字体格式' }, { Choice: '.ql-script[value="sub"]', title: '下标' }, { Choice: '.ql-script[value="super"]', title: '上标' }, { Choice: '.ql-indent[value="-1"]', title: '向左缩进' }, { Choice: '.ql-indent[value="+1"]', title: '向右缩进' }, { Choice: '.ql-header .ql-picker-label', title: '标题大小' }, { Choice: '.ql-header .ql-picker-item[data-value="1"]', title: '标题一' }, { Choice: '.ql-header .ql-picker-item[data-value="2"]', title: '标题二' }, { Choice: '.ql-header .ql-picker-item[data-value="3"]', title: '标题三' }, { Choice: '.ql-header .ql-picker-item[data-value="4"]', title: '标题四' }, { Choice: '.ql-header .ql-picker-item[data-value="5"]', title: '标题五' }, { Choice: '.ql-header .ql-picker-item[data-value="6"]', title: '标题六' }, { Choice: '.ql-header .ql-picker-item:last-child', title: '标准' }, { Choice: '.ql-size .ql-picker-item[data-value="small"]', title: '小号' }, { Choice: '.ql-size .ql-picker-item[data-value="large"]', title: '大号' }, { Choice: '.ql-size .ql-picker-item[data-value="huge"]', title: '超大号' }, { Choice: '.ql-size .ql-picker-item:nth-child(2)', title: '标准' }, { Choice: '.ql-align .ql-picker-item:first-child', title: '居左对齐' }, { Choice: '.ql-align .ql-picker-item[data-value="center"]', title: '居中对齐' }, { Choice: '.ql-align .ql-picker-item[data-value="right"]', title: '居右对齐' }, { Choice: '.ql-align .ql-picker-item[data-value="justify"]', title: '两端对齐' } ]) // 上传前校检格式和大小 function handleBeforeUpload(file) { const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]; const isJPG = type.includes(file.type); //检验文件格式 if (!isJPG) { ElMessage.error(`图片格式错误!只能上传jpeg/jpg/png格式`) return false } // 校检文件大小 if (props.fileSize) { const isLt = file.size / 1024 / 1024 < props.fileSize if (!isLt) { ElMessage.error(`上传文件大小不能超过 ${props.fileSize} MB!`) return false } } return true } // 监听富文本内容变化,删除被服务器中被用户回车删除的图片 function onContentChange(content) { emit('handleRichTextContentChange', content) } // 上传成功处理 function handleUploadSuccess(res, file) { // 如果上传成功 if (res.status == 200) { let rawMyQuillEditor = toRaw(myQuillEditor.value) // 获取富文本实例 let quill = rawMyQuillEditor.getQuill(); // 获取光标位置 let length = quill.selection.savedRange.index; // 插入图片,res为服务器返回的图片链接地址 const imageUrl = import.meta.env.VITE_BASE_FILE_PREFIX + res.body[0].lowPath; const imageId = res.body[0].id; quill.insertEmbed(length, 'image', { url: imageUrl, id: imageId, }); quill.setSelection(length + 1); emit('getFileId', res.body[0].id) } else { ElMessage.error('图片插入失败') } } // 上传失败处理 function handleUploadError() { ElMessage.error('图片插入失败') } // 增加hover工具栏有中文提示 function initTitle() { document.getElementsByClassName('ql-editor')[0].dataset.placeholder = '' for (let item of titleConfig.value) { let tip = document.querySelector('.ql-toolbar ' + item.Choice) if (!tip) continue tip.setAttribute('title', item.title) } } onMounted(() => { initTitle() oldContent.value = props.content }) </script> //通过css样式来汉化 <style> .editor, .ql-toolbar { white-space: pre-wrap !important; line-height: normal !important; } .editor-img-uploader { display: none; } .ql-editor { min-height: 200px; max-height: 300px; overflow: auto; } .ql-snow .ql-tooltip[data-mode='link']::before { content: '请输入链接地址:'; } .ql-snow .ql-tooltip.ql-editing a.ql-action::after { border-right: 0px; content: '保存'; padding-right: 0px; } .ql-snow .ql-tooltip[data-mode='video']::before { content: '请输入视频地址:'; } .ql-snow .ql-picker.ql-size .ql-picker-label::before, .ql-snow .ql-picker.ql-size .ql-picker-item::before { content: '14px'; } .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { content: '10px'; } .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { content: '18px'; } .ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, .ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { content: '32px'; } .ql-snow .ql-picker.ql-header .ql-picker-label::before, .ql-snow .ql-picker.ql-header .ql-picker-item::before { content: '文本'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { content: '标题1'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { content: '标题2'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { content: '标题3'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { content: '标题4'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { content: '标题5'; } .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { content: '标题6'; } .ql-snow .ql-picker.ql-font .ql-picker-label::before, .ql-snow .ql-picker.ql-font .ql-picker-item::before { content: '标准字体'; } .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { content: '衬线字体'; } .ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { content: '等宽字体'; } </style>
Editor/quill.js
用于使得插入图片标签的时候能够插入id在图片标签上,不然直接使用insertEmbed方法是无法插入id在img标签上的
import { Quill } from '@vueup/vue-quill' var BlockEmbed = Quill.import('blots/block/embed') class ImageBlot extends BlockEmbed { static create(value) { let node = super.create(); node.setAttribute('src', value.url); node.setAttribute('id', value.id) // node.setAttribute('width', value.width) // node.setAttribute('height', value.height) return node; } static value(node) { return { url: node.getAttribute('src'), id: node.getAttribute('id'), } } } ImageBlot.blotName = 'image'; ImageBlot.tagName = 'img'; Quill.register(ImageBlot)
父组件中的handleRichTextContentChange事件
// 根据富文本实时变化,观察有没有删除已经上传的id function handleRichTextContentChange(content) { const currentIds = getRichTextIds(content) if (uploadedRichTextIds.value.length > 0) { // 拿当前form里面已经上传的id来进行查询,如果不存在currentIds里面,则已经被删除 uploadedRichTextIds.value.find(oldId => { if (!currentIds.includes(oldId) && !removedRichTextIds.value.includes(oldId)) { removedRichTextIds.value.push(oldId) //向删除的id里面推入被删除的项 let index = uploadedRichTextIds.value.indexOf(oldId) uploadedRichTextIds.value.splice(index, 1) //删除已上传的过程记录变量 } }) } }
父组件的getFileId方法
// 富文本组件随时更新已经上传的富文本id function getFileId(id) { uploadedRichTextIds.value.push(id) console.log('uploadedRichTextIds', uploadedRichTextIds.value); }
父组件的getRichTextIds 方法,用于获取富文本中含有的图片的id集合
/** * * @param {String} content //富文本字符串 * @param {Array} ids //富文本里面的图片文件id集合 */ function getRichTextIds(content) { const ids = [] const myDiv = document.createElement("div"); myDiv.innerHTML = content; const imgDom = myDiv.getElementsByTagName('img') for (let i = 0; i < imgDom.length; i++) { // 只有富文本处的img标签是有id的 if (imgDom[i].src && imgDom[i].id) { ids.push(imgDom[i].id) } } return ids }
最终我会向后端提交removedRichTextIds,这些是已经在富文本编辑过程中已经上传到服务器中的文件id,需要被删除掉,不然服务器会一直存储着这些文件,造成服务器的空间紧张
整体思路
文本输入、汉化工具栏、增加hover提示整体都是比较简单的传统思路,只是上传图片没有采用base64的方式,是因为base64插入一两张后,整个富文本就会变得巨大无比,导致整个页面加载都非常卡顿,因此只能采用插入img标签的形式。在插入img标签之后需要被回显成正常的图片,因此也就只能实时上传,用后端返回的路径来拼接显示。
虽然这样轻量了,但是问题也来了,如果用户使用回车删除了该图片,在服务器还是会存在该张图片。因此在用户删除时,也要删除服务器中该文件。
因此,我们通过id来确定用户到底删除的是哪张图片。首先在插入图片时,就将upload后后端返回的id插入到对应图片的img标签上,用id属性名=id属性值的方式绑定到img标签上。同时使用一个记录变量uploadedRichTextIds 来记录已经上传的id,通过富文本编辑器本身自带的事件change来监听当前的富文本内容,通过getRichTextIds方法获取当前富文本中的img标签里面的id组合,和uploadedRichTextIds中的id进行比对,这便知道哪些是已经上传过但是又被用户删除的文件了。这个地方是我的难点,因此我想记录一下。
待解决
最后,我想加入图片可以自由调节大小,可拖拽的插件,但是在网上寻求了很多解决方案,始终没有解决,如果有朋友解决了这个问题,麻烦评论区回复我一下,因为富文本编辑器真的经常要用到!!非常感谢,如果我解决了我也会及时更新的!!
总结
到此这篇关于vue3+js+elementPlus使用富文本编辑器@vueup/vue-quill的文章就介绍到这了,更多相关vue3+js+elementPlus富文本编辑器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!