vue中使用富文本编辑器vue-quill-editor踩过的坑

1,518 阅读3分钟

image.png

安装

npm install vue-quill-editor -S

引入到组件中

(这里我是直接单独封装)

import { quillEditor } from 'vue-quill-editor'
import { addQuillTitle } from './quill-title.js'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

export default {
    components: { quillEditor }
}

hl-upload实际就是自己项目中el-upload二次封装的,大家根据自己需求来, 这里设置样式隐藏上传按钮,通过富文本框中自定义按钮触发点击上传事件(图片上传成功会添加到文本框内)

<hl-upload
  class="avatar-uploader quill-img"
  :show-file-list="false"
  accept=".png, .PNG, .jpg, .JPG, .jpeg, .JPEG"
  max-file-size="10 * 1024"
  :on-success="quillImgSuccess"
/>
<quill-editor
    ref="quillEditor"
    v-model="content"
    class="editor"
    :options="editorOption"
    @ready="onEditorReady($event)"
    @blur="onEditorBlur($event)"
    @focus="onEditorFocus($event)"
    @change="onEditorChange($event)"
/>
<!-- 文档上传组件辅助 -->
<hl-upload
  class="avatar-uploader quill-file"
  accept=".xlsx, .XLSX, .pptx, .PPTX, .docx, .DOCX, ,PDF, .pdf , .doc , .DOC, .xls, .XLS"
  max-file-size="20 * 1024"
  list-type="picture"
  :file-list.sync="fileLists"
/>

配置

editorOption: {
  theme: 'snow', // or 'bubble'
  placeholder: '请输入内容',
  modules: {
    toolbar: {
      container: [
        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
        ['blockquote', 'code-block'], // 引用  代码块
        [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
        [{ indent: '-1' }, { indent: '+1' }], // 缩进
        [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
        [{ align: [] }], // 对齐方式
        ['clean'], // 清除文本格式
        ['image', 'upload'] // 自定义图片上传,文档上传按钮
      ]
    }
  }
}
js
// 失去焦点事件
onEditorBlur () {
},
// 获得焦点事件
onEditorFocus () {
},
// 内容改变事件
onEditorChange () {
    this.$emit('input', this.content)
},
// 富文本准备时的事件
onEditorReady (event) {
},
 // 图片上传成功
quillImgSuccess (res, file) {
    // res为图片服务器返回的数据
    // 获取富文本组件实例
    const quill = this.$refs.quillEditor.quill
    // 如果上传成功
    if (res) {
      // 获取光标所在位置
      const length = quill.getSelection().index
      // 插入图片  res为服务器返回的图片id
      getAttachmentList({ ids: [res] }).then(response => {
        if (response?.data) {
          response.data.map(item => {
            this.imgLists.push(item)
            quill.insertEmbed(length, 'image', item.url)
          })
          // 调整光标到最后
          quill.setSelection(length + 1)
        }
      })
    }
}

样式放在文末

image.png

到这里,富文本基本已经可以使用了,美中不足的是,文本输入框上方的图标没有鼠标悬停上去的提示文字,这里我们可以通过css加到里面去......

优化图标样式(鼠标悬停显示tooltip)
  • 新建文件quill-title.js
const titleConfig = {
  'ql-bold': '加粗',
  'ql-font': '字体',
  'ql-code': '插入代码',
  'ql-italic': '斜体',
  'ql-link': '添加链接',
  'ql-size': '字体大小',
  'ql-strike': '删除线',
  'ql-script': '上标/下标',
  'ql-underline': '下划线',
  'ql-blockquote': '引用',
  'ql-header': '标题',
  'ql-indent': '缩进',
  'ql-list': '列表',
  'ql-align': '文本对齐',
  'ql-direction': '文本方向',
  'ql-code-block': '代码块',
  'ql-formula': '公式',
  'ql-image': '图片',
  'ql-upload': '文档',
  'ql-clean': '清除字体样式'
}
export function addQuillTitle () {
  const oToolBar = document.querySelector('.ql-toolbar')
  if (oToolBar) {
    const aButton = oToolBar.querySelectorAll('button')
    const aSelect = oToolBar.querySelectorAll('select')
    const aColor = oToolBar.querySelectorAll('.ql-color-picker')
    aButton.forEach(function (item) {
      if (item.className === 'ql-script') {
        item.value === 'sub' ? item.title = '下标' : item.title = '上标'
      } else if (item.className === 'ql-indent') {
        item.value === '+1' ? item.title = '向右缩进' : item.title = '向左缩进'
      } else {
        item.title = titleConfig[item.classList[0]]
      }
    })
    aSelect.forEach(function (item) {
      item.parentNode.title = titleConfig[item.classList[0]]
    })
    aColor.forEach(function (item) {
      if (item.className.includes('ql-background')) {
        item.title = '文本突出显示颜色'
      } else {
        item.title = '字体颜色'
      }
    })
  }
}
  • 富文本组件中引入quill-title.js
import { addQuillTitle } from './quill-title.js'
mounted () {
  addQuillTitle()
},

image.png

image.png 到这里优化就完成了

。 。 。 。 。

发布到测试环境后,测试这时候提了个bug

富文本框可以粘贴复制过来的图片,导致图片保存了却没有同步上传至文件服务器,这里我们需要阻止用户复制粘贴图片到富文本框

查看了下vue-quill-editor的文档,发现在配置项中可以添加粘贴板事件

(这里添加了initClipboard作为粘贴板初始化的标识,防止出现查看和修改富文本时图片无法回填的问题,在富文本框获得焦点时,修改initClipboard的值)

data () {
  return {
    editorOption: {
      theme: 'snow', // or 'bubble'
      placeholder: '请输入内容',
      modules: {
        clipboard: {
          // 粘贴板,处理粘贴时候带图片
          matchers: [[Node.ELEMENT_NODE, this.desMatcher]]
        },
        toolbar: {
          ...
        }
      }
    },
    initClipboard: true, // 粘贴板初始化
  }
}

通过在desMatcher方法里可以对富文本框中输入的内容进行处理

methods: {
  desMatcher (node, Delta) {
    const ts = this
    const ops = []
    Delta.ops.forEach(op => {
      if (op.insert && typeof op.insert === 'string') {
        // 如果粘贴了图片,这里会是一个对象,如果是字符串则可以添加
        ops.push({
          ...op
        })
      } else {
        // 给出提醒
        if (op.insert.image && !ts.initClipboard) {
          ts.$message.warning('不允许粘贴图片,请手动上传')
        } else {
          ops.push({
            ...op
          })
        }
      }
    })
    Delta.ops = ops
    return Delta
  },
  // 获得焦点事件
  onEditorFocus () {
    // 文本回显后,禁用粘贴板
    this.initClipboard = false
  }
}
css
<style lang="scss" scoped>
.editor {
  line-height: normal !important;
  /deep/ {
    .ql-container {
      height: 200px;
    }
    .ql-snow .ql-tooltip[data-mode="link"]::before {
      content: "请输入链接地址:";
    }
    .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
      border-right: 0px;
      content: "保存";
      padding-right: 0px;
    }

    .ql-snow .ql-tooltip[data-mode="video"]::before {
      content: "请输入视频地址:";
    }

    .ql-snow .ql-picker.ql-size .ql-picker-label::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item::before {
      content: "14px";
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
      content: "10px";
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
      content: "18px";
    }
    .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
    .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
      content: "32px";
    }

    .ql-snow .ql-picker.ql-header .ql-picker-label::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item::before {
      content: "文本";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
      content: "标题1";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
      content: "标题2";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
      content: "标题3";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
      content: "标题4";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
      content: "标题5";
    }
    .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
    .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
      content: "标题6";
    }

    .ql-snow .ql-picker.ql-font .ql-picker-label::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item::before {
      content: "标准字体";
    }
    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
      content: "衬线字体";
    }
    .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
    .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
      content: "等宽字体";
    }
    .ql-snow.ql-toolbar .ql-upload {
      background-image: url("~@/assets/images/index/file-word.svg");
      background-size: 18px 18px;
      background-position: center center;
      background-repeat:no-repeat;
    }
    .icon-file-image {
      display: block;
      width: 17px;
      height: 17px;
      background-image: url("~@/assets/images/index/file-image.svg");
      background-size: 17px 17px;
      background-position: center center;
      background-repeat:no-repeat;
    }
  }
}
.quill-img
{
  display: none;
}
.quill-file {
  /deep/.el-upload.el-upload--picture {
    display: none;
  }
}
.file-list {
  .el-icon-close {
    vertical-align: text-bottom;
    margin-left: 5px;
    cursor: pointer;
  }
}
</style>

image.png