From 281222407fd0a039525694f5e9d8b4c7e224b8e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:32:20 +0000 Subject: [PATCH 1/4] Initial plan From 877311ceb3467f8ead80f6b45024de420f27a1a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:38:03 +0000 Subject: [PATCH 2/4] Implement optimized zoom and pan functionality for PhotoViewer - Replace simple toggle zoom with smooth, interactive zoom/pan system - Add pinch-to-zoom gesture support for touch devices - Add mouse wheel zooming for desktop - Add drag-to-pan when image is zoomed - Use CSS transforms with will-change for GPU acceleration - Smooth transitions with cubic-bezier easing - Scale range: 1x to 4x for better control - Disable swiper touch when dragging zoomed images Co-authored-by: shellRaining <111564053+shellRaining@users.noreply.github.com> --- .../src/client/components/PhotoViewer.vue | 255 ++++++++++++++++-- 1 file changed, 234 insertions(+), 21 deletions(-) diff --git a/packages/theme/src/client/components/PhotoViewer.vue b/packages/theme/src/client/components/PhotoViewer.vue index 812dab2..2c1763c 100644 --- a/packages/theme/src/client/components/PhotoViewer.vue +++ b/packages/theme/src/client/components/PhotoViewer.vue @@ -67,23 +67,32 @@ :keyboard="{ enabled: true }" :navigation="true" :loop="false" + :allow-touch-move="!isDragging && !touchState.isDragging" @swiperslidechange="onSlideChange" >
-
双击或捏合缩放
+
双击或捏合缩放 · 滚轮缩放
@@ -205,13 +214,211 @@ const emit = defineEmits(); const swiperRef = ref(null); const currentIndex = ref(props.initialIndex); -const zoomedSlides = ref(new Set()); const thumbnailsContainerRef = ref(null); const thumbnailRefs: HTMLElement[] = []; const toastRef = ref | null>(null); +// 图片缩放和平移状态 +interface ImageTransform { + scale: number; + translateX: number; + translateY: number; +} + +const imageTransforms = ref>(new Map()); + const currentPhoto = computed(() => props.photos[currentIndex.value]); +// 获取或初始化图片变换状态 +function getTransform(index: number): ImageTransform { + if (!imageTransforms.value.has(index)) { + imageTransforms.value.set(index, { + scale: 1, + translateX: 0, + translateY: 0, + }); + } + return imageTransforms.value.get(index)!; +} + +// 重置图片变换状态 +function resetTransform(index: number) { + imageTransforms.value.set(index, { + scale: 1, + translateX: 0, + translateY: 0, + }); +} + +// 触摸和手势状态 +interface TouchState { + startDistance: number; + startScale: number; + lastX: number; + lastY: number; + isDragging: boolean; +} + +const touchState = ref({ + startDistance: 0, + startScale: 1, + lastX: 0, + lastY: 0, + isDragging: false, +}); + +// 计算两点间距离(用于pinch缩放) +function getDistance(touch1: Touch, touch2: Touch): number { + const dx = touch1.clientX - touch2.clientX; + const dy = touch1.clientY - touch2.clientY; + return Math.sqrt(dx * dx + dy * dy); +} + +// 处理触摸开始 +function handleTouchStart(event: TouchEvent, index: number) { + const transform = getTransform(index); + + if (event.touches.length === 2) { + // 双指触摸:准备缩放 + event.preventDefault(); + touchState.value.startDistance = getDistance( + event.touches[0], + event.touches[1] + ); + touchState.value.startScale = transform.scale; + } else if (event.touches.length === 1 && transform.scale > 1) { + // 单指触摸且已放大:准备拖动 + event.preventDefault(); + touchState.value.isDragging = true; + touchState.value.lastX = event.touches[0].clientX; + touchState.value.lastY = event.touches[0].clientY; + } +} + +// 处理触摸移动 +function handleTouchMove(event: TouchEvent, index: number) { + const transform = getTransform(index); + + if (event.touches.length === 2) { + // 双指缩放 + event.preventDefault(); + const currentDistance = getDistance(event.touches[0], event.touches[1]); + const scale = (currentDistance / touchState.value.startDistance) * touchState.value.startScale; + + // 限制缩放范围 1x-4x + transform.scale = Math.max(1, Math.min(4, scale)); + + // 如果缩放到1,重置平移 + if (transform.scale <= 1) { + transform.translateX = 0; + transform.translateY = 0; + } + } else if (event.touches.length === 1 && touchState.value.isDragging && transform.scale > 1) { + // 单指拖动 + event.preventDefault(); + const deltaX = event.touches[0].clientX - touchState.value.lastX; + const deltaY = event.touches[0].clientY - touchState.value.lastY; + + transform.translateX += deltaX; + transform.translateY += deltaY; + + touchState.value.lastX = event.touches[0].clientX; + touchState.value.lastY = event.touches[0].clientY; + } +} + +// 处理触摸结束 +function handleTouchEnd(event: TouchEvent, index: number) { + touchState.value.isDragging = false; + + // 如果缩放回到1,确保重置平移 + const transform = getTransform(index); + if (transform.scale <= 1) { + transform.translateX = 0; + transform.translateY = 0; + } +} + +// 双击缩放 +function handleDoubleClick(event: MouseEvent, index: number) { + event.preventDefault(); + const transform = getTransform(index); + + if (transform.scale > 1) { + // 已放大,缩小到原始大小 + resetTransform(index); + } else { + // 原始大小,放大到2倍 + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const centerX = rect.width / 2; + const centerY = rect.height / 2; + + transform.scale = 2; + // 以点击位置为中心放大 + transform.translateX = (centerX - x) * 0.5; + transform.translateY = (centerY - y) * 0.5; + } +} + +// 鼠标滚轮缩放 +function handleWheel(event: WheelEvent, index: number) { + event.preventDefault(); + const transform = getTransform(index); + + const delta = -event.deltaY; + const scaleChange = delta > 0 ? 1.1 : 0.9; + const newScale = transform.scale * scaleChange; + + // 限制缩放范围 + transform.scale = Math.max(1, Math.min(4, newScale)); + + // 如果缩放到1,重置平移 + if (transform.scale <= 1) { + transform.translateX = 0; + transform.translateY = 0; + } +} + +// 鼠标拖动(当放大时) +let isDragging = false; +let dragStartX = 0; +let dragStartY = 0; + +function handleMouseDown(event: MouseEvent, index: number) { + const transform = getTransform(index); + if (transform.scale > 1) { + isDragging = true; + dragStartX = event.clientX - transform.translateX; + dragStartY = event.clientY - transform.translateY; + event.preventDefault(); + } +} + +function handleMouseMove(event: MouseEvent, index: number) { + if (isDragging) { + const transform = getTransform(index); + transform.translateX = event.clientX - dragStartX; + transform.translateY = event.clientY - dragStartY; + event.preventDefault(); + } +} + +function handleMouseUp() { + isDragging = false; +} + +// 获取图片变换样式 +function getImageStyle(index: number): Record { + const transform = getTransform(index); + return { + transform: `translate(${transform.translateX}px, ${transform.translateY}px) scale(${transform.scale})`, + cursor: transform.scale > 1 ? 'grab' : 'zoom-in', + transition: isDragging || touchState.value.isDragging ? 'none' : 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', + }; +} + function onSlideChange(event: any) { // Swiper Web Components 事件格式:event.detail[0] 是 swiper 实例 const swiper = event.detail?.[0]; @@ -226,14 +433,6 @@ function goToSlide(index: number) { } } -function toggleZoom(index: number) { - if (zoomedSlides.value.has(index)) { - zoomedSlides.value.delete(index); - } else { - zoomedSlides.value.add(index); - } -} - function close() { emit("close"); } @@ -312,11 +511,17 @@ function handleKeydown(event: KeyboardEvent) { } } -// 监听当前索引变化,自动滚动缩略图 -watch(currentIndex, () => { +// 监听当前索引变化,自动滚动缩略图并重置上一个图片的缩放 +watch(currentIndex, (newIndex, oldIndex) => { nextTick(() => { scrollThumbnailIntoView(); }); + + // 重置上一个图片的变换状态(可选) + if (oldIndex !== undefined && oldIndex !== newIndex) { + // 可以选择重置或保留缩放状态 + // resetTransform(oldIndex); + } }); onMounted(() => { @@ -325,6 +530,14 @@ onMounted(() => { // 监听键盘事件 window.addEventListener("keydown", handleKeydown); + + // 添加全局鼠标事件监听,确保拖动平滑 + window.addEventListener("mousemove", (e) => { + if (isDragging) { + handleMouseMove(e as MouseEvent, currentIndex.value); + } + }); + window.addEventListener("mouseup", handleMouseUp); // 初始化时滚动到当前缩略图 nextTick(() => { @@ -338,6 +551,9 @@ onUnmounted(() => { // 移除键盘监听 window.removeEventListener("keydown", handleKeydown); + + // 移除全局鼠标监听 + window.removeEventListener("mouseup", handleMouseUp); }); @@ -443,13 +659,8 @@ swiper-slide { position: relative; max-width: 90%; max-height: 80%; - transition: transform 0.3s ease; - cursor: zoom-in; -} - -.photo-container.zoomed { - transform: scale(1.5); - cursor: zoom-out; + overflow: hidden; + touch-action: none; /* 禁用浏览器默认手势 */ } .viewer-image { @@ -457,6 +668,8 @@ swiper-slide { max-height: 80vh; object-fit: contain; user-select: none; + transform-origin: center center; + will-change: transform; /* 优化性能 */ } .zoom-hint { From 6bcd005ca7277f328df419471c88f7ca334c3195 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:39:51 +0000 Subject: [PATCH 3/4] Replace viewerjs with lightweight custom ImageViewer - Create custom ImageViewer component with optimized zoom/pan - Remove viewerjs dependency to reduce bundle size - Implement same touch/mouse gestures as PhotoViewer - Support pinch-to-zoom, double-click zoom, wheel zoom - Add smooth drag-to-pan when zoomed - Simpler and more performant than viewerjs library Co-authored-by: shellRaining <111564053+shellRaining@users.noreply.github.com> --- packages/theme/package.json | 3 +- .../theme/src/client/Viewer/ImageViewer.vue | 358 ++++++++++++++++++ packages/theme/src/client/Viewer/Viewer.vue | 70 +--- 3 files changed, 377 insertions(+), 54 deletions(-) create mode 100644 packages/theme/src/client/Viewer/ImageViewer.vue diff --git a/packages/theme/package.json b/packages/theme/package.json index cbf32b7..ee7f7ec 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -20,8 +20,7 @@ "sharp": "^0.34.4", "subset-font": "^2.4.0", "swiper": "^12.0.2", - "thumbhash": "^0.1.1", - "viewerjs": "^1.11.7" + "thumbhash": "^0.1.1" }, "peerDependencies": { "@vueuse/core": "^10.0.0", diff --git a/packages/theme/src/client/Viewer/ImageViewer.vue b/packages/theme/src/client/Viewer/ImageViewer.vue new file mode 100644 index 0000000..0c3771d --- /dev/null +++ b/packages/theme/src/client/Viewer/ImageViewer.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/packages/theme/src/client/Viewer/Viewer.vue b/packages/theme/src/client/Viewer/Viewer.vue index 3f85d8f..ccf625f 100644 --- a/packages/theme/src/client/Viewer/Viewer.vue +++ b/packages/theme/src/client/Viewer/Viewer.vue @@ -1,78 +1,44 @@ - + From 7e719eef27d7e4e5f7e396ead8921843d0449902 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:41:43 +0000 Subject: [PATCH 4/4] Address code review feedback for ImageViewer - Move drag state variables into setup function as refs - Save and restore original body overflow value - Update zoom hint text for clarity Co-authored-by: shellRaining <111564053+shellRaining@users.noreply.github.com> --- .../theme/src/client/Viewer/ImageViewer.vue | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/theme/src/client/Viewer/ImageViewer.vue b/packages/theme/src/client/Viewer/ImageViewer.vue index 0c3771d..65a678d 100644 --- a/packages/theme/src/client/Viewer/ImageViewer.vue +++ b/packages/theme/src/client/Viewer/ImageViewer.vue @@ -44,7 +44,7 @@
- 双击或滚轮缩放 · 拖动移动 · ESC 关闭 + 双击放大 · 滚轮缩放 · 拖动移动 · ESC 关闭
@@ -74,6 +74,13 @@ const translateX = ref(0); const translateY = ref(0); const isDragging = ref(false); +// 鼠标拖动状态 +const dragStartX = ref(0); +const dragStartY = ref(0); + +// 保存原始 body overflow 值 +const originalBodyOverflow = ref(""); + // 触摸状态 interface TouchState { startDistance: number; @@ -151,22 +158,19 @@ function handleWheel(event: WheelEvent) { } // 鼠标拖动 -let dragStartX = 0; -let dragStartY = 0; - function handleMouseDown(event: MouseEvent) { if (scale.value > 1) { isDragging.value = true; - dragStartX = event.clientX - translateX.value; - dragStartY = event.clientY - translateY.value; + dragStartX.value = event.clientX - translateX.value; + dragStartY.value = event.clientY - translateY.value; event.preventDefault(); } } function handleMouseMove(event: MouseEvent) { if (isDragging.value) { - translateX.value = event.clientX - dragStartX; - translateY.value = event.clientY - dragStartY; + translateX.value = event.clientX - dragStartX.value; + translateY.value = event.clientY - dragStartY.value; event.preventDefault(); } } @@ -244,9 +248,12 @@ watch( (newValue) => { if (newValue) { resetTransform(); + // 保存原始 overflow 值并锁定滚动 + originalBodyOverflow.value = document.body.style.overflow; document.body.style.overflow = "hidden"; } else { - document.body.style.overflow = ""; + // 恢复原始 overflow 值 + document.body.style.overflow = originalBodyOverflow.value; } } ); @@ -261,7 +268,8 @@ onUnmounted(() => { window.removeEventListener("keydown", handleKeydown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); - document.body.style.overflow = ""; + // 恢复原始 overflow 值 + document.body.style.overflow = originalBodyOverflow.value; });