基于quill-editor 封装一个富文本组件,并实现自定义上传图片以及视频
- 下载quill-editor
npm install vue-quill-editor --save
- 对插件进行自定义改造(自定义字体大小选择,自定义标题,以及自定义工具栏功能)
<template>
<div class="edtior-box">
<quill-editor
v-model="contentHtml"
ref="myQuillEditor"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@change="onEditorChange($event)"
@ready="onEditorReady($event)"
>
</quill-editor>
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:multiple = false
:beforeUpload="beforeUpload"
:customRequest="uploadImg"
style="display: none;"
></a-upload>
<a-upload
name="avatar"
list-type="picture-card"
class="video-upload"
:show-upload-list="false"
:multiple = false
:customRequest="uploadVideo"
style="display: none;"
accept="video/*"
></a-upload>
<!-- <input type="file" id="video-upload" style="display: none;" accept="video/*"> -->
</div>
</template>
<script>
import * as commonApi from "@/api/common";
import { SYSTEM_ID } from '@/utils/enum';
import moment from 'moment';
import { quillEditor } from "vue-quill-editor";
import * as Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { ImageDrop } from "quill-image-drop-module"; //实现图片拖拽以及大小改变
import ImageResize from "quill-image-resize-module"; //实现图片拖拽以及大小改变
Quill.register("modules/imageDrop", ImageDrop);
Quill.register("modules/imageResize", ImageResize);
// 这里引入修改过的video模块并注册
import Video from './quill-video'
Quill.register(Video, true)
// 设置字体大小
const fontSizeStyle = Quill.import("attributors/style/size"); // 引入这个后会把样式写在style上
fontSizeStyle.whitelist = [
"12px",
"14px",
"16px",
"18px",
"20px",
"24px",
"28px",
"32px",
"36px",
];
Quill.register(fontSizeStyle, true);
let Align = Quill.import('attributors/style/align');
Align.whitelist = ['right', 'center', 'justify'];
Quill.register(Align, true)
// var _EditorOption_ =
// 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-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: "两端对齐",
},
];
export default {
name: "CommonEditor",
components: {
quillEditor,
},
props: {
content: {
type: String,
},
disabled:{
type: Boolean,
default: false
},
customImg:{ //是否自定义上传图片到服务器 默认false 使用base64
type: Boolean,
default: false
},
imgVedio:{ //自定义是否显示图片 视频
type: Array,
default: ()=>{
return ["link","image","video"]
}
},
},
data() {
return {
contentHtml: this.content,
editorOption: {
modules: {
toolbar:{
container: [
["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: ["12", "14", "16", "18", "20", "22", "24", "28", "32", "36"] }], // 字体大小
[{ size: fontSizeStyle.whitelist }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6,false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
// [{ font: ['songti'] }], // 字体种类
[{ align: [] }], // 对齐方式
["clean"], // 清除文本格式
this.imgVedio // 链接、图片、视频
],
},
// 新增下面
imageDrop: false, // 拖动加载图片组件。
imageResize: {
//调整大小组件。
displayStyles: {
backgroundColor: "black",
border: "none",
color: "white",
},
modules: ["Resize", "DisplaySize", "Toolbar"],
},
},
placeholder: "请输入正文",
},
quill:null,
};
},
watch: {
content(val) {
this.contentHtml = val;
},
},
mounted() {
this.initTitle();
this.$refs.myQuillEditor.quill.getModule('toolbar').addHandler('image', this.handleImage);
this.$refs.myQuillEditor.quill.getModule('toolbar').addHandler('video', this.handleVideo);
this.quill = this.$refs.myQuillEditor.quill;
this.quill.root.addEventListener('paste', this.handlePaste, false);
},
beforeDestroy(){
this.quill.root.removeEventListener('paste', this.handlePaste, false);
},
methods: {
// 上传前校验
async beforeUpload(file){
const isXls = /.(xls|jpg|png|jpeg)$/.test(file.name.toLowerCase());
const isLt20M = file.size / 1024 / 1024 < 20;
return new Promise((resolve) => {
if (!isXls) {
this.$message.error('请上传正确格式的文件!');
return false
}
if (!isLt20M) {
this.$message.error('请上传小于20M的文件!');
return false
}
resolve(true);
return true;
});
},
//图片上传之后
async uploadImg(options){
if(this.customImg){
let result = await this.uploadFile(options.file)
let quill = this.$refs.myQuillEditor.quill
let length = quill.getSelection()?quill.getSelection().index:0;
let protocol = window.location.protocol //协议
let domain = window.location.hostname // 域名
let port = window.location.port ? `:${window.location.port}` : '' // 端口号
const URL = protocol+'//'+domain+port
quill.insertEmbed(length, 'image', URL+'/aldApi'+result.filePath)
quill.setSelection(length + 1)
}else{
let quill = this.$refs.myQuillEditor.quill
let length = quill.getSelection()?quill.getSelection().index:0;
let url = await this.readFileAsBase64(options.file)
quill.insertEmbed(length, 'image', url)
quill.setSelection(length + 1)
}
},
readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(event) {
resolve(event.target.result);
};
reader.onerror = function(error) {
reject(error);
};
reader.readAsDataURL(file);
});
},
//视频上传之后
async uploadVideo(options){
let result = await this.uploadFile(options.file)
let quill = this.$refs.myQuillEditor.quill
let length = quill.getSelection()?quill.getSelection().index:0;
let protocol = window.location.protocol //协议
let domain = window.location.hostname // 域名
let port = window.location.port ? `:${window.location.port}` : '' // 端口号
const URL = protocol+'//'+domain+port
quill.insertEmbed(length, 'video',URL+'/aldApi'+result.filePath)
quill.setSelection(length + 1)
},
//上传接口
async uploadFile(file){
let params = {
originalFileName: file.name,
resourceType:0,
systemId:SYSTEM_ID['operation'],
systemCode:'Base_Operate_'+process.env.VUE_APP_SYSTEMCODE,
relationCode:"HotelQualificationImage",
storageFormat:moment().format('YYYYMMDD'),
expirationTime:-1,
confirm:1
}
this.loading = true;
let fileContent = null
fileContent = await this.readFile(file);
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
let res = await commonApi.uploadStream(queryString,fileContent);
if (res.code != '0') {
this.$message.error(res.data.errorMsg);
this.loading = false;
return;
}
this.loading = false;
return res.data
},
readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsArrayBuffer(file); // 你可以根据需要选择其他方法
});
},
// 失去焦点事件
onEditorBlur(v) {
v.enable(!this.disabled)
// console.log("editor blur!", quill);
},
// 获得焦点事件
onEditorFocus(v) {
v.enable(!this.disabled)
// console.log("editor focus!", quill);
},
// 准备富文本编辑器
onEditorReady() {
// console.log("editor ready!", quill);
},
// 内容改变事件
onEditorChange({ html }) {
this.contentHtml = html;
this.$emit("onEditorChange", html);
},
//设置标题
initTitle() {
this.$nextTick(() => {
document.getElementsByClassName("ql-editor")[0].dataset.placeholder = "";
for (let item of titleConfig) {
let tip = document.querySelector(".quill-editor " + item.Choice);
if (!tip) continue;
tip.setAttribute("title", item.title);
}
})
},
//自定义上传图片
handleImage() {
if(!this.disabled){
document.querySelector(".avatar-uploader input").click();
}else{
return
}
},
//自定义上传视频
handleVideo() {
if(!this.disabled){
document.querySelector('.video-upload input').click(); // 触发视频上传
}else{
return
}
},
//禁止复制图片 如果有这个需求可以解开,详细可根据这个随笔的粘贴复制 思路
handlePaste(e) {
const clipboardData = e.clipboardData;
const types = clipboardData.types;
if (types.includes('Files')) {
// 禁止粘贴图片
this.$message.error('禁止粘贴图片视频')
e.preventDefault();
}
},
}
};
</script>
<style>
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px" !important;
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
content: "10px" !important;
font-size: 10px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
content: "12px" !important;
font-size: 12px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
content: "16px" !important;
font-size: 16px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
content: "18px" !important;
font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
content: "20px" !important;
font-size: 20px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
content: "24px" !important;
font-size: 24px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before {
content: "28px" !important;
font-size: 28px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="32px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before {
content: "32px" !important;
font-size: 32px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
content: "36px" !important;
font-size: 36px;
}
</style>
<style lang="less">
.ql-snow {
.ql-header {
&.ql-picker {
.ql-picker-label,
.ql-picker-item {
&::before {
content: '正文';
}
&[data-value='1']::before {
content: '标题1';
}
&[data-value='2']::before {
content: '标题2';
}
&[data-value='3']::before {
content: '标题3';
}
&[data-value='4']::before {
content: '标题4';
}
&[data-value='5']::before {
content: '标题5';
}
&[data-value='6']::before {
content: '标题6';
}
}
}
}
}
</style>
视频上传成功之后改用 video 标签插入富文本
import { Quill } from "vue-quill-editor";
// 源码中是import直接倒入,这里要用Quill.import引入
const BlockEmbed = Quill.import('blots/block/embed')
const Link = Quill.import('formats/link')
const ATTRIBUTES = ['height', 'width']
class Video extends BlockEmbed {
static create(value) {
const node = super.create(value)
// 添加video标签所需的属性
node.setAttribute('controls', 'controls')
node.setAttribute('type', 'video/mp4')
node.setAttribute('src', this.sanitize(value))
return node
}
static formats(domNode) {
return ATTRIBUTES.reduce((formats, attribute) => {
if (domNode.hasAttribute(attribute)) {
formats[attribute] = domNode.getAttribute(attribute)
}
return formats
}, {})
}
static sanitize(url) {
return Link.sanitize(url) // eslint-disable-line import/no-named-as-default-member
}
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}" rel="external nofollow" rel="external nofollow" >${video}</a>`
}
}
Video.blotName = 'video' // 这里不用改,楼主不用iframe,直接替换掉原来,如果需要也可以保留原来的,这里用个新的blot
Video.className = 'ql-video'
Video.tagName = 'video' **// 用video标签替换iframe**
export default Video
组件的使用方法:
import editor from '@/views/components/editor'
<editor :content="formData.content" @onEditorChange="onEditorChange" :customImg="true"></editor>
// 富文本编辑器内容改变
onEditorChange(value){
this.formData.content = value
},