ElementUI中使用富文本编辑器(quill-editor)
本文相当于是承上启下的一篇文章,之前的博客讲了如何文件上传,富文本编辑器需要利用到
文件上传,而楼主的下一个项目,需要对文章进行管理,所以今天来封装一个富文本编辑器(在之前的博客里用到了UEditor,当时出于官网太久没有更新),换了一个富文本编辑器使用。
一、引入quill-editor
安装依赖,需要先安装vue-quill-editor插件
npm install vue-quill-editor --save
npm install quill
安装图片插件(图片拖拽和修改图片大小),如果你有这个方面的需求的话,可以引入,否则忽略
npm install quill-image-drop-module --save
npm install quill-image-resize-module --save
二、使用quill-editor
全局引入
我们安装完成之后,可以在main.js中全局引入quill-editor
import VueQuillEditor from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
Vue.use(VueQuillEditor)
import * as Quill from 'quill'
import { ImageDrop } from 'quill-image-drop-module'
Quill.register('modules/imageDrop', ImageDrop)
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageResize', ImageResize)
按需引入
用到的时候,再引入
import VueQuillEditor from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import * as Quill from 'quill'
import { ImageDrop } from 'quill-image-drop-module'
import ImageDrop from 'quill-image-resize-module'
export default {
components: {
VueQuillEditor,
ImageDrop,
ImageDrop
}
}
配置修改
使用图片与视频的上传功能,请在此之前完成上传组件,为其 class 赋值,并隐藏该组件
<el-upload
class="avatar-uploader"
:action="uploadImgUrl"
:accept="'image/*,video/*'"
:show-file-list="false"
:on-success="uploadEditorSuccess"
:on-error="uploadEditorError"
:before-upload="beforeEditorUpload"
:headers="headers">
</el-upload>
视频修改
如果需要用到视频上传,且希望视频可以正常回显,需要修改quill源码中视频 标签为
写一个quillVideo.js
import { Quill } from 'vue-quill-editor'
const BlockEmbed = Quill.import('blots/block/embed')
const ATTRIBUTES = ['height', 'width']
class Video extends BlockEmbed {
static create (value) {
const node = super.create(value)
node.setAttribute('controls', 'controls')
node.setAttribute('controlsList', 'nofullscreen')
node.setAttribute('type', 'video/mp4')
node.setAttribute('style', 'object-fit:fill;width: 100%;')
node.setAttribute('preload', 'auto')
node.setAttribute('playsinline', 'true')
node.setAttribute('x-webkit-airplay', 'allow')
node.setAttribute('x5-video-orientation', 'portraint')
node.setAttribute('x5-playsinline', 'true')
node.setAttribute('x5-video-player-fullscreen', 'true')
node.setAttribute('src', window.jsValue)
return node
}
static formats (domNode) {
return ATTRIBUTES.reduce((formats, attribute) => {
if (domNode.hasAttribute(attribute)) {
formats[attribute] = domNode.getAttribute(attribute)
}
return formats
}, {})
}
static value (domNode) {
return domNode.getAttribute('src')
}
format (name, value) {
if (ATTRIBUTES.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value)
} else {
this.domNode.removeAttribute(name)
}
} else {
super.format(name, value)
}
}
html () {
const { video } = this.value()
return `<a href="${video}">${video}</a>`
}
}
Video.blotName = 'video'
Video.className = 'ql-video'
Video.tagName = 'video'
export default Video
在quillEditor界面写
window.jsValue=imgUrl;
滚动效果
富文本里面的下拉框默认是不滚动的,想要滚动效果,加上下面的css
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
border-color: #ccc;
height: 125px;
overflow: auto;
}
添加表情包
添加插件
npm i node-emoji
或者
npm install node-emoji
在main.js将它挂载到vue原型上
import emoji from 'node-emoji'
Vue.prototype.emoji = emoji.
添加插件
npm isntall quill-emoji
在main.js中加入(全局注册)
import 'quill/dist/quill.bubble.css'
import quillEmoji from 'quill-emoji'
注册
Quill.register('modules/quillEmoji', quillEmoji)//将emoji插件注册进富文本
在main.js中添加(注意导入的位置,否则可能会报错,前端界面启动不起来)
import quillEmoji from 'quill-emoji'
import 'quill-emoji/dist/quill-emoji.css'、
Quill.register('modules/quillEmoji', quillEmoji)
悬停加上中文释义
先定义一个数组,把所有的工具放在里面
const titleConfig = [
{ 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-emoji', 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: '两端对齐' }
]
在页面上已经渲染好元素之后,执行下面函数,放在onEditorReady函数内
for (let item of titleConfig) {
let tip = document.querySelector('.quill-editor ' + item.Choice)
if (!tip) continue
tip.setAttribute('title', item.title)
}
具体使用
写一个quillEditor.vue
<template>
<div>
<div id='quillEditorQiniu'>
<!-- 基于elementUi的上传组件 el-upload begin-->
<el-upload
class="avatar-uploader"
:action="uploadImgUrl"
:accept="'image/*,video/*'"
:show-file-list="false"
:on-success="uploadEditorSuccess"
:on-error="uploadEditorError"
:before-upload="beforeEditorUpload"
:headers="headers">
</el-upload>
<!-- 基于elementUi的上传组件 el-upload end-->
<quill-editor class="editor" v-model="content" ref="customQuillEditor" :options="editorOption" @ready="onEditorReady($event)" >
</quill-editor>
</div>
</div>
</template>
<script>
import * as Quill from 'quill'
// 这里引入修改过的video模块并注册
import Video from "@/components/quillEditor/quillVideo";
Quill.register(Video, true)
//自定义编辑器的工作条
import {getToken} from "@/utils/auth";
// toolbar标题
const titleConfig = [
{ 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-emoji', 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="justify"]', title: '两端对齐' },
// { Choice: '.ql-table', title: '插入表格' },
// { Choice: '.ql-table-insert-row', title: '插入行' },
// { Choice: '.ql-table-insert-column', title: '插入列' },
// { Choice: '.ql-table-delete-row', title: '删除行' },
// { Choice: '.ql-table-delete-column', title: '删除列' },
]
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
['blockquote', 'code-block'], // 引用 代码块
[{ header: 1 }, { header: 2 }], // 1、2 级标题
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
[{ script: 'sub' }, { script: 'super' }], // 上标/下标
[{ indent: '-1' }, { indent: '+1' }], // 缩进
[{ direction: 'rtl' }], // 文本方向
[{ size: ['small', false, 'large', 'huge'] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ font: ['songti'] }], // 字体种类
[{ align: [] }], // 对齐方式
// [
// {'table': 'TD'},
// {'table-insert-row': 'TIR'},
// {'table-insert-column': 'TIC'},
// {'table-delete-row': 'TDR'},
// {'table-delete-column': 'TDC'}
// ],
['image', 'video'], // 链接、图片、视频
['emoji'],//表情包
['clean'], // 清除文本格式
];
export default {
data(){
return {
headers: {
Authorization: "Bearer " + getToken(),
},
uploadImgUrl:"/v1/admin/common/upload",
uploadUrlPath:"没有文件上传",
quillUpdateImg:false,
content:'', //最终保存的内容
editorOption:{
placeholder:'请输入内容',
modules: {
'emoji-toolbar': true,
"emoji-textarea": true,
'emoji-shortname': true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
},
toolbar: {
container: toolbarOptions, // 工具栏
table: true, // 设为true
handlers: {
'image': function (value) {
if (value) {
document.querySelector('#quillEditorQiniu .avatar-uploader input').click()
} else {
this.quill.format('image', false);
}
},
'video': function (value) {
if (value) {
document.querySelector('#quillEditorQiniu .avatar-uploader input').click()
} else {
this.quill.format('video', false);
}
},
}
},
}
},
}
},
methods:{
// 准备富文本编辑器
onEditorReady(quill) {
console.log('editor ready!', quill);
for (let item of titleConfig) {
let tip = document.querySelector('.quill-editor ' + item.Choice)
if (!tip) continue
tip.setAttribute('title', item.title)
}
},
//上传图片之前async
beforeEditorUpload(res, file){
//显示上传动画
this.quillUpdateImg = true;
// const res1 = await uploadImage()
// console.log(res1,'=====');
// this.$emit('before',res, file)
console.log(res);
console.log(file);
},
// 上传图片成功
uploadEditorSuccess(res, file) {
console.log("上传成功")
// this.$emit('upload',res, file)
console.log(res, file);
//拼接出上传的图片在服务器的完整地址
let imgUrl=res.data.url;
let type=imgUrl.substring(imgUrl.lastIndexOf(".")+1);
console.log(type);
// 获取富文本组件实例
let quill = this.$refs.customQuillEditor.quill;
// 获取光标所在位置
let length = quill.getSelection().index;
// 插入图片||视频 res.info为服务器返回的图片地址
if(type=='mp4' || type=='MP4'){
window.jsValue=imgUrl;
quill.insertEmbed(length, 'video', imgUrl)
}else{
quill.insertEmbed(length, 'image', imgUrl)
}
// 调整光标到最后
quill.setSelection(length + 1)
//取消上传动画
this.quillUpdateImg = false;
},
// 上传(文件)图片失败
uploadEditorError(res, file) {
console.log(res);
console.log(file);
//页面提示
this.$message.error('上传图片失败')
//取消上传动画
this.quillUpdateImg = false;
},
//上传组件返回的结果
uploadResult:function (res){
this.uploadUrlPath=res;
},
openContent:function (){
console.log(this.content)
},
},
created () {
},
//只执行一次,加载执行
mounted () {
console.log("开始加载");
},
watch:{
content(newVal, oldVal) {
//this.$emit('input', newVal);
console.log(newVal)
console.log(oldVal)
}
},
}
</script>
<style>
.editor {
line-height: normal !important;
height: 400px;
margin-bottom: 50px;
}
.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: "等宽字体";
}
/*加上height和滚动属性就可以,滚动条样式是系统默认样式,可能不同*/
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
border-color: #ccc;
height: 125px;
overflow: auto;
}
</style>
结构如下
把该界面写入到路由router.js,测试效果
如果控制台出现如下错误,导致前端界面启动失败
Cannot read property 'imports' of undefined"
Failed to mount component: template or render function not defined.
Cannot read property 'registerof undefined’
解决方法:
在vue.config.js加上下面代码(重启即可)
const webpack = require("webpack")
module.exports = {
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
'window.Quill': 'quill/dist/quill.js',
'Quill': 'quill/dist/quill.js',
})
]
},
}
效果演示
|