vue3+js+elementPlus使用富文本編輯器@vueup/vue-quill詳細(xì)教程
前言
本篇文章是基于vue3、js、elementPlus框架進(jìn)行的,
主要是核心涉及的包是以下三個(gè)
"vue": "^3.2.47", "@vueup/vue-quill": "^1.0.0-alpha.40", "element-plus": "^2.3.6",
如果有問(wèn)題的先看下版本是否一致。因?yàn)槲颐看握医鉀Q方案的時(shí)候,發(fā)現(xiàn)了好多問(wèn)題都是版本不一致導(dǎo)致的。
本篇文章使用到的編輯器包含以下幾個(gè)功能:
- 輸入文本
- 插入圖片(以img標(biāo)簽的形式插入,并且還能在標(biāo)簽上插入文件id)
- 工具欄顯示為中文
- 工具欄hover后有中文提示
- 已插入的圖片文件的刪除
暫時(shí)沒(méi)有完善的:
- 圖片的大小尺寸修改和支持拖拽圖片(后面如果解決了會(huì)更新,當(dāng)前日期是2023年7月4日)
源碼
共涉及兩個(gè)文件,一個(gè)Editor/index.vue,一個(gè)Editor/quill.js
Editor/index.vue
詳細(xì)目錄是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"; // 引入插入圖片標(biāo)簽自定義的類 import './quill' // 注冊(cè)圖片拖拽和大小修改插件(不起效果暫時(shí)屏蔽) // 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' }], // 有序、無(wú)序列表 [{ indent: '-1' }, { indent: '+1' }], // 縮進(jìn) [{ size: ['small', false, 'large', 'huge'] }], // 字體大小 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 標(biāo)題 [{ color: [] }, { background: [] }], // 字體顏色、字體背景顏色 [{ align: [] }], // 對(duì)齊方式 ['clean'], // 清除文本格式 ['link', 'image'], // 鏈接、圖片、視頻 ], handlers: { // 重寫(xiě)圖片上傳事件 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: '請(qǐng)輸入公告內(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標(biāo)題(此項(xiàng)是用來(lái)增加hover標(biāo)題) 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: '編號(hào)列表' }, { Choice: '.ql-list[value="bullet"]', title: '項(xiàng)目列表' }, { Choice: '.ql-direction', title: '文本方向' }, { Choice: '.ql-header[value="1"]', title: 'h1' }, { Choice: '.ql-header[value="2"]', title: 'h2' }, { Choice: '.ql-align', title: '對(duì)齊方式' }, { 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: '下標(biāo)' }, { Choice: '.ql-script[value="super"]', title: '上標(biāo)' }, { Choice: '.ql-indent[value="-1"]', title: '向左縮進(jìn)' }, { Choice: '.ql-indent[value="+1"]', title: '向右縮進(jìn)' }, { Choice: '.ql-header .ql-picker-label', title: '標(biāo)題大小' }, { Choice: '.ql-header .ql-picker-item[data-value="1"]', title: '標(biāo)題一' }, { Choice: '.ql-header .ql-picker-item[data-value="2"]', title: '標(biāo)題二' }, { Choice: '.ql-header .ql-picker-item[data-value="3"]', title: '標(biāo)題三' }, { Choice: '.ql-header .ql-picker-item[data-value="4"]', title: '標(biāo)題四' }, { Choice: '.ql-header .ql-picker-item[data-value="5"]', title: '標(biāo)題五' }, { Choice: '.ql-header .ql-picker-item[data-value="6"]', title: '標(biāo)題六' }, { Choice: '.ql-header .ql-picker-item:last-child', title: '標(biāo)準(zhǔn)' }, { Choice: '.ql-size .ql-picker-item[data-value="small"]', title: '小號(hào)' }, { Choice: '.ql-size .ql-picker-item[data-value="large"]', title: '大號(hào)' }, { Choice: '.ql-size .ql-picker-item[data-value="huge"]', title: '超大號(hào)' }, { Choice: '.ql-size .ql-picker-item:nth-child(2)', title: '標(biāo)準(zhǔn)' }, { Choice: '.ql-align .ql-picker-item:first-child', title: '居左對(duì)齊' }, { Choice: '.ql-align .ql-picker-item[data-value="center"]', title: '居中對(duì)齊' }, { Choice: '.ql-align .ql-picker-item[data-value="right"]', title: '居右對(duì)齊' }, { Choice: '.ql-align .ql-picker-item[data-value="justify"]', title: '兩端對(duì)齊' } ]) // 上傳前校檢格式和大小 function handleBeforeUpload(file) { const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]; const isJPG = type.includes(file.type); //檢驗(yàn)文件格式 if (!isJPG) { ElMessage.error(`圖片格式錯(cuò)誤!只能上傳jpeg/jpg/png格式`) return false } // 校檢文件大小 if (props.fileSize) { const isLt = file.size / 1024 / 1024 < props.fileSize if (!isLt) { ElMessage.error(`上傳文件大小不能超過(guò) ${props.fileSize} MB!`) return false } } return true } // 監(jiān)聽(tīng)富文本內(nèi)容變化,刪除被服務(wù)器中被用戶回車刪除的圖片 function onContentChange(content) { emit('handleRichTextContentChange', content) } // 上傳成功處理 function handleUploadSuccess(res, file) { // 如果上傳成功 if (res.status == 200) { let rawMyQuillEditor = toRaw(myQuillEditor.value) // 獲取富文本實(shí)例 let quill = rawMyQuillEditor.getQuill(); // 獲取光標(biāo)位置 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> //通過(guò)css樣式來(lái)漢化 <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: '請(qǐng)輸入鏈接地址:'; } .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: '請(qǐng)輸入視頻地址:'; } .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: '標(biāo)題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: '標(biāo)題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: '標(biāo)題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: '標(biāo)題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: '標(biāo)題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: '標(biāo)題6'; } .ql-snow .ql-picker.ql-font .ql-picker-label::before, .ql-snow .ql-picker.ql-font .ql-picker-item::before { content: '標(biāo)準(zhǔn)字體'; } .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
用于使得插入圖片標(biāo)簽的時(shí)候能夠插入id在圖片標(biāo)簽上,不然直接使用insertEmbed方法是無(wú)法插入id在img標(biāo)簽上的
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ù)富文本實(shí)時(shí)變化,觀察有沒(méi)有刪除已經(jīng)上傳的id function handleRichTextContentChange(content) { const currentIds = getRichTextIds(content) if (uploadedRichTextIds.value.length > 0) { // 拿當(dāng)前form里面已經(jīng)上傳的id來(lái)進(jìn)行查詢,如果不存在currentIds里面,則已經(jīng)被刪除 uploadedRichTextIds.value.find(oldId => { if (!currentIds.includes(oldId) && !removedRichTextIds.value.includes(oldId)) { removedRichTextIds.value.push(oldId) //向刪除的id里面推入被刪除的項(xiàng) let index = uploadedRichTextIds.value.indexOf(oldId) uploadedRichTextIds.value.splice(index, 1) //刪除已上傳的過(guò)程記錄變量 } }) } }
父組件的getFileId方法
// 富文本組件隨時(shí)更新已經(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標(biāo)簽是有id的 if (imgDom[i].src && imgDom[i].id) { ids.push(imgDom[i].id) } } return ids }
最終我會(huì)向后端提交removedRichTextIds,這些是已經(jīng)在富文本編輯過(guò)程中已經(jīng)上傳到服務(wù)器中的文件id,需要被刪除掉,不然服務(wù)器會(huì)一直存儲(chǔ)著這些文件,造成服務(wù)器的空間緊張
整體思路
文本輸入、漢化工具欄、增加hover提示整體都是比較簡(jiǎn)單的傳統(tǒng)思路,只是上傳圖片沒(méi)有采用base64的方式,是因?yàn)閎ase64插入一兩張后,整個(gè)富文本就會(huì)變得巨大無(wú)比,導(dǎo)致整個(gè)頁(yè)面加載都非常卡頓,因此只能采用插入img標(biāo)簽的形式。在插入img標(biāo)簽之后需要被回顯成正常的圖片,因此也就只能實(shí)時(shí)上傳,用后端返回的路徑來(lái)拼接顯示。
雖然這樣輕量了,但是問(wèn)題也來(lái)了,如果用戶使用回車刪除了該圖片,在服務(wù)器還是會(huì)存在該張圖片。因此在用戶刪除時(shí),也要?jiǎng)h除服務(wù)器中該文件。
因此,我們通過(guò)id來(lái)確定用戶到底刪除的是哪張圖片。首先在插入圖片時(shí),就將upload后后端返回的id插入到對(duì)應(yīng)圖片的img標(biāo)簽上,用id屬性名=id屬性值的方式綁定到img標(biāo)簽上。同時(shí)使用一個(gè)記錄變量uploadedRichTextIds 來(lái)記錄已經(jīng)上傳的id,通過(guò)富文本編輯器本身自帶的事件change來(lái)監(jiān)聽(tīng)當(dāng)前的富文本內(nèi)容,通過(guò)getRichTextIds方法獲取當(dāng)前富文本中的img標(biāo)簽里面的id組合,和uploadedRichTextIds中的id進(jìn)行比對(duì),這便知道哪些是已經(jīng)上傳過(guò)但是又被用戶刪除的文件了。這個(gè)地方是我的難點(diǎn),因此我想記錄一下。
待解決
最后,我想加入圖片可以自由調(diào)節(jié)大小,可拖拽的插件,但是在網(wǎng)上尋求了很多解決方案,始終沒(méi)有解決,如果有朋友解決了這個(gè)問(wèn)題,麻煩評(píng)論區(qū)回復(fù)我一下,因?yàn)楦晃谋揪庉嬈髡娴慕?jīng)常要用到!!非常感謝,如果我解決了我也會(huì)及時(shí)更新的?。?/p>
總結(jié)
到此這篇關(guān)于vue3+js+elementPlus使用富文本編輯器@vueup/vue-quill的文章就介紹到這了,更多相關(guān)vue3+js+elementPlus富文本編輯器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue3+Vite+ElementPlus構(gòu)建學(xué)習(xí)筆記
- Vue3通過(guò)JSON渲染ElementPlus表單的流程步驟
- Vue3+ElementPlus封裝圖片空間組件的門面實(shí)例
- vue3基于elementplus 簡(jiǎn)單實(shí)現(xiàn)表格二次封裝過(guò)程
- vue3+elementplus 樹(shù)節(jié)點(diǎn)過(guò)濾功能實(shí)現(xiàn)
- 如何在Vue3中正確使用ElementPlus,親測(cè)有效,避坑
- vue3+elementplus前端生成圖片驗(yàn)證碼完整代碼舉例
- Vue3 + ElementPlus動(dòng)態(tài)合并數(shù)據(jù)相同的單元格的完整代碼
相關(guān)文章
Vue升級(jí)帶來(lái)的elementui沖突警告:Invalid prop: custom va
在頁(yè)面渲染的時(shí)候,控制臺(tái)彈出大量警告,嚴(yán)重影響控制臺(tái)的信息獲取功能,但是頁(yè)面基本能正常顯示,這是因?yàn)閂ue升級(jí)帶來(lái)的elementui沖突警告: Invalid prop: custom validator check failed for prop “type“.的解決方案,本文給大家介紹了詳細(xì)的解決方案2025-04-04Vue 中 reactive創(chuàng)建對(duì)象類型響應(yīng)式數(shù)據(jù)的方法
在 Vue 的開(kāi)發(fā)世界里,響應(yīng)式數(shù)據(jù)是構(gòu)建交互性良好應(yīng)用的基礎(chǔ),之前我們了解了ref用于定義基本類型的數(shù)據(jù),今天就來(lái)深入探討一下如何使用reactive定義對(duì)象類型的響應(yīng)式數(shù)據(jù),感興趣的朋友一起看看吧2025-02-02Vue實(shí)現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示
這篇文章主要介紹了Vue實(shí)現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04Vue子組件關(guān)閉后調(diào)用刷新父組件的實(shí)現(xiàn)
這篇文章主要介紹了Vue子組件關(guān)閉后調(diào)用刷新父組件的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03el-table樹(shù)形數(shù)據(jù)序號(hào)排序處理方案
這篇文章主要介紹了el-table樹(shù)形數(shù)據(jù)序號(hào)排序處理方案,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-03-03