当前位置: 首页 > news >正文

VUE 的弹出框实现图片预览和视频预览

这是一个基于Vue3封装的媒体预览组件,主要功能包括:

  1. 多格式支持:可同时预览图片和视频
  2. 图片操作功能
    • 缩放(支持滚轮缩放和按钮控制)
    • 旋转(90度增量旋转)
    • 拖拽(仅在放大状态下可用)
  3. 自适应显示:图片自动适应容器大小
  4. 响应式设计:使用Element UI的Dialog作为容器

组件特点:

  • 通过计算属性动态计算图片样式
  • 使用requestAnimationFrame优化拖拽性能
  • 支持图片加载后自动调整方向
  • 提供视频播放控制功能

该组件封装了完整的交互逻辑,可方便地集成到项目中实现媒体预览功能。

下面是实现代码:

<template><el-dialog v-model="visible" width="1184px" class="preview-dialog" close align-center><template v-if="!isVideoPreview" #footer><div class="preview-dialog-footer"><el-button type="text" @click="zoomOut" class="zoom-button"><svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"viewBox="0 0 24 24"fill="none"stroke="currentColor"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg></el-button><el-button type="text" @click="zoomIn" class="zoom-button"><svgxmlns="http://www.w3.org/2000/svg"width="20"height="20"viewBox="0 0 24 24"fill="none"stroke="currentColor"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg></el-button><el-button type="text" @click="rotateImage(90)" class="rotate-button"><img :src="Rotate" /></el-button></div></template><div class="preview-content" @wheel="handleWheel"><imgv-if="!isVideoPreview":src="previewUrl"@load="onImageLoad":style="imageStyle"ref="previewImage"@mousedown="startDrag"@mousemove="onDrag"@mouseup="endDrag"@mouseleave="endDrag"/><video v-if="isVideoPreview" :src="previewUrl" class="media-video" controls autoplay></video></div></el-dialog>
</template><script setup>import { ref, computed, watch } from 'vue';import Rotate from '@/assets/home/icon/rotate.svg';const props = defineProps({modelValue: Boolean,previewUrl: {type: String,default: ''},isVideoPreview: Boolean});const emit = defineEmits(['update:modelValue']);const visible = ref(props.modelValue);watch(() => props.modelValue,(newVal) => {visible.value = newVal;});watch(visible, (val) => {emit('update:modelValue', val);});const imageRotation = ref(0);const previewImage = ref(null);const dialogWidth = 1184;const dialogHeight = 648;const zoomLevel = ref(1);// 缩放限制const minZoom = 0.1;const maxZoom = 5;// 拖拽相关变量const isDragging = ref(false);const dragStartX = ref(0);const dragStartY = ref(0);const imageStartLeft = ref(0);const imageStartTop = ref(0);const imageLeft = ref(0);const imageTop = ref(0);const rafId = ref(0);const zoomIn = () => {if (zoomLevel.value < maxZoom) {zoomLevel.value = Math.min(zoomLevel.value + 0.1, maxZoom);}};const zoomOut = () => {if (zoomLevel.value > minZoom) {zoomLevel.value = Math.max(zoomLevel.value - 0.1, minZoom);}};const handleWheel = (event) => {event.preventDefault();if (event.deltaY < 0) {zoomIn();} else {zoomOut();}};const startDrag = (event) => {if (zoomLevel.value <= 1) return; // 只有在放大时才能拖拽isDragging.value = true;dragStartX.value = event.clientX;dragStartY.value = event.clientY;imageStartLeft.value = imageLeft.value;imageStartTop.value = imageTop.value;if (previewImage.value) {previewImage.value.style.cursor = 'grabbing';}// 阻止默认行为,防止图片被选中event.preventDefault();};const onDrag = (event) => {if (!isDragging.value || zoomLevel.value <= 1) return;// 使用 requestAnimationFrame 优化性能if (rafId.value) {cancelAnimationFrame(rafId.value);}rafId.value = requestAnimationFrame(() => {const deltaX = event.clientX - dragStartX.value;const deltaY = event.clientY - dragStartY.value;imageLeft.value = imageStartLeft.value + deltaX;imageTop.value = imageStartTop.value + deltaY;rafId.value = 0;});// 阻止默认行为event.preventDefault();};const endDrag = () => {isDragging.value = false;if (rafId.value) {cancelAnimationFrame(rafId.value);rafId.value = 0;}if (previewImage.value) {previewImage.value.style.cursor = 'grab';}};const rotateImage = (degree) => {console.log('翻转', degree, (imageRotation.value + degree) % 360);imageRotation.value += degree;// zoomIn();// 旋转时重置缩放级别以避免布局问题zoomLevel.value = 1;// 重置拖拽位置imageLeft.value = 0;imageTop.value = 0;};const imageDimensions = computed(() => {if (!previewImage.value) return { width: 0, height: 0 };const img = previewImage.value;const naturalWidth = img.naturalWidth;const naturalHeight = img.naturalHeight;const isRotated = imageRotation.value % 180 !== 0;const displayWidth = isRotated ? naturalHeight : naturalWidth;const displayHeight = isRotated ? naturalWidth : naturalHeight;return { width: displayWidth, height: displayHeight };});const imageStyle = computed(() => {if (!previewImage.value) return {};const { width: displayWidth, height: displayHeight } = imageDimensions.value;// 计算基础缩放比例,确保图片适应容器const baseScale = Math.min(dialogWidth / displayWidth, dialogHeight / displayHeight);// 应用用户缩放级别const finalScale = baseScale * zoomLevel.value;// 计算缩放后的尺寸const scaledWidth = displayWidth * finalScale;const scaledHeight = displayHeight * finalScale;// 居中定位const left = (dialogWidth - scaledWidth) / 2 + imageLeft.value;const top = (dialogHeight - scaledHeight) / 2 + imageTop.value;return {position: 'absolute',left: `${left}px`,top: `${top}px`,width: `${scaledWidth}px`,height: `${scaledHeight}px`,transform: `rotate(${imageRotation.value}deg)`,transformOrigin: 'center center',cursor: zoomLevel.value > 1 ? 'grab' : 'default'};});const onImageLoad = () => {// 重置旋转和缩放imageRotation.value = 0;zoomLevel.value = 1;imageLeft.value = 0;imageTop.value = 0;for (let index = 0; index < 4; index++) {console.log('执行第几次', index + 1);rotateImage(90); //执行四次 可以让图片以合适的宽度呈现}// 可选:调试用// console.log('Image loaded:', previewImage.value.naturalWidth, previewImage.value.naturalHeight);};
</script><style lang="scss" scoped>.preview-dialog {:deep(.el-dialog) {height: 648px;display: flex;flex-direction: column;}:deep(.el-dialog__body) {flex: 1;overflow: hidden !important;text-align: center;padding: 0;position: relative;}.preview-dialog-footer {display: flex;justify-content: center;align-items: center;}.rotate-button {font-size: 20px;padding: 10px;}.preview-content {width: 1184px;height: 648px;display: flex;justify-content: center;align-items: center;overflow: hidden;position: relative;img {max-width: none;max-height: none;object-fit: contain;user-select: none;// 添加硬件加速transform: translateZ(0);backface-visibility: hidden;perspective: 1000px;}.media-video {max-width: 100%;max-height: 100%;object-fit: contain;}}}
</style>

http://www.dtcms.com/a/350207.html

相关文章:

  • (多线程)线程安全和线程不安全 产生的原因 synchronized关键字 synchronized可重入特性死锁 如何避免死锁 内存可见性
  • React Native核心技术深度解析_Trip Footprints
  • 电商商品管理效率低?MuseDAM 系统如何破解库存混乱难题
  • AR技术:航空维修工具校准的精准革命
  • 【python】if __name__ == ‘__main__‘的作用
  • 正则表达式 —— \s*
  • C语言运行时候出现栈溢出、段错误(Segmentation fault)、异常码解决?
  • 车灯最新测试标准测试设备太阳光模拟器
  • Kafka 在 6 大典型用例的落地实践架构、参数与避坑清单
  • 【Flink】运行模式
  • Rust Async 异步编程(五):async/.await
  • 怎么把iphone文件传输到windows电脑?分场景选方法
  • 【ansible】roles的介绍
  • 【完整源码+数据集+部署教程】化妆品实例分割系统源码和数据集:改进yolo11-DynamicConv
  • 【C#】.net framework 4.8非常久远的框架如何把日期格式/Date(1754548600000)/以及带T的2025-08-07T14:36:40时间格式转为统一的格式输出
  • 并发编程原理与实战(二十六)深入synchronized底层原理实现
  • 京东API分类接口实战指南:获取各类商品信息
  • Microsoft 365 中的 School Data Sync 功能深度解析:教育机构数字化管理的智能核心
  • Android音频学习(十五)——打开输出流
  • 如何用DeepSeek让Excel数据处理自动化:告别重复劳动的智能助手
  • 面试手写 Promise:链式 + 静态方法全实现
  • 扣子智能体商业化卡在哪?井云系统自动化交易+私域管理,闭环成交全流程拆解
  • 3491定期复盘代码实现设计模式的忌假应用
  • 使用Docker配置Redis Stack集群的步骤
  • React 19 与 Next.js:利用最新 React 功能
  • SQL性能调优
  • HTTP、HTTPS 与 WebSocket 详解
  • UDS诊断案例-新能源汽车电池管理系统(BMS)诊断
  • Git提交流程与最佳实践
  • debug kernel 的一些trace的方法