VUE 的弹出框实现图片预览和视频预览
这是一个基于Vue3封装的媒体预览组件,主要功能包括:
- 多格式支持:可同时预览图片和视频
- 图片操作功能:
- 缩放(支持滚轮缩放和按钮控制)
- 旋转(90度增量旋转)
- 拖拽(仅在放大状态下可用)
- 自适应显示:图片自动适应容器大小
- 响应式设计:使用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>