黄冈市网站建设_网站建设公司_Sketch_seo优化
2025/12/23 0:21:49 网站建设 项目流程

一.效果展示

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

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

二.处理前的效果图。

1.问题截图

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

三.解决方案。

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

<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文件

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上传组件

<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组件

<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文件

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

/** * 图片压缩 * @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) }) }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询