解决@vueup/vue-quill图片上传、视频上传问题

2023-09-18 12:11:48

Editor.vue

<template>
  <el-upload
    :action="uploadUrl"
    :before-upload="handleBeforeUpload"
    :on-success="handleUploadSuccess"
    name="files"
    :on-error="handleUploadError"
    :show-file-list="false"
    class="editor-img-uploader"
    accept=".jpeg,.jpg,.png"
    :headers="headers"
  >
    <i ref="uploadRef" class="Plus editor-img-uploader"></i>
  </el-upload>
  <!-- 使用input 标签劫持原本视频上传事件,实现视频上传 -->
  <input
    type="file"
    accept="video/*"
    name="file"
    ref="uploadFileVideo"
    id="uploadFileVideo"
    @change="handleVideoUpload"
    style="opacity: 0; width: 0; height: 0; cursor: pointer"
  />
  <div class="editor">
    <QuillEditor
      id="editorId"
      ref="myQuillEditor"
      v-model:content="editorContent"
      contentType="html"
      @update:content="onContentChange"
      :options="options"
    />
  </div>
</template>

<script setup>
  import { Random } from 'mockjs';
  import { ElMessage } from 'element-plus';
  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 { uploadFichFileToOSSNew } from '@/api/treelog/treelog';
  // 引入插入图片标签自定义的类
  import ImageBlot from './quill-image';
  import Video from './quill-video';
  import { useUserStoreWidthOut } from '@/store/modules/user';
  const userStore = useUserStoreWidthOut();
  const token = userStore.getToken;
  const headers = {
    ssoToken: token,
  };

  Quill.register(Video);
  Quill.register(ImageBlot);
  // 注册图片拖拽和大小修改插件(不起效果暂时屏蔽)
  // 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',
    'uploadVideoConfig',
    'getFileId',
    'handleRichTextContentChange',
  ]);
  const uploadFileVideo = ref();
  const props = defineProps({
    /* 编辑器的内容 */
    content: {
      type: String,
      default: '',
    },
    /* 只读 */
    readOnly: {
      type: Boolean,
      default: false,
    },
    // 上传文件大小限制(MB)
    fileSizeLimit: {
      type: Number,
      default: 10,
    },
  });

  const editorContent = computed({
    get: () => props.content,
    set: (val) => {
      emit('update:content', val);
    },
  });
  const myQuillEditor = ref(null);
  const uploadUrl = 'https://mis-api-test4.petem.com.cn/mis/upload/file/oss?fType=24'; // 上传的图片服务器地址
  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', 'video'], // 链接、图片、视频
        ],
        handlers: {
          // 重写图片上传事件
          image: function (value) {
            if (value) {
              //调用图片上传
              proxy.$refs.uploadRef.click();
            } else {
              Quill.format('image', true);
            }
          },
          video: function (value) {
            if (value) {
              //调用图片上传
              // proxy.$refs.uploadRef.click();
              // 劫持原来的视频点击按钮事件
              document.querySelector('#uploadFileVideo')?.click();
            } else {
              Quill.format('video', 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: '两端对齐' },
  ]);
  const handleBeforeUpload = (file) => {
    console.log('file', file);
    const fileType = file.type;
    const testmsg = file.name.substring(file.name.lastIndexOf('.') + 1);
    const extension =
      testmsg === 'ts' || testmsg === 'mp4' || testmsg === 'mov' || testmsg === 'avi';
    console.log('extension rich editor', extension);
    // 图片
    if (
      fileType == 'image/jpeg' ||
      fileType == 'image/png' ||
      fileType == 'image/gif' ||
      fileType == 'image/jpg' ||
      fileType == 'image/bmp' ||
      fileType == 'image/webp' ||
      fileType == 'video/mov' ||
      fileType == 'video/ts' ||
      fileType == 'video/mp4' ||
      fileType == 'video/avi'
    ) {
      const fileSizeLimit = file.size;
      // 校检文件大小
      const isLt = fileSizeLimit / 1024 / 1024 < props.fileSizeLimit;
      if (!isLt) {
        console.log(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`);
        alert(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`);
        return false;
      } else {
        console.log(`RIch MB!`);
        return true;
      }
    } else {
      alert(`文件格式不正确!`);
      return false;
    }
  };
  //视频上传
  const handleVideoUpload = async (evt) => {
    if (evt.target.files.length === 0) {
      return;
    }
    const formData = new FormData();
    formData.append('files', evt.target.files[0]);
    uploadFichFileToOSSNew(formData).then((res) => {
      console.log(res, '----res');
      if (res.data.fileName) {
        handleUploadVideoSuccess(res.data.fileName, 'video');
        // handleUploadSuccess(res, 'video');
      }
    });
  };

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

  const handleUploadVideoSuccess = (fileRemotePath, type) => {
    try {
      let quill = toRaw(myQuillEditor.value).getQuill();
      // 获取光标位置
      let length = quill.selection.savedRange.index;
      quill.insertEmbed(length, type, {
        url: fileRemotePath,
      });
      // 调整光标到最后
      quill.setSelection(length + 1);
    } catch (error) {
      console.log(error);
    }

    uploadFileVideo.value.value = '';
  };
  // 上传成功处理
  function handleUploadSuccess(res, file) {
    console.log(res, file, '---res');
    // 如果上传成功
    if (res.success) {
      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 = Random.natural(1, 5000);
      // const imageId = res.body[0].id;
      quill.insertEmbed(length, 'image', {
        url: res.data.fileName,
        id: imageId,
      });
      // if (file === 'video') {
      //   quill.insertEmbed(length, 'video', {
      //     url: res.data.fileName,
      //     id: imageId,
      //   });
      // } else {
      //   quill.insertEmbed(length, 'image', {
      //     url: res.data.fileName,
      //     id: imageId,
      //   });
      // }

      quill.setSelection(length + 1);
      emit('getFileId', imageId);
    } 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>
<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>

quill-video.js

import { Quill } from '@vueup/vue-quill';
// 源码中是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) {
    let node = super.create();
    // 添加video标签所需的属性
    node.setAttribute('controls', 'controls');
    node.setAttribute('playsinline', 'true');
    node.setAttribute('webkit-playsinline', 'true');
    node.setAttribute('type', 'video/mp4');
    // poster 属性指定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。
    node.setAttribute('poster', value.poster);
    node.setAttribute('src', this.sanitize(value.url));
    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);
  }

  static value(domNode) {
    // 设置自定义的属性值
    return {
      url: domNode.getAttribute('src'),
      poster: domNode.getAttribute('poster'),
    };
  }

  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;

quill-image.js

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';
export default ImageBlot;

更多推荐

1. js中let、var、const定义变量区别与方式

1声明语法varupperA='A';letupperB='B';constupperC='C';只声明不初始化的结果,【const定义的常量不可以修改,而且必须初始化】//var声明变量varupperA;console.log('打印大写的A:%s',upperA);//结果:打印大写的A:undefined//l

影响软文效果的三大因素,一定要牢记

在信息技术发展速度越来越快的今天,企业宣传时已经不再局限于传统的硬广,开始利用软文来提升曝光率,软文作为一种全新的宣传手段,具有覆盖面广、成本低且持续时间长,但是有三大因素稍不注意就会影响软文发布的效果,下面就让媒介盒子告诉大家是哪些因素。一、&nbsp;软文主题是否明确许多企业为了提高热度,经常会在软文中堆砌过多的关

Java基于SpringBoot的在线考试系统的研究与实现(附源码,教程)

博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W+,Csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌文章目录第一章第二章.主要技术第三章第四章系统设计4.1功能结构4.2数据库设计4.2.1数据库E/R图4.2.2数据库表第五章系统功能实现5.1系统登

P2P协议的传输艺术

TP采用两个TCP连接来传输一个文件。控制连接:服务器以被动的方式,打开众所周知用于FTP的端口21,客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。常用的命令有:list——获取文件目录;reter——取一个文件;store——存一个文件。数据连接:每当一个文件在客户端与服务器之间传输时,就

Learn Prompt-提供示例

目前我们与ChatGPT交流的主要形式是文字。提示除了指令+问题的形式外,还可以包含例子。特别是当我们需要具体的输出时,提供例子可以省去我们对具体任务的解释,帮助ChatGPT更好地理解我们的确切需求,从而提供更准确,更有针对性的答案。1-shot单个示例​值得注意的是,shot代表的是“样本”。0-shot就是没有样

【unity】关于技能释放shader.CreateGPUProgram造成卡顿,优化和定位方法。

关于优化方法,UWA这边有介绍Unity移动端游戏性能优化简谱之CPU耗时调优|单帧|动画|调用|unity|实例化_网易订阅对此,我们可以将Shader通过ShaderVariantCollection收集要用到的变体并进行AssetBundle打包。在将该ShaderVariantCollection资源加载进内存

自动化测试---即selenium

自动化测试自动化测试的意思就是:本来是人去做测试的,现在让机器来帮你做测试。优点体现在:节约成本提高效率减少人工去做事提高复用性UI自动化1、关于UI自动化大家常见工具或者框架有:AutoItV3、selenium、uiautomator2、Appium、adb、ATX-Test、Airtest、ATXServer2、

python教程:内置函数和语法糖触发魔法方法

前言大家早好、午好、晚好吖❤~欢迎光临本文章如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码下面总结python内置函数对应的魔法方法魔法方法数学计算abs(args):返回绝对值,调用__abs__;round(args):返回四舍五入的值,调用__round__;math.floor():向下取整,调用__f

java专题练习(抢红包)

package专题练习;importjava.util.Random;publicclassgrab_red_packet{/*需求:直播抽奖,分别由{2,588,888,1000,10000}五个奖金,请用代码模拟抽奖,奖项出现顺序要随机且不重复打印效果:588元的奖金被抽出*///思路://1.先用数组把奖金定义好

【第49篇】Swin Transformer V2:扩展容量和分辨率

摘要https://arxiv.org/pdf/2111.09883.pdf大规模NLP模型已被证明可以显着提高语言任务的性能,并且没有饱和迹象。它们还展示了与人类一样的惊人的少发能力。本文旨在探索计算机视觉中的大规模模型。我们解决了大型视觉模型训练和应用中的三个主要问题,包括训练不稳定性、预训练和微调之间的分辨率差距

OpenCV(四十四):亚像素级别角点位置优化

1.角点位置亚像素位置优化原理介绍亚像素优化的原理在于通过对初始角点位置的微小调整,利用更精确的灰度信息,来获取更准确的角点位置。传统的角点检测算法基于像素级别的灰度变化来定位角点,而亚像素优化则进一步利用图像灰度的局部变化进行更精细的插值,提高了角点位置的准确度。2.优化角点亚像素位置函数cornerSubPix()

热文推荐