vue-quill-editor --save 实现自定义图片/视频上传

踩在这个作者的肩膀上实现了自定义图片/视频上传 https://sanshui.blog.csdn.net/article/details/81464100,原文章有些地方有点没说清楚的地方,稍作了些修改。也解决了文中未提及的bug。
复制以下代码即可使用

一,安装 vue-quill-editor

npm install vue-quill-editor --save 

二,
创建QuillEditor组件

<template>
  <div>
    <quill-editor
      ref="myTextEditor"
      v-model="contentValue"
      :options="editorOption"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @ready="onEditorReady($event)"
      @change="onEditorChange($event)"
      class="cfpa-quill-editor"
      :style="{ height: quillEditorHeight + 'px' }"
    >
      <div id="toolbar" slot="toolbar">
        <!-- Add a bold button -->
        <button class="ql-bold" title="加粗">Bold</button>
        <button class="ql-italic" title="斜体">Italic</button>
        <button class="ql-underline" title="下划线">underline</button>
        <button class="ql-strike" title="删除线">strike</button>
        <button class="ql-blockquote" title="引用"></button>
        <button class="ql-code-block" title="代码"></button>
        <button class="ql-header" value="1" title="标题1"></button>
        <button class="ql-header" value="2" title="标题2"></button>
        <!--Add list -->
        <button class="ql-list" value="ordered" title="有序列表"></button>
        <button class="ql-list" value="bullet" title="无序列表"></button>
        <!-- Add font size dropdown -->
        <select class="ql-header" title="段落格式">
          <option selected>段落</option>
          <option value="1">标题1</option>
          <option value="2">标题2</option>
          <option value="3">标题3</option>
          <option value="4">标题4</option>
          <option value="5">标题5</option>
          <option value="6">标题6</option>
        </select>
        <select class="ql-size" title="字体大小">
          <option value="10px">10px</option>
          <option value="12px">12px</option>
          <option value="14px">14px</option>
          <option value="16px" selected>16px</option>
          <option value="18px">18px</option>
          <option value="20px">20px</option>
        </select>
        <select class="ql-font" title="字体">
          <option value="SimSun" selected="selected"></option>
          <option value="SimHei"></option>
          <option value="Microsoft-YaHei"></option>
          <option value="KaiTi"></option>
          <option value="FangSong"></option>
          <option value="Arial"></option>
          <!-- <option value="Times-New-Roman"></option>
          <option value="sans-serif"></option> -->
        </select>

        <!-- Add subscript and superscript buttons -->
        <select class="ql-color" value="color" title="字体颜色"></select>
        <select
          class="ql-background"
          value="background"
          title="背景颜色"
        ></select>
        <select class="ql-align" value="align" title="对齐"></select>
        <button class="ql-clean" title="还原"></button>
        <!-- <button class="ql-link" title="超链接"></button> -->
        <!-- You can also add your own -->
        <button
          id="custom-button"
          @click.prevent="fnOpenUploadImage"
          title="图片"
        >
          <i class="iconfont el-icon-picture"></i>
        </button>
        <button
          id="custom-button"
          @click.prevent="fnOpenUploadVideo"
          title="视频"
        >
          <i class="iconfont el-icon-video-play"></i>
        </button>
      </div>
    </quill-editor>
    <div :style="wordCount" v-if="wordCount" class="cfpa-quill-wordCount">
      <div class="cfpa-quill-wordCount-text">
        当前已经输入<span style="color: red">{{ contentLength }}</span
        >个字符
      </div>
    </div>
    <el-dialog
      :title="title"
      width="30%"
      :visible.sync="dialogFnOpenUpload"
      :close-on-click-modal="false"
    >
      <file-upload
        :data_extra="data_extra"
        @fnUploadSucess="fnUploadSucess"
        @fnCloseDialog="dialogFnOpenUpload = false"
        ref="fileUpload"
        :types="
          uploadType === 'video'
            ? ['video/mp4']
            : ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp']
        "
      ></file-upload>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogFnOpenUpload = false">取 消</el-button>
        <el-button type="primary" @click="fnOpenUploadSubmit">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import fileUpload from './upload'
import { Quill, quillEditor } from 'vue-quill-editor'

// 这里引入修改过的video模块并注册
import Video from './upload/video'
Quill.register(Video, true)

// 图片可收缩
import { ImageDrop } from 'quill-image-drop-module'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageDrop', ImageDrop)
Quill.register('modules/imageResize', ImageResize)

// 自定义字体大小
let Size = Quill.import('attributors/style/size')
Size.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px']
Quill.register(Size, true)

// 自定义字体类型
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif']
var Font = Quill.import('formats/font')
Font.whitelist = fonts // 将字体加入到白名单
Quill.register(Font, true)

export default {
  name: 'editor',
  components: {
    quillEditor,
    fileUpload
  },
  props: {
    value: {
      type: String,
      default: ''
    },
    editorHeight: {
      type: Number,
      default: 355
    },
    editorWordCount: {
      type: Number,
      default: 0
    }
  },
  data () {
    return {
      contentValue: '',
      preContent: '',
      dialogFnOpenUpload: false,
      uploadType: 'image',
      editorOption: {
        modules: {
          toolbar: '#toolbar',
          history: {
            delay: 1000,
            maxStack: 50,
            userOnly: false
          },
          imageDrop: true,
          imageResize: {
            displayStyles: {
              backgroundColor: 'black',
              border: 'none',
              color: 'white'
            },
            modules: ['Resize', 'DisplaySize', 'Toolbar']
          }
        },
        placeholder: '请编写内容...'
      },
      data_extra: {
        parentId: 0,
        fileName: ''
      },
      contentLength: 0,
      wordCount: '',
      title: '添加图片',
      quillEditorHeight: 300
    }
  },
  computed: {
    editor () {
      return this.$refs.myTextEditor.quill
    }
  },
  methods: {
    /**
     * @description [onEditorBlur 失去焦点]
     * @author   zoumiao
     * @param {Object} editor 返回的quill对象
     * @return   {null}   [没有返回]
     */
    onEditorBlur (editor) {
      this.$emit('editorBlur')
    },
    /**
     * @description [onEditorFocus 获取焦点]
     * @author   zoumiao
     * @param {Object} editor 返回的quill对象
     * @return   {null}   [没有返回]
     */
    onEditorFocus (editor) {
      this.$emit('editorFocus')
    },
    /**
     * @description [onEditorReady 可以输入]
     * @author   zoumiao
     * @param {Object} editor 返回的quill对象
     * @return   {null}   [没有返回]
     */
    onEditorReady (editor) {
    },
    /**
     * @description [onEditorChange 输入文本改变事件]
     * @author   zoumiao
     * @param {Object} editor 返回的编辑对象{html, text, quill}
     * @return   {null}   [没有返回]
     */
    onEditorChange (editor) {
      console.log(editor);
      let html = editor.html
      this.preContent = html
      this.$emit('input', html)
      this.contentLength = editor.text.trim().length
    },
    /**
     * @description [fnOpenUploadImage 上传图片]
     * @author   zoumiao
     * @return   {null}   [没有返回]
     */
    fnOpenUploadImage () {
      this.uploadType = 'image'
      this.title = '添加图片'
      this.dialogFnOpenUpload = true
    },
    /**
     * @description [fnOpenUploadVideo 上传视频]
     * @author   zoumiao
     * @return   {null}   [没有返回]
     */
    fnOpenUploadVideo () {
      this.uploadType = 'video'
      this.title = '添加视频'
      this.dialogFnOpenUpload = true
    },
    /**
     * [fnOpenUploadSubmit 提交上传文件]
     * @author   zoumiao
     * @return   {null}   [没有返回]
     */
    async fnOpenUploadSubmit () {
      await this.$refs.fileUpload.$refs.upload.submit()
    },
    /**
     * [fnUploadSucess 上传文件成功]
     * @author   zoumiao
     * @param {Array} uploadFileUrlList [上传文件返回的url]
     * @return   {null}   [没有返回]
     */
    fnUploadSucess (uploadFileUrlList) {
      this.editor.focus()
      this.editor.insertEmbed(this.editor.getSelection().index, this.uploadType, uploadFileUrlList)
      this.dialogFnOpenUpload = false
    }
  },
  created () {
    this.quillEditorHeight = document.body.clientHeight - this.editorHeight
    this.contentValue = this.value
    this.contentLength = this.editorWordCount || 0
  },
  mounted () {
    let toolbar = document.querySelector('div.ql-toolbar.ql-snow')
    if (toolbar) {
      let toolbarHeight = toolbar.offsetHeight
      this.wordCount = {
        'top': `${toolbarHeight}px`
      }
      return
    }
    this.wordCount = {
      'top': '42px'
    }
  },
  watch: {
    // Watch content change
    value (newVal, oldVal) {
      if (newVal && newVal !== this.preContent) {
        this.preContent = newVal
        this.contentValue = newVal
      } else if (!newVal) {
        this.contentValue = ''
      }
    }
  }
}
</script>
<style lang="scss">
.cfpa-quill-editor {
  line-height: 24px;
  .ql-snow {
    background-color: #ffffff;
  }
}
.cfpa-quill-wordCount {
  background-color: #ffffff;
  position: relative;
  border-left: 1px solid #ccc;
  border-right: 1px solid #ccc;
  border-bottom: 1px solid #ccc;
  border-bottom-left-radius: 3px;
  border-bottom-right-radius: 3px;
  line-height: 20px;
  font-size: 12px;
  .cfpa-quill-wordCount-text {
    text-align: right;
    margin-right: 10px;
    color: #aaa;
  }
}
</style>

创建上传的图片和视频的组件,基于element ui和oss,可根据自己的需求修改
./upload/index.vue

<template>
  <div id="fileUpload">
    <el-upload
      :disabled="loading && showLoading"
      :action="action"
      :data="dataObj"
      :before-upload="beforeUpload"
      :on-success="fnUploadSucess"
      :on-error="handlerError"
      :on-preview="handlePreview"
      :show-file-list="false"
      :on-remove="handleRemove"
      :loading="loading"
      :file-list="fileList"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
    </el-upload>
  </div>
</template>

<script>
export default {
  name: 'FileUpload',
  data () {
    return {
      action: 'api/third/oss/upload',
      dataObj: {},
      fileList: [],
      loading: false,
    }
  },
  props: {
    types: {
      type: Array
    },
    showLoading: {
      type: Boolean,
      default: true
    },
    limitSize: {
      type: Boolean,
      default: false
    }
  },
  created () {
  },
  methods: {
    handleRemove (file, fileList) {
      this.$emit('removeFile', [file, fileList])
    },
    handlePreview (file, fileList) {
      this.$emit('preview', [file, fileList])
    },
    beforeUpload (file) {
      let canUpload = false
      // 若文件限制格式不为空但是在可上传类型内 或 未指定上传文件 则让他上传
      if (this.types && this.types.length !== 0) {
        for (let i = 0; i < this.types.length; i++) {
          if (file.type === this.types[i]) {
            canUpload = true
          }
        }
      } else {
        canUpload = true
      }
      if (!canUpload) {
        this.$message.error('上传仅支持:' + this.types + '文件')
        return false
      }
      if (this.limitSize) {
        //限制大小
        const isLt2M = file.size / 1024 / 1024 < 2;
        if (!isLt2M) {
          this.$message.error('上传图片大小不能超过 2MB!');
          return false
        }
      }
      this.loading = true
      //获取时长
      this.getVideoDuration(file)

    },
    getVideoDuration (file) {
      var url = URL.createObjectURL(file);
      var audioElement = new Audio(url);
      var duration;
      audioElement.addEventListener("loadedmetadata", () => {
        duration = audioElement.duration; //时长为秒,小数,182.36
        this.$emit('getDuration', duration)
      });
    },

    fnUploadSucess (res, file) {
      this.$emit('fnUploadSucess', file.response.data)
      this.loading = false
      this.fileList = []
    },
    handlerError () {
      this.loading = false
      this.$message.error('上传失败')
      this.$emit('error')
    }
  }
}
</script>

<style scoped lang="scss">
#fileUpload {
  .fileInput {
    display: none;
  }
}
</style>

修改Quill内置的video blot,用video标签替换iframe
./upload/video.js

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}">${video}</a>`;
  }
}
Video.blotName = "video"; // 这里不用改,楼主不用iframe,直接替换掉原来,如果需要也可以保留原来的,这里用个新的blot
Video.className = "ql-video";
Video.tagName = "video"; // 用video标签替换iframe

export default Video;

如果报错说没有下面这些依赖,请依次安装

npm install quill-image-extend-module --save-dev
npm install quill-image-drop-module --save-dev 
npm install quill-image-resize-module --save-dev

如果在引进quill-image-resize-module 报错TypeError: Cannot read property 'imports' of undefined,
在vue.config.js中:(没有就在在根目录下新建vue.config.js 文件 (不是在src 下))(配置后需要重新运行一下)

var webpack = require('webpack');
module.exports = {
  // 在vue.config.js中configureWebpack中配置
// 要引入webpack

configureWebpack: {
    plugins: [
      new webpack.ProvidePlugin({
        'window.Quill': 'quill/dist/quill.js',
        'Quill': 'quill/dist/quill.js'
      }),
    ]
  }
}

最后在页面中使用:


<template>
<div>
 <quill-editor @input="changeContent" ref="quill"></quill-editor>
</div>
</template>
         

import QuillEditor from '@/components/QuillEditor'
//多余代码就不写了
  components: {
  QuillEditor
  },
data(){
return {
content :""
}
}
methods:{
    // 获取html
    changeContent (val) {
      this.content = val
      console.log(val);
    },
}

清空编辑区内容使用:

this.$refs.quill.$refs.myTextEditor.quill.setText('\n')
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352