一、新建名为ImageViewer.vue的组件,代码如下:
<!-- ImageViewer.vue -->
<template><div class="fullscreen-viewer" v-if="visible" @click="closeViewer"><div class="viewer-container" @click.stop><div class="image-wrapper" @wheel="handleWheel" @mousedown="startDrag" @mousemove="dragging" @mouseup="endDrag"@mouseleave="endDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"@touchcancel="handleTouchEnd" :style="transformStyle"><img :src="imageUrl" :alt="altText" draggable="false"></div><div class="controls"><span class="zoom-info">{{ Math.round(scale * 100) }}%</span><button class="close-btn" @click="closeViewer">×</button></div></div></div>
</template><script setup>
import { ref, computed } from 'vue'const props = defineProps({imageUrl: {type: String,required: true},altText: {type: String,default: '预览图片'}
})const visible = ref(false)
const scale = ref(1)
const position = ref({ x: 0, y: 0 })
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const touchStart = ref({ x: 0, y: 0, distance: 0, scale: 1 })const transformStyle = computed(() => ({transform: `translate(${position.value.x}px, ${position.value.y}px) scale(${scale.value})`,cursor: isDragging.value ? 'grabbing' : 'grab'
}))const openViewer = () => {visible.value = truescale.value = 1position.value = { x: 0, y: 0 }
}const closeViewer = () => {visible.value = falseisDragging.value = falseposition.value = { x: 0, y: 0 }
}const handleWheel = (e) => {e.preventDefault()const delta = e.deltaY > 0 ? -0.1 : 0.1let newScale = scale.value + deltanewScale = Math.max(0.9, Math.min(3, newScale))scale.value = newScale
}const startDrag = (e) => {isDragging.value = truedragStart.value = {x: e.clientX - position.value.x,y: e.clientY - position.value.y}
}const dragging = (e) => {if (!isDragging.value) returnconst newX = e.clientX - dragStart.value.xconst newY = e.clientY - dragStart.value.y// 计算边界限制const maxX = Math.max(0, (scale.value - 1) * window.innerWidth / 2)const maxY = Math.max(0, (scale.value - 1) * window.innerHeight / 2)position.value = {x: Math.max(-maxX, Math.min(maxX, newX)),y: Math.max(-maxY, Math.min(maxY, newY))}
}const endDrag = () => {isDragging.value = false
}const handleTouchStart = (e) => {if (e.touches.length === 1) {isDragging.value = truedragStart.value = {x: e.touches[0].clientX - position.value.x,y: e.touches[0].clientY - position.value.y}} else if (e.touches.length === 2) {isDragging.value = falseconst touch1 = e.touches[0]const touch2 = e.touches[1]const distance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +Math.pow(touch2.clientY - touch1.clientY, 2))touchStart.value = {x: (touch1.clientX + touch2.clientX) / 2,y: (touch1.clientY + touch2.clientY) / 2,distance: distance,scale: scale.value}}
}const handleTouchMove = (e) => {e.preventDefault()if (e.touches.length === 1 && isDragging.value) {if (e.touches.length === 1 && isDragging.value) {const newX = e.touches[0].clientX - dragStart.value.xconst newY = e.touches[0].clientY - dragStart.value.y// 计算边界限制const maxX = Math.max(0, (scale.value - 1) * window.innerWidth / 2)const maxY = Math.max(0, (scale.value - 1) * window.innerHeight / 2)position.value = {x: Math.max(-maxX, Math.min(maxX, newX)),y: Math.max(-maxY, Math.min(maxY, newY))}}} else if (e.touches.length === 2) {const touch1 = e.touches[0]const touch2 = e.touches[1]const distance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +Math.pow(touch2.clientY - touch1.clientY, 2))const scaleChange = distance / touchStart.value.distancescale.value = Math.max(0.9, Math.min(3, touchStart.value.scale * scaleChange))const centerX = (touch1.clientX + touch2.clientX) / 2const centerY = (touch1.clientY + touch2.clientY) / 2position.value = {x: position.value.x + (centerX - touchStart.value.x),y: position.value.y + (centerY - touchStart.value.y)}touchStart.value.x = centerXtouchStart.value.y = centerY}
}const handleTouchEnd = () => {isDragging.value = false
}defineExpose({openViewer,closeViewer
})
</script><style scoped>
.fullscreen-viewer {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.9);display: flex;justify-content: center;align-items: center;z-index: 9999;
}.viewer-container {position: relative;width: 100%;height: 100%;display: flex;flex-direction: column;
}.image-wrapper {flex: 1;display: flex;justify-content: center;align-items: center;overflow: hidden;transition: transform 0.1s ease-out;touch-action: none;
}.image-wrapper img {max-width: 100%;max-height: 100%;object-fit: contain;user-select: none;-webkit-user-drag: none;
}.controls {position: absolute;bottom: 30px;right: 20px;display: flex;gap: 10px;align-items: center;
}.zoom-info {color: white;background-color: rgba(0, 0, 0, 0.5);padding: 5px 10px;border-radius: 4px;font-size: 14px;
}.close-btn {width: 36px;height: 36px;border-radius: 50%;background-color: rgba(0, 0, 0, 0.5);color: white;border: none;cursor: pointer;font-size: 22px;display: flex;justify-content: center;align-items: center;transition: background-color 0.3s;
}.close-btn:hover {background-color: rgba(255, 255, 255, 0.3);
}@media (max-width: 768px) {.controls {bottom: 30px;right: 20px;}.zoom-info {font-size: 12px;padding: 3px 8px;}.close-btn {width: 36px;height: 36px;font-size: 22px;}
}
</style>
二、引入组件使用
<!-- index.vue -->
<template><div><!-- 点击图片打开查看器 --><img src="your-image-url" @click="openImageViewer" /><!-- 图片查看器组件 --><ImageViewer ref="imageViewer"imageUrl="your-image-url"altText="图片描述"/></div>
</template><script setup>
import { ref } from 'vue'
import ImageViewer from './ImageViewer.vue'const imageViewer = ref(null)const openImageViewer = () => {imageViewer.value.openViewer()
}
</script>