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",
如果有問題的先看下版本是否一致。因?yàn)槲颐看握医鉀Q方案的時(shí)候,發(fā)現(xiàn)了好多問題都是版本不一致導(dǎo)致的。
本篇文章使用到的編輯器包含以下幾個(gè)功能:
- 輸入文本
- 插入圖片(以img標(biāo)簽的形式插入,并且還能在標(biāo)簽上插入文件id)
- 工具欄顯示為中文
- 工具欄hover后有中文提示
- 已插入的圖片文件的刪除
暫時(shí)沒有完善的:
- 圖片的大小尺寸修改和支持拖拽圖片(后面如果解決了會(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'
// 注冊圖片拖拽和大小修改插件(不起效果暫時(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' }], // 有序、無序列表
[{ 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: {
// 重寫圖片上傳事件
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)是用來增加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(`上傳文件大小不能超過 ${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)
// 獲取富文本實(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>
//通過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: '請(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方法是無法插入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í)變化,觀察有沒有刪除已經(jīng)上傳的id
function handleRichTextContentChange(content) {
const currentIds = getRichTextIds(content)
if (uploadedRichTextIds.value.length > 0) {
// 拿當(dāng)前form里面已經(jīng)上傳的id來進(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) //刪除已上傳的過程記錄變量
}
})
}
}
父組件的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)在富文本編輯過程中已經(jīng)上傳到服務(wù)器中的文件id,需要被刪除掉,不然服務(wù)器會(huì)一直存儲(chǔ)著這些文件,造成服務(wù)器的空間緊張
整體思路
文本輸入、漢化工具欄、增加hover提示整體都是比較簡單的傳統(tǒng)思路,只是上傳圖片沒有采用base64的方式,是因?yàn)閎ase64插入一兩張后,整個(gè)富文本就會(huì)變得巨大無比,導(dǎo)致整個(gè)頁面加載都非??D,因此只能采用插入img標(biāo)簽的形式。在插入img標(biāo)簽之后需要被回顯成正常的圖片,因此也就只能實(shí)時(shí)上傳,用后端返回的路徑來拼接顯示。
雖然這樣輕量了,但是問題也來了,如果用戶使用回車刪除了該圖片,在服務(wù)器還是會(huì)存在該張圖片。因此在用戶刪除時(shí),也要?jiǎng)h除服務(wù)器中該文件。
因此,我們通過id來確定用戶到底刪除的是哪張圖片。首先在插入圖片時(shí),就將upload后后端返回的id插入到對(duì)應(yīng)圖片的img標(biāo)簽上,用id屬性名=id屬性值的方式綁定到img標(biāo)簽上。同時(shí)使用一個(gè)記錄變量uploadedRichTextIds 來記錄已經(jīng)上傳的id,通過富文本編輯器本身自帶的事件change來監(jiān)聽當(dāng)前的富文本內(nèi)容,通過getRichTextIds方法獲取當(dāng)前富文本中的img標(biāo)簽里面的id組合,和uploadedRichTextIds中的id進(jìn)行比對(duì),這便知道哪些是已經(jīng)上傳過但是又被用戶刪除的文件了。這個(gè)地方是我的難點(diǎn),因此我想記錄一下。
待解決
最后,我想加入圖片可以自由調(diào)節(jié)大小,可拖拽的插件,但是在網(wǎng)上尋求了很多解決方案,始終沒有解決,如果有朋友解決了這個(gè)問題,麻煩評(píng)論區(qū)回復(fù)我一下,因?yàn)楦晃谋揪庉嬈髡娴慕?jīng)常要用到??!非常感謝,如果我解決了我也會(huì)及時(shí)更新的??!
總結(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通過JSON渲染ElementPlus表單的流程步驟
- Vue3+ElementPlus封裝圖片空間組件的門面實(shí)例
- vue3基于elementplus 簡單實(shí)現(xiàn)表格二次封裝過程
- vue3+elementplus 樹節(jié)點(diǎn)過濾功能實(shí)現(xiàn)
- 如何在Vue3中正確使用ElementPlus,親測有效,避坑
- vue3+elementplus前端生成圖片驗(yàn)證碼完整代碼舉例
- Vue3 + ElementPlus動(dòng)態(tài)合并數(shù)據(jù)相同的單元格的完整代碼
相關(guān)文章
關(guān)于Vue背景圖打包之后訪問路徑錯(cuò)誤問題的解決
本篇文章主要介紹了關(guān)于Vue背景圖打包之后訪問路徑錯(cuò)誤問題的解決,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11
vue學(xué)習(xí)筆記之過濾器的基本使用方法實(shí)例分析
這篇文章主要介紹了vue學(xué)習(xí)筆記之過濾器的基本使用方法,結(jié)合實(shí)例形式分析了vue.js過濾器的基本功能、用法與操作注意事項(xiàng),需要的朋友可以參考下2020-02-02
el-date-picker默認(rèn)結(jié)束為當(dāng)前時(shí)分秒的操作方法
在element?ui中的日期時(shí)間選擇組件中默認(rèn)是00:00,現(xiàn)在需求是點(diǎn)擊默認(rèn)結(jié)束時(shí)間為當(dāng)前時(shí)分秒,查了很多資料寫的都不準(zhǔn)確?,今天給大家分享el-date-picker默認(rèn)結(jié)束為當(dāng)前時(shí)分秒的操作方法,感興趣的朋友一起看看吧2024-01-01
Vue 中文本內(nèi)容超出規(guī)定行數(shù)后展開收起的處理的實(shí)現(xiàn)方法
這篇文章主要介紹了Vue 中文本內(nèi)容超出規(guī)定行數(shù)后展開收起的處理的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

