近期项目中需要使用富文本编辑器,开始想到的富文本编辑器是百度的UEditor,UEditor功能齐全、插件多,但是图片只能上传到本地服务器,如果需要上传到其他服务器需要改动源码,而且是PHP、JSP、ASP、.Net版本,同时UEditor体积过大压缩包有3.3M(jsp版本),加载速度慢。实际项目中并不需要那么多的功能,只需要基础的操作:字体、字体大小、标题、段落、图片上传、视频上传、居中对齐等;所以我们只需要一个轻量级的富文本编辑器就行,推荐使用Quill和TinyMCE,QuillTinyMCE都是轻量级,插件多,功能强,编辑能力优秀,界面好看。选择Quill的原因是它所有能看到的,不能看到的功能统统都是一个一个独立的模块,全部都是可以替换的,可以自定义编辑。
  站在巨人vue-quill-editor的肩膀上进行Quill editor的自定义图片/视频上传(Element UI + OSS)、字体、字体大小、标题、段落等封装。先看效果再来编码:
这里写图片描述

<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 icon-tupian"></i></button>
        <button id="custom-button" @click.prevent="fnOpenUploadVideo" title="视频"><i class="iconfont icon-video2"></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 :accept="accept" :data_extra="data_extra" @fnUploadSucess="fnUploadSucess" @fnCloseDialog="dialogFnOpenUpload = false" ref="fileUpload"></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 '@/components/briefFileUpload'
import config from '@/config'
import { Quill, quillEditor } from 'vue-quill-editor'

// 图片可收缩
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,
      accept: '',
      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) {
      let html = editor.html
      this.preContent = html
      this.$emit('input', html)
      this.contentLength = this._.trim(editor.text).length
    },
    /**
     * @description [fnOpenUploadImage 上传图片]
     * @author   zoumiao
     * @return   {null}   [没有返回]
     */
    fnOpenUploadImage () {
      this.uploadType = 'image'
      this.accept = config.accept.image
      this.title = '添加图片'
      this.dialogFnOpenUpload = true
    },
    /**
     * @description [fnOpenUploadVideo 上传视频]
     * @author   zoumiao
     * @return   {null}   [没有返回]
     */
    fnOpenUploadVideo () {
      this.uploadType = 'video'
      this.accept = config.accept.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()
      for (let url of uploadFileUrlList) {
        this.editor.insertEmbed(this.editor.getSelection().index, this.uploadType, url)
      }
    }
  },
  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>

1、自定义字体、字体大小、段落

<div id="toolbar" slot="toolbar">
  <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>
</div>

在Quill初始化之前进行注册:

// 自定义字体大小
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)

  自定义字体、字体大小、段落需要在使用Quill编辑器之前引入quill.css,可以在App.vue或者main.js中引入。

.ql-snow .ql-picker.ql-size,
.ql-snow .ql-picker.ql-header {
  width: 75px !important;
}

.ql-snow .ql-picker.ql-font {
  width: 80px !important;
}

.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';
  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';
  font-size: 12px;
}

.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
  content: '14px';
  font-size: 14px;
}

.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';
  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';
  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';
  font-size: 20px;
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimSun]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimSun]::before {
  content: "宋体";
  font-family: "SimSun";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimHei]::before {
  content: "黑体";
  font-family: "SimHei";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Microsoft-YaHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Microsoft-YaHei]::before {
  content: "微软雅黑";
  font-family: "Microsoft YaHei";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=KaiTi]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=KaiTi]::before {
  content: "楷体";
  font-family: "KaiTi";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=FangSong]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=FangSong]::before {
  content: "仿宋";
  font-family: "FangSong";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Arial]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Arial]::before {
  content: "Arial";
  font-family: "Arial";
}

/* .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Times-New-Roman]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Times-New-Roman]::before {
  content: "Times New Roman";
  font-family: "Times New Roman";
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=sans-serif]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=sans-serif]::before {
  content: "sans-serif";
  font-family: "sans-serif";
} */

.ql-font-SimSun {
  font-family: "SimSun";
}

.ql-font-SimHei {
  font-family: "SimHei";
}

.ql-font-Microsoft-YaHei {
  font-family: "Microsoft YaHei";
}

.ql-font-KaiTi {
  font-family: "KaiTi";
}

.ql-font-FangSong {
  font-family: "FangSong";
}

.ql-font-Arial {
  font-family: "Arial";
}

/* .ql-font-Times-New-Roman {
  font-family: "Times New Roman";
}

.ql-font-sans-serif {
  font-family: "sans-serif";
} */

2、自定义图片和视频上传(Element UI + OSS)

<div id="toolbar" slot="toolbar">
  <button id="custom-button" @click.prevent="fnOpenUploadImage" title="图片"><i class="iconfont icon-tupian"></i></button>
  <button id="custom-button" @click.prevent="fnOpenUploadVideo" title="视频"><i class="iconfont icon-video2"></i></button>
</div>

  自定义图片和视频上传使用的是Element UI + OSS上传组件,参考上一篇Vue Element UI + OSS上传文件,上传成功之后需要把图片或者视频插入内容区域,通过查看Quill文档,

insertEmbed(index: Number, type: String, value: any, source: String = 'api'): Delta

可以插入视频和图片:

    /**
     * [fnUploadSucess 提交上传文件函数]
     * @author   zoumiao
     * @param {Array} uploadFileUrlList [上传文件返回的url]
     * @return   {null}   [没有返回]
     */
    fnUploadSucess (uploadFileUrlList) {
      this.editor.focus()
      for (let url of uploadFileUrlList) {
        this.editor.insertEmbed(this.editor.getSelection().index, this.uploadType, url)
      }
    }

3、字数统计
  Quill没有字节提供字数统计,API提供了getLength方法,但是如果输入空格,该方法也会计算为字符;通过editor-change事件来计算字符个数:

    /**
     * @description [onEditorChange 输入文本改变事件]
     * @author   zoumiao
     * @param {Object} editor 返回的编辑对象{html, text, quill}
     * @return   {null}   [没有返回]
     */
    onEditorChange (editor) {
      let html = editor.html
      this.preContent = html
      this.$emit('input', html)
      this.contentLength = this._.trim(editor.text).length
    },

4、图片可收缩

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

Quill配置项中也要进行配置:

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' ]
    }
  }
}

  在开发环境中使用这两个插件没有什么问题,但是生成打包之后会报错,vue-quill-editor 的quill-image-resize-module 改变插入图片大小模块引入报错问题

Logo

前往低代码交流专区

更多推荐