vue-quill-editor 富文本編輯器封裝,可上傳圖片酬凳,視頻(附源碼)

組件說(shuō)明:
(1)支持圖片上傳到服務(wù)器惠况,也可使用base64的方式
(2)支持視頻上傳,或者插入視頻鏈接
【其實(shí)無(wú)論是上傳圖片到服務(wù)器還是上傳視頻到服務(wù)器宁仔,其實(shí)本質(zhì)上都是上傳文件然后后端返回一個(gè)地址插入到編輯器中】
(3)當(dāng)前版本為vue 2.x + element ui稠屠,暫不支持vue 3.0

  1. 安裝
npm install vue-quill-editor --save

2.引入下面我封裝的組件

<!--富文本編輯器-->
<template>
  <div class="RichTextEditor-Wrap" v-loading="loading">

    <quill-editor :content="content"
                  :options="editorOption"
                  class="ql-editor"
                  ref="myQuillEditor"
                  @change="onEditorChange($event)">
    </quill-editor>

    <!-- 圖片上傳組件輔助-->
    <el-upload
      v-show="false"
      :show-file-list="false"
      :name="uploadImgConfig.name"
      :multiple="false"
      :action="uploadImgConfig.uploadUrl"
      :before-upload="onBeforeUpload"
      :on-success="onSuccess"
      :on-error="onError"
      :file-list="fileList">
      <!--<i class="el-icon-upload"></i>-->
      <!--<div class="el-upload__text">將文件拖到此處,或<em>點(diǎn)擊上傳</em></div>-->
      <!--<div class="el-upload__tip" slot="tip">最多只能上傳兩個(gè)附件</div>-->
      <button ref="myinput">上傳文件</button>
    </el-upload>

    <!--視頻上傳-->
    <div>
      <el-dialog
        :close-on-click-modal="false"
        width="50%"
        style="margin-top: 1px"
        title="視頻上傳"
        :visible.sync="videoDialog.show"
        append-to-body>
        <el-tabs v-model="videoDialog.activeName">
          <el-tab-pane label="添加視頻鏈接" name="first">
            <el-input v-model="videoDialog.videoLink" placeholder="請(qǐng)輸入視頻鏈接" clearable></el-input>
            <el-button type="primary" size="small" style="margin: 20px 0px 0px 0px "
                       @click="addVideoLink(videoDialog.videoLink)">添加
            </el-button>
          </el-tab-pane>
          <el-tab-pane label="本地視頻上傳" name="second">
            <el-upload
              v-loading="loading"
              style="text-align: center;"
              drag
              :action="uploadVideoConfig.uploadUrl"
              accept="video/*"
              :name="uploadVideoConfig.name"
              :before-upload="onBeforeUploadVideo"
              :on-success="onSuccessVideo"
              :on-error="onErrorVideo"
              :multiple="false">
              <i class="el-icon-upload"></i>
              <div class="el-upload__text">將文件拖到此處,或<em>點(diǎn)擊上傳</em></div>
              <div class="el-upload__tip" slot="tip">只能上傳MP4文件权埠,且不超過(guò){{uploadVideoConfig.maxSize}}M</div>
            </el-upload>

          </el-tab-pane>
        </el-tabs>
      </el-dialog>
    </div>
  </div>
</template>
<script>
  // require styles
  import 'quill/dist/quill.core.css'
  import 'quill/dist/quill.snow.css'
  import 'quill/dist/quill.bubble.css'
  import {quillEditor} from 'vue-quill-editor'
  // 設(shè)置title
  import {addQuillTitle} from './quill-title.js'

  // 工具欄
  const toolbarOptions = [
    ['bold', 'italic', 'underline', 'strike'], // toggled buttons
    ['blockquote', 'code-block'],
    [{'header': 1}, {'header': 2}],
    [{'list': 'ordered'}, {'list': 'bullet'}],
    [{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
    [{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
    [{'direction': 'rtl'}],
    [{'size': ['small', false, 'large', 'huge']}],
    [{'header': [1, 2, 3, 4, 5, 6, false]}],
    [{'font': []}],
    [{'color': []}, {'background': []}], // dropdown with defaults from theme
    [{'align': []}],
    [{'clean': '清除'}], // remove formatting button
    // ['link', 'image', 'video']
    ['image', 'video']
  ]
  export default {
    name: 'RichTextEditor',
    model: {
      prop: 'content',
      event: 'change'
    },
    components: {
      quillEditor
    },
    props: {
      content: { // 返回的html片段
        type: String,
        default: ''
      },
      uploadImgConfig: { // 圖片上傳配置 - 若不配置則使用quillEditor默認(rèn)方式榨了,即base64方式
        type: Object,
        default(){
          return {
            uploadUrl: '', // 圖片上傳地址
            maxSize: 2, // 圖片上傳大小限制,默認(rèn)不超過(guò)2M
            name: 'Filedata' // 圖片上傳字段
          }
        }
      },
      uploadVideoConfig: { // 視頻上傳配置
        type: Object,
        default(){
          return {
            uploadUrl: '', // 上傳地址
            maxSize: 10, // 圖片上傳大小限制攘蔽,默認(rèn)不超過(guò)2M
            name: 'Filedata' // 圖片上傳字段
          }
        }
      }
    },
    data() {
      let _self = this;
      return {
        loading: false, // 加載loading
        editorOption: {
          placeholder: '',
          theme: 'snow', // or 'bubble'
          modules: {
            toolbar: {
              container: toolbarOptions, // 工具欄
              handlers: {
                'video': function (value) {
                  _self.videoDialog.show = true;
                }
              }
            }
          }
        },

        // 圖片上傳變量
        fileList: [],

        // 視頻上傳變量
        videoDialog: {
          show: false,
          videoLink: '',
          activeName: 'first'
        }
      }
    },
    mounted () {
      // 初始給編輯器設(shè)置title
      addQuillTitle()

      let toolbar = this.$refs['myQuillEditor'].quill.getModule('toolbar');
      // 是否開(kāi)啟圖片上傳到服務(wù)器功能
      if (this.uploadImgConfig.uploadUrl) {
        toolbar.addHandler('image', this.addImageHandler);
      }

    },
    methods: {
      // 文本編輯
      onEditorChange ({quill, html, text}) {
        // console.log('editor change!', quill, html, text)
        // console.log(html.replace(/<[^>]*>|/g, ''), 33333333)
        this.$emit('update:content', html)
        this.$emit('change', html)
      },
      hideLoading(){
        this.loading = false
      },
      // --------- 圖片上傳相關(guān) start ---------

      addImageHandler(value){
        if (value) {
          // 觸發(fā)input框選擇圖片文件
          this.$refs['myinput'].click();
        } else {
          this.quill.format('image', false)
        }
      },
      // 把已經(jīng)上傳的圖片顯示回富文本編輯框中
      uploadSuccess (imgurl) {
        let quill = this.$refs['myQuillEditor'].quill
        let range = quill.getSelection()
        let index = 0;
        if (range == null) {
          index = 0;
        } else {
          index = range.index; // 獲取光標(biāo)所在位置
        }
        // 插入
        quill.insertEmbed(index, 'image', imgurl) // imgurl是服務(wù)器返回的圖片鏈接地址
        // 調(diào)整光標(biāo)到最后
        quill.setSelection(index + 1)
      },
      // el-文件上傳組件
      onBeforeUpload (file) {
        this.loading = true
        let acceptArr = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']
        const isIMAGE = acceptArr.includes(file.type)
        const isLt1M = file.size / 1024 / 1024 < this.uploadImgConfig.maxSize
        if (!isIMAGE) {
          this.hideLoading()
          this.$message.error('只能插入圖片格式!')
        }
        if (!isLt1M) {
          this.hideLoading()
          this.$message.error(`上傳文件大小不能超過(guò) ${this.uploadImgConfig.maxSize}MB!`)
        }
        return isLt1M && isIMAGE
      },
      // 文件上傳成功時(shí)的鉤子
      onSuccess (response, file, fileList) { // ---- 注意這部分需要改為對(duì)應(yīng)的返回格式
        this.hideLoading()
        if (response.retCode === '00') {
          this.uploadSuccess(response.url)
        } else {
          this.$message.error('上傳失敗')
        }
      },
      // 文件上傳失敗時(shí)的鉤子
      onError (file, fileList) {
        this.hideLoading()
        this.$message.error('上傳失敗')
      },

      // --------- 圖片上傳相關(guān) end ---------

      // --------- 視頻上傳相關(guān) start ---------

      addVideoLink(videoLink) {
        if (!videoLink) return this.$message.error('請(qǐng)輸入視頻地址')
        this.videoDialog.show = false
        let quill = this.$refs['myQuillEditor'].quill
        let range = quill.getSelection()
        let index = 0;
        if (range == null) {
          index = 0;
        } else {
          index = range.index;
        }
        // 插入
        quill.insertEmbed(index, 'video', videoLink)
        // 調(diào)整光標(biāo)到最后
        quill.setSelection(index + 1)
      },

      // el-文件上傳組件
      onBeforeUploadVideo (file) {
        this.loading = true
        let acceptArr = ['video/mp4']
        const isVideo = acceptArr.includes(file.type)
        const isLt1M = file.size / 1024 / 1024 < this.uploadVideoConfig.maxSize
        if (!isVideo) {
          this.hideLoading()
          this.$message.error('只能上傳mp4格式文件!')
        }
        if (!isLt1M) {
          this.hideLoading()
          this.$message.error(`上傳文件大小不能超過(guò) ${this.uploadVideoConfig.maxSize}MB!`)
        }
        return isLt1M && isVideo
      },
      // 文件上傳成功時(shí)的鉤子
      onSuccessVideo (response, file, fileList) { // ---- 注意這部分需要改為對(duì)應(yīng)的返回格式
        this.hideLoading()
        if (response.retCode === '00') {
          this.addVideoLink(response.url)
        } else {
          this.$message.error('上傳失敗')
        }
      },
      // 文件上傳失敗時(shí)的鉤子
      onErrorVideo (file, fileList) {
        this.hideLoading()
        this.$message.error('上傳失敗')
      },

      // --------- 視頻上傳相關(guān) end ---------
    }
  }
</script>
<style>
  .RichTextEditor-Wrap .ql-container {
    height: 300px;
  }

  .RichTextEditor-Wrap .ql-editor {
    padding: 0;
  }

  .RichTextEditor-Wrap .ql-tooltip {
    left: 5px !important;
  }
</style>


quill-title.js這個(gè)文件是拿來(lái)給工具欄設(shè)置title的阻逮,代碼如下:

const titleConfig = {
  'ql-bold': '加粗',
  'ql-font': '字體',
  'ql-code': '插入代碼',
  'ql-italic': '斜體',
  'ql-link': '添加鏈接',
  'ql-color': '字體顏色',
  'ql-background': '背景顏色',
  'ql-size': '字體大小',
  'ql-strike': '刪除線',
  'ql-script': '上標(biāo)/下標(biāo)',
  'ql-underline': '下劃線',
  'ql-blockquote': '引用',
  'ql-header': '標(biāo)題',
  'ql-indent': '縮進(jìn)',
  'ql-list': '列表',
  'ql-align': '文本對(duì)齊',
  'ql-direction': '文本方向',
  'ql-code-block': '代碼塊',
  'ql-formula': '公式',
  'ql-image': '圖片',
  'ql-video': '視頻',
  'ql-clean': '清除字體樣式'
}

export function addQuillTitle () {
  const oToolBar = document.querySelector('.ql-toolbar')
  const aButton = oToolBar.querySelectorAll('button')
  const aSelect = oToolBar.querySelectorAll('select')
  aButton.forEach(function (item) {
    if (item.className === 'ql-script') {
      item.value === 'sub' ? item.title = '下標(biāo)' : item.title = '上標(biāo)'
    } else if (item.className === 'ql-indent') {
      item.value === '+1' ? item.title = '向右縮進(jìn)' : item.title = '向左縮進(jìn)'
    } else {
      item.title = titleConfig[item.className]
    }
  })
  // 字體顏色和字體背景特殊處理,兩個(gè)在相同的盒子
  aSelect.forEach(function (item) {
    if (item.className.indexOf('ql-background') > -1) {
      item.previousSibling.title = titleConfig['ql-background']
    } else if (item.className.indexOf('ql-color') > -1) {
      item.previousSibling.title = titleConfig['ql-color']
    } else {
      item.parentNode.title = titleConfig[item.className]
    }
  })
}

title.gif

組件使用: 注意props秩彤,其中 uploadImgConfig 是針對(duì)上傳圖片的配置叔扼,如果不傳則默認(rèn)使用原始的base64方式,視頻上傳必須要有上傳服務(wù)器的地址

image.png

組件復(fù)制過(guò)去后漫雷,還需更改下下面這部分代碼瓜富,根據(jù)自己的接口返回值類型更改:
image.png

組件調(diào)用方式
支持v-model、.sync方式降盹。

上傳圖片效果圖:

image.png

上傳視頻效果圖:

image.png

image.png

最后: 對(duì)于組件中上傳圖片与柑、視頻的格式判斷可自行更改,也可提取到props中蓄坏,整個(gè)組件代碼邏輯比較簡(jiǎn)單价捧,大家自由發(fā)揮吧。

組件中視頻上傳封裝參考了這篇文章:
https://zhuanlan.zhihu.com/p/108705388

如果對(duì)你有幫助涡戳,點(diǎn)個(gè)贊支持下吧结蟋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市渔彰,隨后出現(xiàn)的幾起案子嵌屎,更是在濱河造成了極大的恐慌,老刑警劉巖恍涂,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宝惰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡再沧,警方通過(guò)查閱死者的電腦和手機(jī)尼夺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)炒瘸,“玉大人淤堵,你說(shuō)我怎么就攤上這事∈惭啵” “怎么了粘勒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)屎即。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么技俐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任乘陪,我火速辦了婚禮,結(jié)果婚禮上雕擂,老公的妹妹穿的比我還像新娘啡邑。我一直安慰自己,他們只是感情好井赌,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布谤逼。 她就那樣靜靜地躺著,像睡著了一般仇穗。 火紅的嫁衣襯著肌膚如雪流部。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天纹坐,我揣著相機(jī)與錄音枝冀,去河邊找鬼。 笑死耘子,一個(gè)胖子當(dāng)著我的面吹牛果漾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谷誓,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼绒障,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了捍歪?” 一聲冷哼從身側(cè)響起端盆,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎费封,沒(méi)想到半個(gè)月后焕妙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弓摘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年焚鹊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片韧献。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡末患,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锤窑,到底是詐尸還是另有隱情璧针,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布渊啰,位于F島的核電站探橱,受9級(jí)特大地震影響申屹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隧膏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一哗讥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胞枕,春花似錦杆煞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至派桩,卻和暖如春构诚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窄坦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工唤反, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸭津。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓彤侍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親逆趋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盏阶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容