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

Editor.vue

javascript 复制代码
<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

javascript 复制代码
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

javascript 复制代码
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;
相关推荐
橙子家4 小时前
浏览器缓存之【基础键值存储】:Local storage 和 Session storage
前端
星星在线6 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒7 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x7 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
京东云开发者8 小时前
京东市民服务又“上新”!这次是黑龙江“龙易办”
前端
袋鱼不重9 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
潜创微科技9 小时前
HDMI1.3 无线传输芯片方案 空旷 150 米量产级音视频方案
音视频
Fireworks9 小时前
深入vue3源码解读 -- 1、响应式的基础概念
前端
程序员黑豆9 小时前
JDK 下载安装与配置详细教程
java·前端·ai编程
hunterandroid9 小时前
文件存储:内部存储与外部存储
前端