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
    },        

  

posted @ 2024-08-27 15:06  沁猿春  阅读(224)  评论(0编辑  收藏  举报