StarFire_xm
  • 文章
  • 粉丝
  • 评论

富文本编辑器图片变成base6问题

2025-06-01 10:09:330 次浏览0 次评论技能类型: edit

当在另外一个地方**图片,然后回到富文本编辑区里ctrl+v粘贴图片就会使图片变成base64

<template>
  <div>
    <!-- 隐藏的图片上传组件 -->
    <el-upload
      ref="imageUploader"
      class="hidden-uploader"
      :headers="upload.headers"
      :before-upload="beforeUpload"
      :http-request="customUpload"
      :on-success="handleUploadSuccess"
      :on-error="handleUploadError"
      :file-list="fileList"
      :on-exceed="handleExceed"
      list-type="picture-card"
      :multiple="true"
    >
      <el-button type="primary" style="display: none">点击上传</el-button>
    </el-upload>

    <!-- 富文本编辑器 -->
    <div class="editor">
      <quill-editor
        ref="quillEditorRef"
        v-model:content="content"
        content-type="html"
        :options="options"
        :style="styles"
        @text-change="(e) => $emit('update:modelValue', content)"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, toRaw, onMounted } from 'vue';
import { QuillEditor, Quill } from '@vueup/vue-quill';
import { ElMessage } from 'element-plus';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { globalHeaders } from '@/utils/request'; // 假设你有全局 headers 的工具函数

const emit = defineEmits(['update:modelValue']);

const props = defineProps({
  modelValue: String,
  height: { type: Number, default: 100 },
  minHeight: { type: Number, default: 400 },
  readOnly: { type: Boolean, default: false },
  fileSize: { type: Number, default: 5 }
});

const { proxy } = getCurrentInstance() as ComponentInternalInstance;

const upload = {
  headers: globalHeaders(),
  url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/config/xxxxxxxx' // 使用预签名 URL 接口
};

const quillEditorRef = ref();
const imageUploader = ref(null); // 引用隐藏的上传组件
const fileList = ref([]);

// 监听粘贴事件(自动上传图片)
const setupPasteHandler = () => {
  const editor = quillEditorRef.value.getQuill().root;
  editor.addEventListener('paste', async (e) => {
    const items = (e.clipboardData || window.clipboardData).items;
    let hasImage = false;

    for (const item of items) {
      if (item.type.startsWith('image/')) {
        e.preventDefault();
        hasImage = true;
        const file = item.getAsFile();
        if (file) {
          try {
            // 插入临时占位
            const quill = quillEditorRef.value.getQuill();
            const range = quill.getSelection();
            const placeholderIndex = range?.index || quill.getLength();
            quill.insertEmbed(placeholderIndex, 'image', '[上传中...]');
            quill.setSelection(placeholderIndex + 1);
            console.log("mxmconaaaaaa:",file)
            // 上传到 OSS
            const result:any = await customUpload(file);
            if (result.url) {
              // 替换占位为真实 URL
              quill.deleteText(placeholderIndex, 1);
              quill.insertEmbed(placeholderIndex, 'image', result.url);
            } else {
              throw new Error('上传失败');
            }
          } catch (error) {
            ElMessage.error('图片上传失败: ' + error.message);
            removeUploadPlaceholder();
          }
        }
        break;
      }
    }

    // 非图片内容允许正常粘贴
    if (!hasImage) {
      setTimeout(() => {
        const delta = quillEditorRef.value.getQuill().getContents();
        emit('update:modelValue', quillEditorRef.value.getHTML());
      }, 0);
    }
  });
};

// 移除上传失败的占位
const removeUploadPlaceholder = () => {
  const quill = quillEditorRef.value.getQuill();
  const contents = quill.getContents();
  const ops = contents.ops;
  for (let i = 0; i < ops.length; i++) {
    if (ops[i].insert?.image === '[上传中...]') {
      quill.deleteText(i, 1);
      break;
    }
  }
};

// 上传队列实现
const uploadQueue = {
  tasks: [],
  isProcessing: false,

  addTask(task) {
    return new Promise((resolve, reject) => {
      this.tasks.push({ task, resolve, reject });
      this.processNext();
    });
  },
 
  async processNext() {
    if (this.isProcessing || this.tasks.length === 0) return;
   
    this.isProcessing = true;
    const { task, resolve, reject } = this.tasks.shift();
   
    try {
      const result = await task();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.isProcessing = false;
      this.processNext();
    }
  }
};

const options = ref({
  theme: 'snow',
  bounds: document.body,
  debug: 'warn',
  modules: {
    toolbar: {
      container: [
        ['bold', 'italic', 'underline', 'strike'],
        ['blockquote'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        [{ indent: '-1' }, { indent: '+1' }],
        [{ size: [false, 'large'] }],
        [{ color: [] }, { background: [] }],
        [{ align: [] }],
        ['image', 'video']
      ],
      handlers: {
        image: (value: boolean) => {
          if (value) {
            // 触发隐藏的图片上传组件
            imageUploader.value.$el.querySelector('input[type="file"]').click();
          } else {
            Quill.format('image', false);
          }
        },
        video: (value: boolean) => {
          if (value) {
            uploadVideo();
          } else {
            Quill.format('video', false);
          }
        }
      }
    }
  },
  placeholder: '请输入内容',
  readOnly: props.readOnly
});

const styles = computed(() => {
  let style: any = { width: '100%' };
  if (props.minHeight) {
    style.minHeight = `${props.minHeight}px`;
  }
  if (props.height) {
    style.height = `${props.height}px`;
  }
  return style;
});

const content = ref('');
watch(
  () => props.modelValue,
  (v: string) => {
    if (v !== content.value) {
      content.value = v || '<p></p>';
    }
  },
  { immediate: true }
);

// 图片上传成功回调
const handleUploadSuccess = (response, file, fileList) => {
  console.log(JSON.stringify(response) + '---->');
  if (response.code === 200 || response.url) {
    const quill = toRaw(quillEditorRef.value).getQuill();
    const length = quill.selection.savedRange.index;
    quill.insertEmbed(length, 'image', response.cdnUrl || response.url); // 插入图片
    quill.setSelection(length + 1);
    ElMessage.success('图片上传成功');
  } else {
    ElMessage.error('图片上传失败:' + response.msg);
  }
};

// 图片上传前校验
const beforeUpload = (file) => {
  const isImage = file.type.startsWith('image/');
  const isLt5M = file.size / 1024 / 1024 < props.fileSize;

  if (!isImage) {
    ElMessage.error('只能上传图片格式!');
    return false;
  }
  if (!isLt5M) {
    ElMessage.error(`上传文件大小不能超过 ${props.fileSize} MB!`);
    return false;
  }
  return true;
};

// 自定义图片上传逻辑
const customUpload = async (file) => {
  file = file.file || file;
  if (!file) return;
  return uploadQueue.addTask(async () => {
    const formData = { originalFileName: file.name };
   
    try {
      const response = await fetch(upload.url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      const result = await response.json();
      if (result.code === 200) {
        const signedUrl = result.data.preSigneUrl;
        const uploadResponse = await fetch(signedUrl, {
          method: 'PUT',
          body: file,
          headers: { 'Content-Type': file.type }
        });

        if (uploadResponse.ok) {
          return { url: result.data.cdnUrl, storageId: result.data.objectName };
        } else {
          ElMessage.error(`图片上传失败:${uploadResponse.statusText}`);
          throw new Error('Upload failed');
        }
      } else {
        ElMessage.error(`获取预签名 URL 失败:${result.msg}`);
        throw new Error('Get presigned URL failed');
      }
    } catch (error) {
      ElMessage.error(`上传过程中出现错误:${error.message}`);
      throw error;
    }
  });
};

// 图片上传超出限制回调
const handleExceed = (files: File[]) => {
  ElMessage.error('最多只能上传20张图片');
};

// 图片上传失败回调
const handleUploadError = () => {
  ElMessage.error('图片上传失败,请稍后再试!');
};

// 视频上传逻辑
const uploadVideo = async () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'video/*';
  input.onchange = async (e: any) => {
    const file = e.target.files[0];
    if (!file) return;

    const isVideo = file.type.startsWith('video/');
    const isLt50M = file.size / 1024 / 1024 < 50;

    if (!isVideo) {
      ElMessage.error('只能上传视频格式!');
      return;
    }
    if (!isLt50M) {
      ElMessage.error('上传文件大小不能超过 50MB!');
      return;
    }

    proxy?.$modal.loading('正在上传视频,请稍候...');
    const formData = { originalFileName: file.name };

    try {
      const response = await fetch(upload.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });

      const result = await response.json();
      if (result.code === 200) {
        const signedUrl = result.data.preSigneUrl;
        const uploadResponse = await fetch(signedUrl, {
          method: 'PUT',
          body: file,
          headers: {
            'Content-Type': file.type
          }
        });

        if (uploadResponse.ok) {
          const quill = toRaw(quillEditorRef.value).getQuill();
          const length = quill.selection.savedRange.index;
          quill.insertEmbed(length, 'video', result.data.cdnUrl); // 插入视频
          quill.setSelection(length + 1);
          proxy?.$modal.closeLoading();
        } else {
          ElMessage.error('视频上传失败:' + uploadResponse.statusText);
          proxy?.$modal.closeLoading();
        }
      } else {
        ElMessage.error('获取预签名 URL 失败:' + result.msg);
        proxy?.$modal.closeLoading();
      }
    } catch (error) {
      ElMessage.error('上传过程中出现错误:' + error.message);
      proxy?.$modal.closeLoading();
    }
  };
  input.click();
};

onMounted(() => {
  setupPasteHandler();
});
</script>

<style scoped>
.editor,
.ql-toolbar {
  width: 100%;
  white-space: pre-wrap;
  line-height: normal;
}
.hidden-uploader {
  display: none; /* 隐藏上传组件 */
}
.ql-video {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 10px 0;
}
::v-deep .ql-snow .ql-editor img{
  display: block;
  max-width: 200px;
  margin: 0 auto;
}
</style>


    发表

    还没有评论哦,来抢个沙发吧!