解决@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;
相关推荐
开心工作室_kaic7 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿26 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx