img使用object-fit:cover属性,导致页面滚动卡顿

当使用CSS属性object-fit:cover显示大尺寸图片时,可能导致滚动条滚动卡顿,严重影响用户体验。经排查,通过在图片元素上添加`transform: translate3d(0,0,0)`样式属性可以有效缓解这一问题。此解决方案能够提升界面流畅性,改善用户交互体验。

项目线上运行发现只要使用object-fit:cover属性且图片质量大的时候,界面卡顿严重,滚动条滚动卡顿非常影响用户体验。经过各方排查最终发现一个解决方案是在元素上

新增样式属性可以有效解决。

原因:使用img样式属性object-fit:cover,或者使用ele-ui el-image标签属性fit:‘cover’

    <el-image class="cover" @click="additional"
 :src="item.imgUrl" :preview-src-list="imgUrlList" 
fit="cover">

解决方法:添加新属性

.cover {
  transform: translate3d(0, 0, 0)
}
<script setup lang="ts"> import { KtModal } from '@/components/KtModal' import { Upload } from '@element-plus/icons-vue' import { defineExpose, ref, watch } from 'vue' import { CircleStencil, Cropper } from 'vue-advanced-cropper' import 'vue-advanced-cropper/dist/style.css' const visible = ref<boolean>(false) defineExpose({ open }) // 图片相关数据 const imageUrl = ref<string>('') const cropper = ref<InstanceType<typeof Cropper> | null>(null) const croppedImage = ref<string>('') const realtimePreview = ref<string>('') // 新增:实时预览图 const isCropping = ref(false) // 新增:追踪当前是否处于裁剪状态 // 开启节流优化,防止频繁操作 let throttleTimeout: ReturnType<typeof setTimeout> | null = null const THROTTLE_TIME = 300 // 节流时间(ms) function open() { visible.value = true } function closeModal() { visible.value = false imageUrl.value = '' croppedImage.value = '' realtimePreview.value = '' // 清除实时预览 if (throttleTimeout) { clearTimeout(throttleTimeout) throttleTimeout = null } } function handleFileChange(event: Event) { const input = event.target as HTMLInputElement if (input.files && input.files[0]) { const file = input.files[0] const reader = new FileReader() reader.onload = (e) => { if (e.target?.result) { imageUrl.value = e.target.result as string } } reader.readAsDataURL(file) } } // 获取裁剪后的圆形图片(用于最终确认) function getCroppedImage() { generateCroppedImage(true) // 生成高质量版本 closeModal() } // 实时生成裁剪预览(带节流优化) function generateCroppedPreview() { // 如果正在操作,取消前一个计时器 if (isCropping.value || !cropper.value) return isCropping.value = true // 使用节流减少操作频率 if (throttleTimeout) { clearTimeout(throttleTimeout) } throttleTimeout = setTimeout(() => { generateCroppedImage(false) // 生成低质量预览版本 isCropping.value = false }, THROTTLE_TIME) } // 生成裁剪图片函数(可配置质量) function generateCroppedImage(highQuality: boolean = false) { if (!cropper.value) return try { const { canvas } = cropper.value.getResult() if (!canvas) return // 设置预览画布尺寸(低质量预览时使用较小尺寸) const previewSize = highQuality ? canvas.width : Math.min(canvas.width, 200) const roundedCanvas = document.createElement('canvas') const ctx = roundedCanvas.getContext('2d') if (!ctx) return roundedCanvas.width = previewSize roundedCanvas.height = previewSize const radius = previewSize / 2 // 绘制圆形裁剪 ctx.beginPath() ctx.arc(radius, radius, radius, 0, Math.PI * 2) ctx.closePath() ctx.clip() // 绘制原始图片(适当缩放) ctx.drawImage( canvas, 0, 0, canvas.width, canvas.height, 0, 0, previewSize, previewSize, ) // 根据用途设置不同格式和质量 if (highQuality) { // 高质量用于保存/上传 croppedImage.value = roundedCanvas.toDataURL('image/png') console.log('裁剪后的圆形图片:', `${croppedImage.value.substring(0, 50)}...`) } else { // 低质量用于实时预览 realtimePreview.value = roundedCanvas.toDataURL('image/jpeg', 0.7) } } catch (error) { console.error('裁剪错误:', error) } finally { if (throttleTimeout) { clearTimeout(throttleTimeout) throttleTimeout = null } isCropping.value = false } } // 监听图像变化并更新实时预览 watch(imageUrl, (newVal) => { if (newVal) { // 在图片加载后立即生成预览 setTimeout(() => { if (cropper.value) { generateCroppedPreview() } }, 300) } }) </script> <template> <KtModal v-model:show="visible" style="width:480px" title="更换照片" width="500px" @cancel="closeModal" @confirm="getCroppedImage" > <!-- 圆形裁剪区域 --> <div class="cropper-container"> <Cropper v-if="imageUrl" ref="cropper" :src="imageUrl" class="cropper" :stencil-component="CircleStencil" :stencil-props="{ handlers: {}, movable: true, aspectRatio: 1, resizable: true, }" :auto-zoom="true" :background="false" @move="generateCroppedPreview" @resize="generateCroppedPreview" > <template #stencil="{ stencilProps, coordinates, image }"> <CircleStencil :image="image" :coordinates="coordinates" :transition="stencilProps.transition" :aspect-ratio="1" :min-aspect-ratio="1" :max-aspect-ratio="1" :movable="stencilProps.movable" :resizable="false" :handlers="{}" class="circle-cropper" /> </template> </Cropper> <div v-else class="placeholder"> <el-icon :size="50"> <Upload /> </el-icon> <p>请上传图片</p> </div> </div> <!-- 新增:实时预览区域 --> <div class="preview-container"> <h3>实时预览</h3> <div class="preview-image"> <img v-if="realtimePreview" :src="realtimePreview" alt="裁剪预览"> <div v-else class="preview-placeholder"> <el-icon :size="30"> <View /> </el-icon> <p>移动裁剪框查看实时预览</p> </div> </div> </div> <!-- 上传图片 --> <div class="upload-container"> <input ref="fileInput" type="file" accept="image/*" style="display: none;" @change="handleFileChange"> <el-button type="primary" :icon="Upload" @click="() => ($refs.fileInput as HTMLInputElement).click()"> 上传头像 </el-button> </div> </KtModal> </template> <style lang="scss" scoped> .cropper-container { width: 220px; height: 220px; margin: auto; position: relative; background-color: #f0f0f0; border-radius: 4px; overflow: hidden; margin-bottom: 16px; margin-top: 40px; .cropper { height: 100%; // 添加圆形预览样式 :deep(.circle-cropper) { border-radius: 50%; border: 2px solid white; box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.4); } // 隐藏默认的拖动柄 :deep(.cropper-handler) { display: none; } } .placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #999; font-size: 14px; p { margin-top: 8px; } } } /* 新增:预览容器样式 */ .preview-container { text-align: center; margin: 20px 0; h3 { margin-bottom: 10px; color: #606266; font-size: 14px; } .preview-image { width: 120px; height: 120px; margin: 0 auto; border: 1px solid #ebeef5; border-radius: 50%; overflow: hidden; background-color: #f5f7fa; img { width: 100%; height: 100%; object-fit: cover; } .preview-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #c0c4cc; font-size: 12px; padding: 10px; } } } .upload-container { text-align: center; } </style> 还是没有实时更新,我想要实现的效果是我移动裁剪框,预览就会同步显示裁剪框的内容
06-05
{{ 'swiper-bundle.min.css' | asset_url | stylesheet_tag }} <style> .featured_video_flex { width: 1200px; margin: 0 auto; display: flex; justify-content: space-around; /* 更改为 "flex-start" */ scroll-padding: 0 20px; /* 添加 padding 避免内容紧贴容器边缘 */ } .featured_video_header { width: 100%; border-bottom: 2px solid #fff; margin-bottom: 20px; } .featured_video_header h2 { text-align: left; margin-bottom: 5px; } .featured_video_flex_video { width: 32%; padding-bottom: 20%; scroll-snap-align: start; /* 设置滚动对齐方式为 "start" */ flex-shrink: 0; /* 禁止内容缩小以适应容器 */ } .mySwiper_zuoy { position: relative; height: 70px; width: 110px; margin: 0 auto 30px; } .swiper-button-prevvideo { position: absolute; top: 50%; transform: translateY(-50%); left: 0; width: 40px!important; height: 40px!important; text-align: center; border-radius: 50%; background: #17bbef; background-image: url('https://cdn.shopify.com/s/files/1/0512/8568/8505/files/Icon-3.svg?v=1660122964'); background-repeat: no-repeat; background-position: center center; transition: background-color 0.5s ease; display: block!important; z-index: 999; } .swiper-button-prevvideo:hover{ background: #ccc; background-image: url('https://cdn.shopify.com/s/files/1/0512/8568/8505/files/Icon-3.svg?v=1660122964'); background-repeat: no-repeat; background-position: center center; } .swiper-button-nextvideo { position: absolute; top: 50%; transform: translateY(-50%); right: 0; width: 40px!important; height: 40px!important; display: block!important; text-align: center; border-radius: 50%; background: #17bbef; background-image: url(https://cdn.shopify.com/s/files/1/0512/8568/8505/files/Icon-2.svg?v=1660122964); background-repeat: no-repeat; background-position: center center; transition: background-color 0.5s ease; z-index: 999; } .swiper-button-nextvideo:hover{ background: #ccc; background-image: url('https://cdn.shopify.com/s/files/1/0512/8568/8505/files/Icon-2.svg?v=1660122964'); background-repeat: no-repeat; background-position: center center; } @media (max-width: 1200px) { .featured_video_flex { width: 95%; } } @media (max-width: 768px) { .featured_video_flex { display: flex; overflow-x: auto; /* 将滚动效果更改为自动 */ overflow-x: scroll; /* 添加水平滚动效果 */ scroll-snap-type: x mandatory; /* 设置滚动属性为 "x" 方向和 "mandatory" */ scroll-snap-type: none; /* 取消滚动捕捉效果 */ justify-content: flex-start; } .featured_video_flex_video { width: 100%; padding-bottom: 56.25%; margin-bottom: 30px; } } </style> <div class="featured_video_flex"> {%- if section.settings.title != blank -%} <div class="featured_video_header"> <h2>{{ section.settings.title }}</h2> </div> {%- endif -%} <div class="swiper videomySwiper"> <div class="swiper-wrapper"> {% for block in section.blocks %} <div class="swiper-slide"> {%- if block.settings.video_url == blank -%} <iframe src="//www.youtube.com/embed/_9VUPq3SxOc?rel=0&showinfo=0&vq=720" width="850" height="480" frameborder="0" allowfullscreen ></iframe> {%- else -%} {%- if block.settings.video_url.type == 'youtube' -%} <iframe src="//www.youtube.com/embed/{{ block.settings.video_url.id }}?rel=0&showinfo=0&vq=720" width="850" height="480" frameborder="0" allowfullscreen ></iframe> {%- endif -%} {%- if block.settings.video_url.type == 'vimeo' -%} <iframe src="//player.vimeo.com/video/{{ block.settings.video_url.id }}?color={{ block.color_button | remove: "#" }}&byline=0&portrait=0&badge=0" width="850" height="480" frameborder="0" allowfullscreen ></iframe> {%- endif -%} {%- endif -%} </div> {% endfor %} </div> <div class="swiper-button-nextvideo"></div> <div class="swiper-button-prevvideo"></div> </div> </div> {%- if section.settings.divider -%}</div>{%- endif -%} <script src="{{ 'jquery.min.js' | asset_url }}"></script> <script src="{{ 'swiper-bundle.min.js' | asset_url }}"></script> <script> var swiper = new Swiper('.videomySwiper', { navigation: { nextEl: '.swiper-button-nextvideo', prevEl: '.swiper-button-prevvideo', }, speed: 2000, loop: true, autoplay: { delay: 3000, // 自动切换的时间间隔,单位为毫秒 disableOnInteraction: false, // 用户操作swiper之后是否禁止自动切换 }, spaceBetween: 10, pagination: { el: '.swiper-pagination', clickable: true, }, breakpoints: { 580: { // 当屏幕宽度大于等于320 slidesPerView: 1, spaceBetween: 6 }, 768: { // 当屏幕宽度大于等于768 slidesPerView: 2, spaceBetween: 10 }, 880: { // 当屏幕宽度大于等于768 slidesPerView: 3, spaceBetween: 10 } } }); // 鼠标悬停时停止轮播 swiper.el.addEventListener("mouseenter", function() { swiper.autoplay.stop(); }); // 鼠标离开时重新开始轮播 swiper.el.addEventListener("mouseleave", function() { swiper.autoplay.start(); }); </script> {% schema %} { "name": "视频flex", "class": "index-section", "settings": [ { "type": "text", "id": "title", "label": "t:sections.featured-video.settings.title.label" } ], "blocks": [ { "type": "slide", "name": "添加视频", "settings": [ { "type": "video_url", "id": "video_url", "label": "t:sections.featured-video.settings.video_url.label", "default": "https://www.youtube.com/watch?v=_9VUPq3SxOc", "accept": ["youtube", "vimeo"] } ] } ], "presets": [ { "name": "视频", "blocks": [ { "type": "slide" }, { "type": "slide" }, { "type": "slide" }, { "type": "slide" } ] } ] } {% endschema %}
06-12
<template> <view class="container"> <picker :range="communities" @change="onCommunityChange"> <view class="picker">选择小区:{{ selectedCommunity || '请选择' }}</view> </picker> <input class="input" placeholder="请输入详细位置" v-model="location" /> <textarea class="textarea" placeholder="请输入工作内容" v-model="content"></textarea> <view class="section-title">上传照片(带水印)</view> <view class="image-preview"> <view class="image-item" v-for="(img, index) in images" :key="index"> <image :src="img" class="preview-img" @click="previewImage(index)" /> <view class="delete-btn" @click.stop="removeImage(index)">×</view> </view> </view> <button class="upload-btn" @click="chooseImages">选择照片</button> <button class="submit-btn" @click="submit">提交</button> <!-- 隐藏 canvas 用于绘制 --> <canvas canvas-id="watermarkCanvas" id="watermarkCanvas" type="2d" style="position: fixed; top: -9999px; left: -9999px; width: 1px; height: 1px;" ></canvas> </view> </template> <script> export default { data() { return { communities: ['小区A', '小区B', '小区C'], selectedCommunity: '', location: '', content: '', images: [] }; }, methods: { onCommunityChange(e) { this.selectedCommunity = this.communities[e.detail.value]; }, chooseImages() { if (!this.selectedCommunity || !this.location || !this.content) { uni.showToast({ title: '请填写完整信息', icon: 'none' }); return; } uni.chooseImage({ count: 3, sourceType: ['camera', 'album'], success: async (res) => { const filePaths = res.tempFilePaths; for (let path of filePaths) { try { const watermarked = await this.addWatermark(path); this.images.push(watermarked); } catch (e) { console.warn('添加水印失败,使用原图', e); this.images.push(path); } } } }); }, async addWatermark(imagePath) { console.log('开始获取图片信息:', imagePath); const imageInfo = await new Promise((resolve, reject) => { uni.getImageInfo({ src: imagePath, success: resolve, fail: reject }); }); const width = imageInfo.width; const height = imageInfo.height; console.log('图片宽高:', width, height); // 设置 canvas 宽高 this.$nextTick(() => { const canvasEl = document.getElementById('watermarkCanvas'); if (canvasEl) { canvasEl.width = width; canvasEl.height = height; canvasEl.style.width = width + 'px'; canvasEl.style.height = height + 'px'; } }); const ctx = uni.createCanvasContext('watermarkCanvas', this); console.log('开始绘制 canvas'); const now = new Date(); const timeStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; ctx.clearRect(0, 0, width, height); ctx.drawImage(imagePath, 0, 0, width, height); const overlayHeight = 120; ctx.setFillStyle('rgba(0, 0, 0, 0.45)'); ctx.fillRect(0, height - overlayHeight, width, overlayHeight); ctx.setFillStyle('#fff'); ctx.setFontSize(28); ctx.setTextBaseline('top'); const lineHeight = 36; ctx.fillText(`时间:${timeStr}`, 20, height - overlayHeight + 10); ctx.fillText(`地点:${this.selectedCommunity} ${this.location}`, 20, height - overlayHeight + 10 + lineHeight); ctx.fillText(`内容:${this.content}`, 20, height - overlayHeight + 10 + lineHeight * 2); return new Promise((resolve, reject) => { setTimeout(() => { ctx.draw(false, () => { console.log('canvas.draw 完成,开始导出'); setTimeout(() => { uni.canvasToTempFilePath({ canvasId: 'watermarkCanvas', destWidth: width, destHeight: height, success: (res) => { console.log('导出成功:', res.tempFilePath); resolve(res.tempFilePath); }, fail: (err) => { console.error('导出失败:', err); reject(err); } }, this); }, 300); }); // 超时保护 setTimeout(() => reject(new Error('canvas.draw() 超时')), 5000); }, 300); // 延时以确保 canvas 尺寸同步完成 }); }, previewImage(index) { uni.previewImage({ urls: this.images, current: this.images[index] }); }, removeImage(index) { this.images.splice(index, 1); }, submit() { if (!this.images.length) { uni.showToast({ title: '请先添加照片', icon: 'none' }); return; } uni.showToast({ title: '提交成功(演示)', icon: 'success' }); } } }; </script> <style scoped> .container { padding: 30rpx; background-color: #f4f6f8; height: 100vh; overflow: auto; } .picker, .input, .textarea { padding: 20rpx; margin-bottom: 30rpx; background-color: #fff; border: 1px solid #ccc; border-radius: 10rpx; } .textarea { height: 150rpx; } .section-title { font-weight: bold; margin: 20rpx 0 10rpx; } .image-preview { display: flex; flex-wrap: wrap; gap: 10rpx; margin-bottom: 20rpx; } .image-item { position: relative; } .preview-img { width: 150rpx; height: 150rpx; border-radius: 10rpx; object-fit: cover; border: 1rpx solid #ddd; } .delete-btn { position: absolute; top: -10rpx; right: -10rpx; width: 40rpx; height: 40rpx; background-color: rgba(0, 0, 0, 0.6); color: white; text-align: center; line-height: 40rpx; font-size: 30rpx; border-radius: 50%; z-index: 10; } .upload-btn, .submit-btn { background-color: #007aff; color: white; padding: 20rpx; border-radius: 10rpx; text-align: center; margin-bottom: 20rpx; } .canvas { position: fixed; left: -9999px; top: -9999px; width: 1px; height: 1px; } </style> 这是我的代码,你看看怎么盖
06-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值