quill-editor 富文本 组件封装并实现自定义上传图片
基于quill-editor 封装一个富文本组件,并实现自定义上传图片以及视频
1. 下载quill-editor
npm install vue-quill-editor --save
2. 对插件进行自定义改造(自定义字体大小选择,自定义标题,以及自定义工具栏功能)
<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 },