vue3+js+elementPlus使用富文本编辑器@vueup/vue-quill详细教程

 更新时间:2024年07月08日 08:20:19   作者:FixUpSth  
富文本编辑器在任何项目中都会用到,下面这篇文章主要给大家介绍了关于vue3+js+elementPlus使用富文本编辑器@vueup/vue-quill的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

本篇文章是基于vue3、js、elementPlus框架进行的,

主要是核心涉及的包是以下三个

  "vue": "^3.2.47",
  "@vueup/vue-quill": "^1.0.0-alpha.40",
  "element-plus": "^2.3.6",

如果有问题的先看下版本是否一致。因为我每次找解决方案的时候,发现了好多问题都是版本不一致导致的。
本篇文章使用到的编辑器包含以下几个功能:

  • 输入文本
  • 插入图片(以img标签的形式插入,并且还能在标签上插入文件id)
  • 工具栏显示为中文
  • 工具栏hover后有中文提示
  • 已插入的图片文件的删除

暂时没有完善的:

  • 图片的大小尺寸修改和支持拖拽图片(后面如果解决了会更新,当前日期是2023年7月4日)

源码

共涉及两个文件,一个Editor/index.vue,一个Editor/quill.js

Editor/index.vue

详细目录是src/components/Editor/index.vue

<template>
  <el-upload :action="uploadUrl" :before-upload="handleBeforeUpload" :on-success="handleUploadSuccess" name="richTextFile"
    :on-error="handleUploadError" :show-file-list="false" class="editor-img-uploader" accept=".jpeg,.jpg,.png">
    <i ref="uploadRef" class="Plus editor-img-uploader"></i>
  </el-upload>
  <div class="editor">
    <QuillEditor id="editorId" ref="myQuillEditor" v-model:content="editorContent" contentType="html"
      @update:content="onContentChange" :options="options" />
  </div>
</template>
 
<script setup>
import { QuillEditor, Quill } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted } from "vue";
// 引入插入图片标签自定义的类
import './quill'

// 注册图片拖拽和大小修改插件(不起效果暂时屏蔽)
// import { ImageDrop } from 'quill-image-drop-module';
// import {ImageResize} from 'quill-image-resize-module';

// Quill.register('modules/ImageDrop', ImageDrop);
// Quill.register('modules/imageResize', ImageResize);

const { proxy } = getCurrentInstance();
const emit = defineEmits(['update:content', 'getFileId', 'handleRichTextContentChange'])
const props = defineProps({
  /* 编辑器的内容 */
  content: {
    type: String,
    default: '',
  },
  /* 只读 */
  readOnly: {
    type: Boolean,
    default: false,
  },
  // 上传文件大小限制(MB)
  fileSize: {
    type: Number,
    default: 10,
  },
})

const editorContent = computed({
  get: () => props.content,
  set: (val) => {
    emit('update:content', val)
  }
});
const myQuillEditor = ref(null)
const uploadUrl = ref(import.meta.env.VITE_BASEURL + '/sysFiles/upload') // 上传的图片服务器地址
const oldContent = ref('')

const options = reactive({
  theme: 'snow',
  debug: 'warn',
  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'], // 清除文本格式
        ['link', 'image'], // 链接、图片、视频
      ],
      handlers: {
        // 重写图片上传事件
        image: function (value) {
          if (value) {
            //调用图片上传
            proxy.$refs.uploadRef.click()
          } else {
            Quill.format("image", true);
          }
        },
      },
      // ImageDrop: true,//支持图片拖拽
      // imageResize: { //支持图片大小尺寸修改
      //   displayStyles: {
      //     backgroundColor: 'black',
      //     border: 'none',
      //     color: 'white'
      //   },
      //   modules: ['Resize', 'DisplaySize','Toolbar']
      // }
    }
  },
  placeholder: '请输入公告内容...',
  readOnly: props.readOnly,
  clipboard: {
    matchers: [
      ['img', (node, delta) => {
        const src = node.getAttribute('src');
        const id = node.getAttribute('id');
        delta.insert({ image: { src, 'id': id } });
      }],
    ],
  },
})

// toolbar标题(此项是用来增加hover标题)
const titleConfig = ref([
  { 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: '两端对齐' }
])


// 上传前校检格式和大小
function handleBeforeUpload(file) {
  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
  const isJPG = type.includes(file.type);
  //检验文件格式
  if (!isJPG) {
    ElMessage.error(`图片格式错误!只能上传jpeg/jpg/png格式`)
    return false
  }
  // 校检文件大小
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize
    if (!isLt) {
      ElMessage.error(`上传文件大小不能超过 ${props.fileSize} MB!`)
      return false
    }
  }
  return true
}

// 监听富文本内容变化,删除被服务器中被用户回车删除的图片
function onContentChange(content) {
  emit('handleRichTextContentChange', content)
}


// 上传成功处理
function handleUploadSuccess(res, file) {
  // 如果上传成功
  if (res.status == 200) {
    let rawMyQuillEditor = toRaw(myQuillEditor.value)
    // 获取富文本实例
    let quill = rawMyQuillEditor.getQuill();
    // 获取光标位置
    let length = quill.selection.savedRange.index;
    // 插入图片,res为服务器返回的图片链接地址
    const imageUrl = import.meta.env.VITE_BASE_FILE_PREFIX + res.body[0].lowPath;
    const imageId = res.body[0].id;
    quill.insertEmbed(length, 'image', {
      url: imageUrl,
      id: imageId,
    });

    quill.setSelection(length + 1);
    emit('getFileId', res.body[0].id)
  } else {
    ElMessage.error('图片插入失败')
  }
}
// 上传失败处理
function handleUploadError() {
  ElMessage.error('图片插入失败')
}

// 增加hover工具栏有中文提示
function initTitle() {
  document.getElementsByClassName('ql-editor')[0].dataset.placeholder = ''
  for (let item of titleConfig.value) {
    let tip = document.querySelector('.ql-toolbar ' + item.Choice)
    if (!tip) continue
    tip.setAttribute('title', item.title)
  }
}

onMounted(() => {
  initTitle()
  oldContent.value = props.content
})
</script>
//通过css样式来汉化
<style>
.editor,
.ql-toolbar {
  white-space: pre-wrap !important;
  line-height: normal !important;
}

.editor-img-uploader {
  display: none;
}

.ql-editor {
  min-height: 200px;
  max-height: 300px;
  overflow: auto;
}


.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: '等宽字体';
}
</style>

Editor/quill.js

用于使得插入图片标签的时候能够插入id在图片标签上,不然直接使用insertEmbed方法是无法插入id在img标签上的

import { Quill } from '@vueup/vue-quill'
var BlockEmbed = Quill.import('blots/block/embed')
class ImageBlot extends BlockEmbed {
  static create(value) {
    let node = super.create();
    node.setAttribute('src', value.url);
    node.setAttribute('id', value.id)
    // node.setAttribute('width', value.width)
    // node.setAttribute('height', value.height)
    return node;
  }

  static value(node) {
    return {
      url: node.getAttribute('src'),
      id: node.getAttribute('id'),
    }
  }
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';
Quill.register(ImageBlot)

父组件中的handleRichTextContentChange事件

// 根据富文本实时变化,观察有没有删除已经上传的id
function handleRichTextContentChange(content) {
  const currentIds = getRichTextIds(content)
  if (uploadedRichTextIds.value.length > 0) {
    // 拿当前form里面已经上传的id来进行查询,如果不存在currentIds里面,则已经被删除
    uploadedRichTextIds.value.find(oldId => {
      if (!currentIds.includes(oldId) && !removedRichTextIds.value.includes(oldId)) {
        removedRichTextIds.value.push(oldId) //向删除的id里面推入被删除的项
        let index = uploadedRichTextIds.value.indexOf(oldId)
        uploadedRichTextIds.value.splice(index, 1) //删除已上传的过程记录变量
      }
    })
  }
}

父组件的getFileId方法

// 富文本组件随时更新已经上传的富文本id
function getFileId(id) {
  uploadedRichTextIds.value.push(id)
  console.log('uploadedRichTextIds', uploadedRichTextIds.value);
}

父组件的getRichTextIds 方法,用于获取富文本中含有的图片的id集合

/**
 * 
 * @param {String} content //富文本字符串
 * @param {Array} ids //富文本里面的图片文件id集合
 */
function getRichTextIds(content) {
  const ids = []
  const myDiv = document.createElement("div");
  myDiv.innerHTML = content;
  const imgDom = myDiv.getElementsByTagName('img')
  for (let i = 0; i < imgDom.length; i++) {
    // 只有富文本处的img标签是有id的
    if (imgDom[i].src && imgDom[i].id) {
      ids.push(imgDom[i].id)
    }
  }
  return ids
}

最终我会向后端提交removedRichTextIds,这些是已经在富文本编辑过程中已经上传到服务器中的文件id,需要被删除掉,不然服务器会一直存储着这些文件,造成服务器的空间紧张

整体思路

文本输入、汉化工具栏、增加hover提示整体都是比较简单的传统思路,只是上传图片没有采用base64的方式,是因为base64插入一两张后,整个富文本就会变得巨大无比,导致整个页面加载都非常卡顿,因此只能采用插入img标签的形式。在插入img标签之后需要被回显成正常的图片,因此也就只能实时上传,用后端返回的路径来拼接显示。

虽然这样轻量了,但是问题也来了,如果用户使用回车删除了该图片,在服务器还是会存在该张图片。因此在用户删除时,也要删除服务器中该文件。

因此,我们通过id来确定用户到底删除的是哪张图片。首先在插入图片时,就将upload后后端返回的id插入到对应图片的img标签上,用id属性名=id属性值的方式绑定到img标签上。同时使用一个记录变量uploadedRichTextIds 来记录已经上传的id,通过富文本编辑器本身自带的事件change来监听当前的富文本内容,通过getRichTextIds方法获取当前富文本中的img标签里面的id组合,和uploadedRichTextIds中的id进行比对,这便知道哪些是已经上传过但是又被用户删除的文件了。这个地方是我的难点,因此我想记录一下。

待解决

最后,我想加入图片可以自由调节大小,可拖拽的插件,但是在网上寻求了很多解决方案,始终没有解决,如果有朋友解决了这个问题,麻烦评论区回复我一下,因为富文本编辑器真的经常要用到!!非常感谢,如果我解决了我也会及时更新的!!

总结

到此这篇关于vue3+js+elementPlus使用富文本编辑器@vueup/vue-quill的文章就介绍到这了,更多相关vue3+js+elementPlus富文本编辑器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue升级带来的elementui冲突警告:Invalid prop: custom validator check failed for prop “type“.的解决方案

    Vue升级带来的elementui冲突警告:Invalid prop: custom va

    在页面渲染的时候,控制台弹出大量警告,严重影响控制台的信息获取功能,但是页面基本能正常显示,这是因为Vue升级带来的elementui冲突警告: Invalid prop: custom validator check failed for prop “type“.的解决方案,本文给大家介绍了详细的解决方案
    2025-04-04
  • Vue3中使用this的详细教程

    Vue3中使用this的详细教程

    在vue3中新的组合式API中没有this,那我们如果需要用到this怎么办?下面这篇文章主要给大家介绍了关于Vue3中使用this的详细教程,需要的朋友可以参考下
    2023-07-07
  • Vue 中 reactive创建对象类型响应式数据的方法

    Vue 中 reactive创建对象类型响应式数据的方法

    在 Vue 的开发世界里,响应式数据是构建交互性良好应用的基础,之前我们了解了ref用于定义基本类型的数据,今天就来深入探讨一下如何使用reactive定义对象类型的响应式数据,感兴趣的朋友一起看看吧
    2025-02-02
  • Vue实现添加数据到二维数组并显示

    Vue实现添加数据到二维数组并显示

    这篇文章主要介绍了Vue实现添加数据到二维数组并显示方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue中使用protobuf的过程记录

    vue中使用protobuf的过程记录

    由于目前公司采用了ProtoBuf做前后端数据交互,进公司以来一直用的是公司大神写好的基础库,完全不了解底层是如何解析的。下面小编给大家分享vue中使用protobuf的过程记录,需要的朋友参考下吧
    2018-10-10
  • Vue子组件关闭后调用刷新父组件的实现

    Vue子组件关闭后调用刷新父组件的实现

    这篇文章主要介绍了Vue子组件关闭后调用刷新父组件的实现方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • vue上传图片文件的多种实现方法

    vue上传图片文件的多种实现方法

    这篇文章主要给大家介绍了关于vue上传图片文件的相关资料,介绍了利用原始input标签form表单上传、elementui自带的el-upload上传以及elementui实现一次性上传多张图片等方法,需要的朋友可以参考下
    2021-05-05
  • vue3+ts使用APlayer的示例代码

    vue3+ts使用APlayer的示例代码

    这篇文章主要介绍了vue3+ts使用APlayer的示例代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • 详解Vue自定义指令及使用

    详解Vue自定义指令及使用

    这篇文章主要介绍了Vue自定义指令及使用,对Vue感兴趣的同学,可以参考下
    2021-05-05
  • el-table树形数据序号排序处理方案

    el-table树形数据序号排序处理方案

    这篇文章主要介绍了el-table树形数据序号排序处理方案,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-03-03

最新评论