vue3+js+elementPlus使用富文本編輯器@vueup/vue-quill詳細(xì)教程
前言
本篇文章是基于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標(biāo)簽的形式插入,并且還能在標(biāo)簽上插入文件id)
- 工具欄顯示為中文
- 工具欄hover后有中文提示
- 已插入的圖片文件的刪除
暫時沒有完善的:
- 圖片的大小尺寸修改和支持拖拽圖片(后面如果解決了會更新,當(dāng)前日期是2023年7月4日)
源碼
共涉及兩個文件,一個Editor/index.vue,一個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'
// 注冊圖片拖拽和大小修改插件(不起效果暫時屏蔽)
// 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] }], // 標(biāo)題
[{ 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標(biāo)題(此項是用來增加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: '編號列表' },
{ 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: '下標(biāo)' },
{ Choice: '.ql-script[value="super"]', title: '上標(biāo)' },
{ Choice: '.ql-indent[value="-1"]', title: '向左縮進' },
{ Choice: '.ql-indent[value="+1"]', title: '向右縮進' },
{ 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: '小號' },
{ 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: '標(biāo)準(zhǔn)' },
{ 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();
// 獲取光標(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>
//通過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: '標(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)簽的時候能夠插入id在圖片標(biāo)簽上,不然直接使用insertEmbed方法是無法插入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ù)富文本實時變化,觀察有沒有刪除已經(jīng)上傳的id
function handleRichTextContentChange(content) {
const currentIds = getRichTextIds(content)
if (uploadedRichTextIds.value.length > 0) {
// 拿當(dāng)前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標(biāo)簽是有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)致整個頁面加載都非常卡頓,因此只能采用插入img標(biāo)簽的形式。在插入img標(biāo)簽之后需要被回顯成正常的圖片,因此也就只能實時上傳,用后端返回的路徑來拼接顯示。
雖然這樣輕量了,但是問題也來了,如果用戶使用回車刪除了該圖片,在服務(wù)器還是會存在該張圖片。因此在用戶刪除時,也要刪除服務(wù)器中該文件。
因此,我們通過id來確定用戶到底刪除的是哪張圖片。首先在插入圖片時,就將upload后后端返回的id插入到對應(yīng)圖片的img標(biāo)簽上,用id屬性名=id屬性值的方式綁定到img標(biāo)簽上。同時使用一個記錄變量uploadedRichTextIds 來記錄已經(jīng)上傳的id,通過富文本編輯器本身自帶的事件change來監(jiān)聽當(dāng)前的富文本內(nèi)容,通過getRichTextIds方法獲取當(dāng)前富文本中的img標(biāo)簽里面的id組合,和uploadedRichTextIds中的id進行比對,這便知道哪些是已經(jīng)上傳過但是又被用戶刪除的文件了。這個地方是我的難點,因此我想記錄一下。
待解決
最后,我想加入圖片可以自由調(diào)節(jié)大小,可拖拽的插件,但是在網(wǎng)上尋求了很多解決方案,始終沒有解決,如果有朋友解決了這個問題,麻煩評論區(qū)回復(fù)我一下,因為富文本編輯器真的經(jīng)常要用到?。》浅8兄x,如果我解決了我也會及時更新的??!
總結(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
在頁面渲染的時候,控制臺彈出大量警告,嚴(yán)重影響控制臺的信息獲取功能,但是頁面基本能正常顯示,這是因為Vue升級帶來的elementui沖突警告: Invalid prop: custom validator check failed for prop “type“.的解決方案,本文給大家介紹了詳細(xì)的解決方案2025-04-04
Vue 中 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-02
Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示
這篇文章主要介紹了Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue子組件關(guān)閉后調(diào)用刷新父組件的實現(xiàn)
這篇文章主要介紹了Vue子組件關(guān)閉后調(diào)用刷新父組件的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03

