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)
  })
}
相关推荐
Dream-Y.ocean2 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn3 小时前
性能监控
前端
Highcharts.js3 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn3 小时前
构建工具
前端
FRing034 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷5 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen6 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒6 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫7 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus7 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude