vue3+js+elementPlus使用富文本編輯器@vueup/vue-quill詳細教程
前言
本篇文章是基于vue3、js、elementPlus框架進行的,
主要是核心涉及的包是以下三個
"vue": "^3.2.47", "@vueup/vue-quill": "^1.0.0-alpha.40", "element-plus": "^2.3.6",
如果有問題的先看下版本是否一致。因為我每次找解決方案的時候,發(fā)現(xiàn)了好多問題都是版本不一致導(dǎo)致的。
本篇文章使用到的編輯器包含以下幾個功能:
- 輸入文本
- 插入圖片(以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({ /* 編輯器的內(nèi)容 */ 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') // 上傳的圖片服務(wù)器地址 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) { //調(diào)用圖片上傳 proxy.$refs.uploadRef.click() } else { Quill.format("image", true); } }, }, // ImageDrop: true,//支持圖片拖拽 // imageResize: { //支持圖片大小尺寸修改 // displayStyles: { // backgroundColor: 'black', // border: 'none', // color: 'white' // }, // modules: ['Resize', 'DisplaySize','Toolbar'] // } } }, placeholder: '請輸入公告內(nèi)容...', 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: '跳轉(zhuǎn)配置' }, { 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 } // 監(jiān)聽富文本內(nèi)容變化,刪除被服務(wù)器中被用戶回車刪除的圖片 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為服務(wù)器返回的圖片鏈接地址 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事件
// 根據(jù)富文本實時變化,觀察有沒有刪除已經(jīng)上傳的id function handleRichTextContentChange(content) { const currentIds = getRichTextIds(content) if (uploadedRichTextIds.value.length > 0) { // 拿當前form里面已經(jīng)上傳的id來進行查詢,如果不存在currentIds里面,則已經(jīng)被刪除 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方法
// 富文本組件隨時更新已經(jīng)上傳的富文本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,這些是已經(jīng)在富文本編輯過程中已經(jīng)上傳到服務(wù)器中的文件id,需要被刪除掉,不然服務(wù)器會一直存儲著這些文件,造成服務(wù)器的空間緊張
整體思路
文本輸入、漢化工具欄、增加hover提示整體都是比較簡單的傳統(tǒng)思路,只是上傳圖片沒有采用base64的方式,是因為base64插入一兩張后,整個富文本就會變得巨大無比,導(dǎo)致整個頁面加載都非??D,因此只能采用插入img標簽的形式。在插入img標簽之后需要被回顯成正常的圖片,因此也就只能實時上傳,用后端返回的路徑來拼接顯示。
雖然這樣輕量了,但是問題也來了,如果用戶使用回車刪除了該圖片,在服務(wù)器還是會存在該張圖片。因此在用戶刪除時,也要刪除服務(wù)器中該文件。
因此,我們通過id來確定用戶到底刪除的是哪張圖片。首先在插入圖片時,就將upload后后端返回的id插入到對應(yīng)圖片的img標簽上,用id屬性名=id屬性值的方式綁定到img標簽上。同時使用一個記錄變量uploadedRichTextIds 來記錄已經(jīng)上傳的id,通過富文本編輯器本身自帶的事件change來監(jiān)聽當前的富文本內(nèi)容,通過getRichTextIds方法獲取當前富文本中的img標簽里面的id組合,和uploadedRichTextIds中的id進行比對,這便知道哪些是已經(jīng)上傳過但是又被用戶刪除的文件了。這個地方是我的難點,因此我想記錄一下。
待解決
最后,我想加入圖片可以自由調(diào)節(jié)大小,可拖拽的插件,但是在網(wǎng)上尋求了很多解決方案,始終沒有解決,如果有朋友解決了這個問題,麻煩評論區(qū)回復(fù)我一下,因為富文本編輯器真的經(jīng)常要用到!!非常感謝,如果我解決了我也會及時更新的??!
總結(jié)
到此這篇關(guān)于vue3+js+elementPlus使用富文本編輯器@vueup/vue-quill的文章就介紹到這了,更多相關(guān)vue3+js+elementPlus富文本編輯器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue升級帶來的elementui沖突警告:Invalid prop: custom va
在頁面渲染的時候,控制臺彈出大量警告,嚴重影響控制臺的信息獲取功能,但是頁面基本能正常顯示,這是因為Vue升級帶來的elementui沖突警告: Invalid prop: custom validator check failed for prop “type“.的解決方案,本文給大家介紹了詳細的解決方案2025-04-04Vue 中 reactive創(chuàng)建對象類型響應(yīng)式數(shù)據(jù)的方法
在 Vue 的開發(fā)世界里,響應(yīng)式數(shù)據(jù)是構(gòu)建交互性良好應(yīng)用的基礎(chǔ),之前我們了解了ref用于定義基本類型的數(shù)據(jù),今天就來深入探討一下如何使用reactive定義對象類型的響應(yīng)式數(shù)據(jù),感興趣的朋友一起看看吧2025-02-02Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示
這篇文章主要介紹了Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04Vue子組件關(guān)閉后調(diào)用刷新父組件的實現(xiàn)
這篇文章主要介紹了Vue子組件關(guān)閉后調(diào)用刷新父組件的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03