引入vue-quill-editor
- 初始化vue-quill-editor
npm install vue-quill-editor --save
- 部分页面引入组件
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import {quillEditor} from 'vue-quill-editor'
全局引入组件
- main.js文件中
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)
根据需求自定义富文本
- 创建QuillEditorForm.vue和toolbarOptions.js,抽象成一个新的组件
- 需求:图片通过右键粘贴、点击上方图片上传按钮进行上传。由于组件默认将图片转为base64再放入内容中,如果图片比较大的话,富文本的内容就会很大,即使图片不大,只要图片较为多,篇幅较长,富文本的内容也变大影响数据传递。所以只能传递到服务器上,然后将url赋值给img的src。监听粘贴事件自定义上传(这个解决方案想了很久)
- show me the code
<template>
<!--富文本编辑器-->
<div class="app-container">
<div class="avatar">
<!-- 图片上传组件辅助-->
<el-upload
id="quill-upload"
action="上传的路径"
ref="annexUpload"
name="content"
:limit="1"
list-type="picture"
:on-exceed="handleExceed"
:on-error="handleError"
:on-success="uploadAttachment"
:file-list="uploadList"
:with-credentials="true"
:auto-upload="true">
</el-upload>
</div>
<el-row v-loading="quillUpdateImg">
<div class="edit_container">
<quill-editor
:disabled="disableFlag"
v-model="detailContent"
ref="myQuillEditor"
:options="editorOption"
@change="onEditorChange($event)">
</quill-editor>
</div>
</el-row>
</div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import {quillEditor} from 'vue-quill-editor'
import toolbarOptions from './toolbarOptions'
export default {
name: 'QuillEditorForm',
components: {
quillEditor
},
props: ['entity', 'disableFlag', 'problemDescribe'],
data() {
return {
quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
uploadList: [],
detailContent: '', // 富文本内容
// 富文本编辑器配置
editorOption: {
placeholder: '',
theme: 'snow', // or 'bubble'
modules: {
toolbar: {
container: toolbarOptions, // 工具栏
handlers: {
'image': function(value) {
if (value) {
// 绑定上传图片按钮
document.querySelector('#quill-upload input').click()
} else {
this.quill.format('image', false)
}
}
}
}
}
}
}
},
mounted() {
// 自定义粘贴图片功能
let quill = this.$refs.myQuillEditor.quill
if (!this.disableFlag) {
toolbarOptions[0] = ['image']
}
this.$forceUpdate()
quill.root.addEventListener('paste', evt => {
if (evt.clipboardData && evt.clipboardData.files && evt.clipboardData.files.length) {
evt.preventDefault();
[].forEach.call(evt.clipboardData.files, file => {
if (!file.type.match(/^image\/(gif|jpe?g|a?png|bmp)/i)) {
return
}
this.uploadToServer(file, (res) => {
// 获取光标内容
var range = quill.getSelection()
if (range) {
this.uploadAttachment(res, file, null)
// 将光标移动到图片后面
this.$refs.myQuillEditor.quill.setSelection(range.index + 1)
}
})
})
}
}, false)
},
methods: {
uploadToServer(file, callback) {
var xhr = new XMLHttpRequest()
var formData = new FormData()
formData.append('content', file)
xhr.open('post', '服务器路径')
xhr.withCredentials = true
xhr.responseType = 'json'
xhr.send(formData)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(xhr.response)
}
}
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制一次性上传最多 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
handleError(err, file, fileList) {
this.$message.warning(`文件上传失败,请尝试重新上传,文件失败原因为: ${err}`)
},
uploadAttachment(response, file, fileList) {
// 保存文件信息
if (response.status.code === 0) {
// 获取富文本组件实例
let quill = this.$refs.myQuillEditor.quill
// 获取光标所在位置
let length = quill.getSelection().index
// 插入图片 res.info为服务器返回的图片地址
quill.insertEmbed(length, 'image', response.url)
quill.setSelection(length + 1)
let fileType = null
if (file.raw && file.raw.type) {
fileType = file.raw.type
} else {
fileType = file.type
}
let params = {}
params = {
// 保存文件上传的参数
}
workService.create(params).then((res) => {
this.$message.success('上传成功')
}).catch((err) => {
this.$message.error(err)
})
} else {
this.$message.error(response.status.message)
}
// 清空文件列表
this.uploadList = []
},
onEditorChange(e) {
this.$emit('change', e.html)
}
}
}
</script>
<style scoped>
.quill-editor {
min-width: 600px;
max-width: 700px;
max-height: 250px;
overflow: auto;
}
.avatar {
display: none;
}
/deep/ .ql-disabled {
background-color: #F5F7FA;
border-color: #E4E7ED;
color: #C0C4CC;
cursor: not-allowed;
}
/*.edit_container {*/
/* position: relative;*/
/*}*/
</style>
toolbarOptions.js
const toolbarOptions = [
// ['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{'header': 1}, {'header': 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, false]}],
// [{'color': []}, {'background': []}],
// [{'font': []}],
// [{'align': []}],
// ['link', 'image', 'video'],
['clean']
]
export default toolbarOptions
- 头部工具栏请参考官网
- 只粘贴文本
// 只能粘贴原文本
handleCustomMatcher(node, Delta) {
let ops = []
Delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'string') {
// 如果粘贴了图片,这里会是一个对象,所以可以这样处理
ops.push({
insert: op.insert
})
}
})
Delta.ops = ops
return Delta
},
拓展
- 调节图片大小
- 打包报错的话请参考
npm install quill-image-resize-module quill-image-drop-module --save
// 引入
import { ImageDrop } from 'quill-image-drop-module'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageDrop', ImageDrop)
Quill.register('modules/imageResize', ImageResize)
// build/webpack.base.conf.js文件
const webpack = require('webpack')
plugins: [
new webpack.ProvidePlugin({
'window.Quill': 'quill/dist/quill.js',
'Quill': 'quill/dist/quill.js'
})
]
// 放在editorOption里面
history: {
delay: 1000,
maxStack: 50,
userOnly: false
},
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
}
兼容性问题
兼容IE10以上
20211012更新富文本框
新增功能:点击预览图片
npm install v-viewer --save
<template>
<!--富文本编辑器-->
<div class="app-container">
<div class="avatar">
<!-- 图片上传组件辅助-->
<el-upload
id="quill-upload"
class="avatar-uploader"
action="/v1/file/upload"
ref="annexUpload"
name="content"
:limit="1"
list-type="picture"
:on-exceed="handleExceed"
:on-error="handleError"
:on-success="uploadAttachment"
:file-list="uploadList"
:with-credentials="true"
:auto-upload="true">
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</div>
<el-row v-loading="quillUpdateImg">
<div v-if="!disableFlag">
<quill-editor
v-if="loaded"
:disabled="disableFlag"
v-model="detailContent"
ref="myQuillEditor"
:options="editorOption"
@focus="onEditorFocus($event)"
@change="onEditorChange($event)">
</quill-editor>
</div>
<div v-else>
<div style="display: flex;">
<span @click="showViewer = !showViewer" style="float: right;cursor: pointer;margin-right: 20px;">
<i :class="!showViewer ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" class="arrow"></i>
</span>
</div>
<viewer v-if="showViewer" :images="[]" class="viewer" ref="viewer">
<div v-html="detailContent"></div>
</viewer>
</div>
</el-row>
</div>
</template>
<script>
import 'viewerjs/dist/viewer.css'
import Viewer from 'v-viewer/src/component'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import Quill from 'quill'
import {quillEditor} from 'vue-quill-editor'
import { ImageDrop } from './ImageDrop'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageDrop', ImageDrop)
Quill.register('modules/imageResize', ImageResize)
export default {
name: 'QuillEditorForm',
components: {
quillEditor, Viewer
},
props: ['entity', 'disableFlag', 'value'],
data() {
return {
quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
serverUrl: '', // 这里写你要上传的图片服务器地址
header: {token: sessionStorage.token}, // 有的图片服务器要求请求头需要有token之类的参数,写在这里
uploadList: [],
activeNames: ['1'],
detailContent: '', // 富文本内容
initFlag: true,
// 富文本编辑器配置
editorOption: {
placeholder: '',
theme: 'snow', // or 'bubble'
modules: {
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
},
toolbar: {
container: [
[], // 预留图片上传
['blockquote', 'code-block'], // 引用,代码块
[{'list': 'ordered'}, { 'list': 'bullet' }], // 列表
[{'header': [1, 2, 3, false]}], // 几级标题(1~6)
// [{'header': 1}, {'header': 2}], // 标题,键值对的形式;1、2表示字体大小
[{'size': ['small', false, 'large']}], // 字体大小('small', false, 'large', 'huge')
[{ 'color': [] }, { 'background': [] }],
['bold', 'italic', 'underline'], // 加粗,斜体,下划线,删除线('strike')
// [{'script': 'sub'}, {'script': 'super'}], // 上下标
[{'indent': '-1'}, {'indent': '+1'}], // 缩进
// [{'direction': 'rtl'}], // 文本方向
// [{ 'font': [] }], // 字体
// [{ 'align': [] }], // 对齐方式
['clean'] // 清除字体样式
], // 工具栏
handlers: {
image: (value) => {
if (value) {
this.$refs.annexUpload.$el.querySelector('#quill-upload input').click()
} else {
this.$refs.myQuillEditor.quill.format('image', false)
}
}
}
}
}
},
quillInstance: null,
loaded: true,
showViewer: true
}
},
watch: {
value: {
handler(val) {
this.detailContent = val
this.$forceUpdate()
},
deep: true,
immediate: true
},
detailContent: {
handler(val) {
if (val && this.initFlag && this.$refs.myQuillEditor && this.$refs.myQuillEditor.quill) {
// 初始化页面,取消focus状态
this.$refs.myQuillEditor.quill.enable(false)
this.detailContent = val
this.$emit('input', val)
setTimeout(() => {
this.$refs.myQuillEditor.quill.enable(true)
})
this.initFlag = false
} else {
this.detailContent = val
this.$emit('input', val)
}
},
deep: true,
immediate: true
},
disableFlag: {
handler(val) {
if (!val) {
this.editorOption.modules.toolbar.container[0] = ['image']
}
this.loaded = false
this.$nextTick(() => {
this.loaded = true
this.$nextTick(() => {
// console.log('update', this, val, this.$refs.myQuillEditor)
if (!this.$refs.myQuillEditor) {
return
}
// 自定义粘贴图片功能
this.quillInstance = this.$refs.myQuillEditor.quill
this.quillInstance.root.removeEventListener('paste', this.handlePaste)
this.quillInstance.root.addEventListener('paste', this.handlePaste, false)
})
})
},
immediate: true
}
},
methods: {
onEditorFocus(e) { // 用户自动获取焦点,不设置focus状态
this.initFlag = false
},
handlePaste(evt) {
if (evt.clipboardData && evt.clipboardData.files && evt.clipboardData.files.length) {
evt.preventDefault();
[].forEach.call(evt.clipboardData.files, file => {
if (!file.type.match(/^image\/(gif|jpe?g|a?png|bmp)/i)) {
return
}
this.uploadToServer(file, (res) => {
// 获取光标内容
var range = this.quillInstance.getSelection()
if (range) {
this.uploadAttachment(res, file, null)
// 在当前光标位置插入图片
// this.$refs.myQuillEditor.quill.insertEmbed(range.index, 'image', res.url)
// 将光标移动到图片后面
this.$refs.myQuillEditor.quill.setSelection(range.index + 1)
}
})
})
}
},
uploadToServer(file, callback) {
var xhr = new XMLHttpRequest()
var formData = new FormData()
formData.append('content', file)
xhr.open('post', '上传路径')
xhr.withCredentials = true
xhr.responseType = 'json'
xhr.send(formData)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(xhr.response)
}
}
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制一次性上传最多 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
handleError(err, file, fileList) {
this.$message.warning(`文件上传失败,请尝试重新上传,文件失败原因为: ${err}`)
},
uploadAttachment(response, file, fileList) {
// 保存文件信息
if (response.status.code === 0) {
// 获取富文本组件实例
let quill = this.$refs.myQuillEditor.quill
// 获取光标所在位置
let length = quill.getSelection().index
// 插入图片 res.info为服务器返回的图片地址
quill.insertEmbed(length, 'image', response.url)
quill.setSelection(length + 1)
let fileType = null
if (file.raw && file.raw.type) {
fileType = file.raw.type
} else {
fileType = file.type
}
let params = {}
params = {
// 保存文件上传的参数
}
service.createWorkOrderFile(params).then((res) => {
this.$message.success('上传成功')
}).catch((err) => {
this.$message.error(err)
})
} else {
this.$message.error(response.status.message)
}
// 清空文件列表
this.uploadList = []
},
onEditorChange(e) {
this.$emit('change', e.html)
}
}
}
</script>
<style scoped>
@import "../../assets/quill.css";
.quill-editor {
min-width: 657px;
overflow: auto;
}
.avatar {
display: none;
}
/deep/ .ql-disabled {
background-color: #F5F7FA;
border-color: #E4E7ED;
color: #7f8185;
cursor: not-allowed;
}
.viewer {
border: 1px solid #ebeef5;
padding: 12px 15px;
min-width: 657px;
max-width: 700px;
max-height: 600px;
overflow: auto;
}
.arrow {
margin: 0 8px 0 auto;
transition: transform .3s;
font-weight: 300;
}
</style>