vue3+el-upload+多张图片(20MB左右)+图片压缩上传到后端+可限制条数+懒加载

一.效果展示

1.展开折叠项,加载视图中图片,

2.随着滚动条移动,逐渐加载对应图片

二.处理前的效果图。

1.问题截图

这就是部分大图加载不出来的截图,就是因为一下加载整个表格的所有图片,一个是卡,第二个最主要的是加载不出来,但是点击单个图片预览却可以,这也是我做这个懒加载的原因。

三.解决方案。

1.页面模版展示图片部分

javascript 复制代码
<div>
          <vxe-grid
            :ref="(el) => (gridRefs[tableIndex] = el)"
            v-bind="gridOptions2"
            v-on="gridEvents"
            border="none"
            :stripe="true"
            maxHeight="300px"
            :data="item.checkItemDetail"
          >
            
            
            <template #referenceImageUrl_content="{ row }">
              <div
                v-if="
                  props.type === 'view' ||
                  formData.documentStatus?.includes('审批') ||
                  formData.documentStatus === '已完成' ||
                  tableIndex < secondTable.length - 1
                "
              >
                <template v-if="row.pictureList.length">
                  <elImageDialog :pictureList="row.pictureList" />
                </template>

                <img v-else class="custom-image" src="../../../../assets/imgs/暂无图片.png"/>
              </div>
              <div v-else>
                <div v-visible="(v) => (row._imgVisible = v)" style="min-height: 110px">
                  <UploadMoreImage
                    v-if="row._imgVisible"
                    :limit="3"
                    :fileList="row.pictureList"
                    @update:fileList="(val) => (row.pictureList = val)"
                    @view="handlePreview"
                    @fileStatusChange="(status) => handleFileStatusChange(status, row)"
                  />
                </div>
              </div>
            </template>
          </vxe-grid>
          </div>
        </div>

<!-- 图片预览弹窗 -->
    <el-dialog v-model="dialogVisible" width="60%">
      <img w-full :src="dialogImageUrl" alt="Preview Image" style="width: 100%" />
    </el-dialog>

 //引入v-visible
import { vVisible } from '@/directive/visible'


// 预览图片
const handlePreview = (file) => {
  dialogImageUrl.value = file.url // 设置预览的图片 URL
  dialogVisible.value = true // 显示预览弹窗
}

注释:大家只需看<template #referenceImageUrl_content="{ row }">... </template>里面这部分

2.v-visible实现

directive/visible.ts文件

javascript 复制代码
import type { Directive } from 'vue'

const observers = new WeakMap<Element, IntersectionObserver>()

export const vVisible: Directive<HTMLElement, (v: boolean) => void> = {
  mounted(el, binding) {
    createObserver(el, binding.value)
  },

  updated(el, binding) {
    // 当绑定函数变化 or 重新渲染时,重新监听
    if (binding.value !== binding.oldValue) {
      cleanup(el)
      createObserver(el, binding.value)
    }
  },

  unmounted(el) {
    cleanup(el)
  }
}

function createObserver(el: HTMLElement, callback: (v: boolean) => void) {
  if (typeof callback !== 'function') return

  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        callback(true)
        observer.disconnect()
      }
    },
    {
      rootMargin: '150px'
    }
  )

  observers.set(el, observer)
  observer.observe(el)
}

function cleanup(el: Element) {
  const observer = observers.get(el)
  if (observer) {
    observer.disconnect()
    observers.delete(el)
  }
}

3.UploadMoreImage上传组件

javascript 复制代码
<template>
  <el-upload
    ref="upload"
    class="custom-upload"
    :action="uploadUrl"
    v-model:file-list="fileList"
    :limit="props.limit"
    list-type="picture-card"
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :auto-upload="false"
    :show-file-list="true"
    multiple
    :on-change="handleChange"
    :on-exceed="handleExceed"
  >
    <el-icon><Plus /></el-icon>
  </el-upload>
</template>

<script lang="ts" setup>
import { ref, computed, defineProps, watch } from 'vue'
import { ElUpload, ElMessage, ElIcon, genFileId } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
import { compressImage } from '@/utils/compressImage'
// Emit事件
const $emit = defineEmits(['view', 'update:fileList', 'fileStatusChange'])

// Props
const props = defineProps({
  limit: {
    type: Number,
    default: 1 // 默认限制为1张图片
  },
  fileList: {
    type: Array,
    default: () => []
  }
})
const upload = ref<UploadInstance>()
const fileList = ref(props.fileList) // 图片文件列表
const uploadUrl = 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15' // 上传接口

// 预览图片
const handlePreview = (file) => {
  $emit('view', file)
}

// 删除图片
const handleRemove = (file) => {
  $emit('fileStatusChange', true)
  const index = fileList.value.indexOf(file)
  if (index > -1) {
    fileList.value.splice(index, 1) // 删除选中的图片
  }
  $emit('update:fileList', fileList.value)
}
// 当超出限制数量时,清空已上传的图片,重新上传
const handleExceed: UploadProps['onExceed'] = (files) => {
  // 判断当前上传的文件数量,超过限制的文件进行处理
  const excessFiles = files.slice(0, files.length - props.limit)
  
  // 校验上传的文件
  files.forEach((newFile, index) => {
    // 只处理没有超过限制的文件
    if (index >= excessFiles.length) {
      if (!newFile.type.startsWith('image/') || newFile.size / 1024 / 1024 > 20) {
        ElMessage.error('请上传20MB以内的图片格式!')
        return
      }
      const raw = newFile as UploadRawFile
      raw.uid = genFileId()

      // 构造可展示的文件对象
      const uploadFile = {
        name: raw.name,
        url: URL.createObjectURL(raw),
        raw
      }

      // 仅在未超出限制时添加文件
      // if (fileList.value.length < props.limit) {
      //   fileList.value.push(uploadFile)
      // } else {
      //   // 如果超过限制,替换掉最后一张
      //   fileList.value.splice(fileList.value.length - 1, 1, uploadFile)
      // }
      if (fileList.value.length < props.limit) {
        // 仅在未超出限制时添加文件
        fileList.value.push(uploadFile)
      } else {
        // 超过限制时删除上传的文件并提示图片最大上传数量3张
        ElMessage.error(`图片最大上传数量${props.limit}张!`)
        return // 直接退出,避免继续添加文件
      }
    }
  })
}
// 格式化文件大小
const formatFileSize = (bytes) => {
  if (bytes === 0 || bytes == null) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  const k = 1024
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  const size = (bytes / Math.pow(k, i)).toFixed(2)
  return `${size} ${units[i]}`
}
const handleChange = async (file, flist) => {
  const rawFile = file.raw as File
  const sizeMB = rawFile.size / 1024 / 1024
  const isImg = rawFile.type.startsWith('image/')
  console.log("当前图片大小", formatFileSize(rawFile?.size))
  if (!isImg) {
    ElMessage.error('请上传图片格式!')
    const index = fileList.value.length - 1
    fileList.value.splice(index, 1)
    return
  }

  if (sizeMB > 20) {
    ElMessage.error('请上传 20MB 以内的图片格式!')
    const index = fileList.value.length - 1
    fileList.value.splice(index, 1)
    return
  }

  if (sizeMB > 5) {
    try {
      const compressedFile = await compressImage(rawFile, 1280, 0.8)
      file.raw = compressedFile
      file.size = compressedFile.size
      file.url = URL.createObjectURL(compressedFile)
    } catch (e) {
      ElMessage.error('图片压缩失败')
      return
    }
  }

  if (flist.length > props.limit) return

  $emit('update:fileList', flist)
  $emit('fileStatusChange', true)
}
</script>

<style lang="less" scoped>
.custom-upload {
  position: relative;
}
::v-deep(.el-upload--picture-card) {
  --el-upload-picture-card-size: 103px;
}

::v-deep(.el-upload-list--picture-card) {
  --el-upload-list-picture-card-size: 103px;
}
::v-deep(.el-icon--close-tip) {
  display: none !important;
}
</style>

注释:图片压缩

import { compressImage } from '@/utils/compressImage'

if (sizeMB > 5) {

try {

const compressedFile = await compressImage(rawFile, 1280, 0.8)

file.raw = compressedFile

file.size = compressedFile.size

file.url = URL.createObjectURL(compressedFile)

} catch (e) {

ElMessage.error('图片压缩失败')

return

}

}

压缩前后大小截图:直接从19.77MB压缩到6.92 MB

4.elImageDialog组件

javascript 复制代码
<template>
  <div>
    <!-- 图片展示区域 -->
    <div class="image-gallery">
      <img
        v-for="(img, index) in props.pictureList"
        :key="index"
        v-lazy="img.url"
        :class="props.isWidth ? 'thumbnail2' : 'thumbnail'"
        @click="handlePreview(index)"
      />
    </div>

    <!-- 预览图片区域 -->
    <div v-if="showPreview" class="preview-overlay" @click="closePreview">
      <img :src="props.pictureList[currentPreviewIndex].url" class="preview-image" />
    </div>
  </div>
</template>

<script setup>
import { ref, defineProps } from 'vue';

// 接收外部传入的图片数据
const props = defineProps({
  pictureList: {
    type: Array,
    required: true
  },
  isWidth:{
    type: Boolean,
    default:false
  }
});

// 当前预览图片的索引
const currentPreviewIndex = ref(0);

// 是否显示预览图片
const showPreview = ref(false);

// 处理点击图片时,打开预览
const handlePreview = (index) => {
  currentPreviewIndex.value = index;
  showPreview.value = true;
};

// 关闭预览
const closePreview = () => {
  showPreview.value = false;
};
</script>

<style lang="less" scoped>
.image-gallery {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.thumbnail {
  width: 100px;
  height: 100px;
  object-fit: cover;
  cursor: pointer;
  transition: transform 0.3s ease;
}
.thumbnail2 {
  width: 70px;
  height: 70px;
  object-fit: cover;
  cursor: pointer;
  transition: transform 0.3s ease;
}

.thumbnail:hover {
  transform: scale(1.1);
}

.preview-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  cursor: pointer;
}

.preview-image {
  max-width: 90%;
  max-height: 90%;
}
</style>

5.v-lazy实现

directive/lazy.ts文件

javascript 复制代码
import type { Directive } from 'vue'

const observers = new WeakMap<Element, IntersectionObserver>()

export const vLazy: Directive<HTMLImageElement, string> = {
  mounted(el, binding) {
    initObserver(el, binding.value)
  },

  updated(el, binding) {
    // 👇 关键:图片地址变了
    if (binding.value !== binding.oldValue) {
      // 重置 src,避免复用旧图
      el.src = ''

      // 重新监听
      observers.get(el)?.disconnect()
      initObserver(el, binding.value)
    }
  },

  /**
 * 清理元素关联的MutationObserver并移除观察者引用
 * @param {HTMLElement} el - 需要清理观察者的DOM元素
 */
unmounted(el) {
    observers.get(el)?.disconnect()
    observers.delete(el)
  }
}

function initObserver(el: HTMLImageElement, src: string) {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      el.src = src
      observer.unobserve(el)
    }
  })

  observers.set(el, observer)
  observer.observe(el)
}

6.全局注册v-lazy

import { createApp } from 'vue'

import App from './App.vue'

import { vLazy } from '@/directive/lazy'

const app = createApp(App)

app.directive('lazy', vLazy)

app.mount('#app')

7.图片压缩组件

utils/compressImage.ts

javascript 复制代码
/**
 * 图片压缩
 * @param file 原始图片 File
 * @param maxWidth 最大宽度
 * @param quality 压缩质量 0~1
 */
export function compressImage(
  file: File,
  maxWidth = 1280,
  quality = 0.8
): Promise<File> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()

    reader.onload = (e) => {
      const img = new Image()
      img.src = e.target?.result as string

      img.onload = () => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')!

        let { width, height } = img

        // 等比缩放
        if (width > maxWidth) {
          height = (maxWidth / width) * height
          width = maxWidth
        }

        canvas.width = width
        canvas.height = height

        ctx.drawImage(img, 0, 0, width, height)

        canvas.toBlob(
          (blob) => {
            if (!blob) return reject('压缩失败')

            const compressedFile = new File([blob], file.name, {
              type: file.type,
              lastModified: Date.now()
            })
            resolve(compressedFile)
          },
          file.type,
          quality
        )
      }

      img.onerror = reject
    }

    reader.onerror = reject
    reader.readAsDataURL(file)
  })
}
相关推荐
一颗奇趣蛋2 小时前
AI Rules & MCP 抄作业(附samples)
前端·openai
BD_Marathon2 小时前
Vue3_列表渲染
前端·javascript·vue.js
知其然亦知其所以然2 小时前
为什么说 String 是 JavaScript 中“最安静却最危险”的类型
前端·javascript·程序员
wusp19942 小时前
【超完整】Tailwind CSS 实战教程
前端·css·tailwind
南山安2 小时前
React 学习:父传子的单项数据流——props
javascript·react.js·前端框架
jun_不见2 小时前
nest初体验-用nest实现一个简单的CRUD功能
前端·node.js·全栈
这个需求建议不做2 小时前
pdf.js(pdfdist)踩坑workerSrc报错pdf.worker.mjs无法正确获取
开发语言·javascript·pdf
soda_yo2 小时前
React哲学:保持组件纯粹 哈气就要哈得纯粹
前端·react.js·设计
小小8程序员2 小时前
springboot + vue
vue.js·spring boot·后端