From 1bd7393f91260173c4151612dcfdf07e477a6569 Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Thu, 14 Nov 2024 17:16:58 +0800 Subject: [PATCH 1/7] feat(imagePreview): Added image preview ability to rotate images --- .../vant/src/image-preview/ImagePreview.tsx | 40 ++++++++++++++ .../src/image-preview/ImagePreviewItem.tsx | 7 +-- packages/vant/src/image-preview/README.md | 4 ++ .../vant/src/image-preview/README.zh-CN.md | 4 ++ packages/vant/src/image-preview/index.less | 29 ++++++++++ .../vant/src/image-preview/test/index.spec.ts | 53 +++++++++++++++++++ packages/vant/src/image-preview/types.ts | 1 + 7 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/vant/src/image-preview/ImagePreview.tsx b/packages/vant/src/image-preview/ImagePreview.tsx index 91df7a15752..571661c920f 100644 --- a/packages/vant/src/image-preview/ImagePreview.tsx +++ b/packages/vant/src/image-preview/ImagePreview.tsx @@ -25,6 +25,7 @@ import { callInterceptor, createNamespace, HAPTICS_FEEDBACK, + makeNumberProp, } from '../utils'; // Composables @@ -78,6 +79,8 @@ export const imagePreviewProps = { closeOnClickOverlay: truthProp, closeIconPosition: makeStringProp('top-right'), teleport: [String, Object] as PropType, + rotate: Boolean, + rotationAngle: makeNumberProp(90), }; export type ImagePreviewProps = ExtractPropTypes; @@ -98,8 +101,17 @@ export default defineComponent({ rootWidth: 0, rootHeight: 0, disableZoom: false, + rotateAngles: [] as number[], }); + const handleRotate = (direction: 'left' | 'right') => { + if (!props.rotate) return; + const angle = props.rotationAngle * (direction === 'left' ? -1 : 1); + // 更新当前图片的旋转角度 + state.rotateAngles[state.active] = + (state.rotateAngles[state.active] || 0) + angle; + }; + const resize = () => { if (swipeRef.value) { const rect = useRect(swipeRef.value.$el); @@ -154,6 +166,31 @@ export default defineComponent({ state.disableZoom = false; }; + const renderRotateButtons = () => { + if (!props.rotate) return null; + + return ( +
+ + +
+ ); + }; + const renderImages = () => ( { if (index === state.active) { activedPreviewItemRef.value = item as ImagePreviewItemInstance; @@ -241,6 +279,7 @@ export default defineComponent({ (value) => { const { images, startPosition } = props; if (value) { + state.rotateAngles = images.map(() => 0); setActive(+startPosition); nextTick(() => { resize(); @@ -266,6 +305,7 @@ export default defineComponent({ {renderClose()} {renderImages()} {renderIndex()} + {renderRotateButtons()} {renderCover()} ); diff --git a/packages/vant/src/image-preview/ImagePreviewItem.tsx b/packages/vant/src/image-preview/ImagePreviewItem.tsx index 15ed76f3a20..f12f2d22519 100644 --- a/packages/vant/src/image-preview/ImagePreviewItem.tsx +++ b/packages/vant/src/image-preview/ImagePreviewItem.tsx @@ -17,6 +17,7 @@ import { makeRequiredProp, LONG_PRESS_START_TIME, type ComponentInstance, + makeNumberProp, } from '../utils'; // Composables @@ -57,6 +58,7 @@ const imagePreviewItemProps = { closeOnClickImage: Boolean, closeOnClickOverlay: Boolean, vertical: Boolean, + rotateAngle: makeNumberProp(90), }; export type ImagePreviewItemProps = ExtractPropTypes< @@ -93,9 +95,8 @@ export default defineComponent({ transitionDuration: zooming || moving || initializing ? '0s' : '.3s', }; - if (scale !== 1 || isLongImage.value) { - // use matrix to solve the problem of elements not rendering due to safari optimization - style.transform = `matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY})`; + if (scale !== 1 || isLongImage.value || props.rotateAngle !== 0) { + style.transform = `matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY}) rotate(${props.rotateAngle}deg)`; } return style; diff --git a/packages/vant/src/image-preview/README.md b/packages/vant/src/image-preview/README.md index a4e6fc1b7c6..ac002c11201 100644 --- a/packages/vant/src/image-preview/README.md +++ b/packages/vant/src/image-preview/README.md @@ -238,6 +238,8 @@ Vant exports following ImagePreview utility functions: | overlayClass | Custom overlay class | _string \| Array \| object_ | - | | overlayStyle | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | +| rotate | Whether to display the rotate picture button | _boolean_ | `false` | +| rotationAngle | The Angle of each rotation of the picture | _number_ | `90` | ### Props @@ -266,6 +268,8 @@ Vant exports following ImagePreview utility functions: | overlay-class | Custom overlay class | _string \| Array \| object_ | - | | overlay-style | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | +| rotate | Whether to display the rotate picture button | _boolean_ | `false` | +| rotation-angle | The Angle of each rotation of the picture | _number_ | `90` | ### Events diff --git a/packages/vant/src/image-preview/README.zh-CN.md b/packages/vant/src/image-preview/README.zh-CN.md index f06483f83f4..865d8e08435 100644 --- a/packages/vant/src/image-preview/README.zh-CN.md +++ b/packages/vant/src/image-preview/README.zh-CN.md @@ -241,6 +241,8 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlayClass | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlayStyle | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | +| rotate | 是否展示旋转图片按钮 | _boolean_ | `false` | +| rotationAngle | 每次旋转图片的角度 | _number_ | `90` | ### Props @@ -271,6 +273,8 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlay-class | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlay-style | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | +| rotate | 是否展示旋转图片按钮 | _boolean_ | `false` | +| rotation-angle | 每次旋转图片的角度 | _number_ | `90` | ### Events diff --git a/packages/vant/src/image-preview/index.less b/packages/vant/src/image-preview/index.less index 5ca9ca308c1..307453391d2 100644 --- a/packages/vant/src/image-preview/index.less +++ b/packages/vant/src/image-preview/index.less @@ -9,6 +9,9 @@ --van-image-preview-close-icon-color: var(--van-gray-5); --van-image-preview-close-icon-margin: var(--van-padding-md); --van-image-preview-close-icon-z-index: 1; + --van-image-preview-rotate-icon-size: 22px; + --van-image-preview-rotate-button-bottom: 40px; + --van-image-preview-rotate-button-gap: 40px; } .van-image-preview { @@ -111,4 +114,30 @@ bottom: var(--van-image-preview-close-icon-margin); } } + + &__rotate-buttons { + position: absolute; + bottom: var(--van-image-preview-rotate-button-bottom); + left: 50%; + transform: translateX(-50%); + z-index: 99; + display: flex; + gap: var(--van-image-preview-rotate-button-gap); + } + + &__rotate-button { + width: var(--van-image-preview-rotate-icon-size); + height: var(--van-image-preview-rotate-icon-size); + border-radius: 50%; + background: var(--van-image-preview-close-icon-color); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + &__rotate-icon--reverse { + transform: scaleX(-1); + } } diff --git a/packages/vant/src/image-preview/test/index.spec.ts b/packages/vant/src/image-preview/test/index.spec.ts index 94525b683a1..e2be8ba46d4 100644 --- a/packages/vant/src/image-preview/test/index.spec.ts +++ b/packages/vant/src/image-preview/test/index.spec.ts @@ -9,6 +9,7 @@ import { LONG_PRESS_START_TIME } from '../../utils'; import ImagePreview from '../ImagePreview'; import { images, triggerDoubleTap, triggerZoom } from './shared'; import type { ImagePreviewInstance } from '../types'; +import { nextTick } from 'vue'; test('should swipe to current index after calling the swipeTo method', async () => { const wrapper = mount(ImagePreview, { @@ -460,3 +461,55 @@ test('should reset scale after calling the resetScale method', async () => { await later(); expect(image.style.transform).toBeFalsy(); }); + +test('should render rotate buttons when rotate prop is true', async () => { + const wrapper = mount(ImagePreview, { + props: { + show: true, + images, + rotate: true, + }, + }); + + await later(); + + const rotateButtons = wrapper.findAll('.van-image-preview__rotate-button'); + expect(rotateButtons).toHaveLength(2); + expect( + wrapper.find('.van-image-preview__rotate-buttons').exists(), + ).toBeTruthy(); +}); + +test('should not render rotate buttons when rotate prop is false', () => { + const wrapper = mount(ImagePreview, { + props: { + show: true, + images, + rotate: false, + }, + }); + + expect( + wrapper.find('.van-image-preview__rotate-buttons').exists(), + ).toBeFalsy(); +}); + +test('should respect custom rotation angle', async () => { + const wrapper = mount(ImagePreview, { + props: { + show: true, + images, + rotate: true, + rotationAngle: 45, + }, + }); + + await later(); + const rotateButtons = wrapper.findAll('.van-image-preview__rotate-button'); + + await rotateButtons[1].trigger('click'); + await nextTick(); + expect( + wrapper.find('.van-image-preview__image').element.style.transform, + ).toContain('rotate(45deg)'); +}); diff --git a/packages/vant/src/image-preview/types.ts b/packages/vant/src/image-preview/types.ts index be2f72c521f..36caf1cb81d 100644 --- a/packages/vant/src/image-preview/types.ts +++ b/packages/vant/src/image-preview/types.ts @@ -44,6 +44,7 @@ export type ImagePreviewScaleEventParams = { type ImagePreviewItemExpose = { resetScale: () => void; + updateRotate: (angle: number) => void; }; export type ImagePreviewItemInstance = ComponentPublicInstance< From f834b9bb6c512f7ef254c408a303cc4622df5239 Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Thu, 14 Nov 2024 17:43:17 +0800 Subject: [PATCH 2/7] feat(imagePreview): Added image preview ability to rotate images --- packages/vant/src/image-preview/ImagePreview.tsx | 2 +- .../vant/src/image-preview/ImagePreviewItem.tsx | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/vant/src/image-preview/ImagePreview.tsx b/packages/vant/src/image-preview/ImagePreview.tsx index 571661c920f..505e91b2df9 100644 --- a/packages/vant/src/image-preview/ImagePreview.tsx +++ b/packages/vant/src/image-preview/ImagePreview.tsx @@ -79,7 +79,7 @@ export const imagePreviewProps = { closeOnClickOverlay: truthProp, closeIconPosition: makeStringProp('top-right'), teleport: [String, Object] as PropType, - rotate: Boolean, + rotate: truthProp, rotationAngle: makeNumberProp(90), }; diff --git a/packages/vant/src/image-preview/ImagePreviewItem.tsx b/packages/vant/src/image-preview/ImagePreviewItem.tsx index f12f2d22519..ac68ffbc3fd 100644 --- a/packages/vant/src/image-preview/ImagePreviewItem.tsx +++ b/packages/vant/src/image-preview/ImagePreviewItem.tsx @@ -58,7 +58,7 @@ const imagePreviewItemProps = { closeOnClickImage: Boolean, closeOnClickOverlay: Boolean, vertical: Boolean, - rotateAngle: makeNumberProp(90), + rotateAngle: makeNumberProp(0), }; export type ImagePreviewItemProps = ExtractPropTypes< @@ -94,11 +94,16 @@ export default defineComponent({ const style: CSSProperties = { transitionDuration: zooming || moving || initializing ? '0s' : '.3s', }; - - if (scale !== 1 || isLongImage.value || props.rotateAngle !== 0) { - style.transform = `matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY}) rotate(${props.rotateAngle}deg)`; + const transforms: string[] = []; + if (scale !== 1 || isLongImage.value) { + transforms.push(`matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY})`); + } + if (props.rotateAngle !== 0) { + transforms.push(`rotate(${props.rotateAngle}deg)`); + } + if (transforms.length) { + style.transform = transforms.join(' '); } - return style; }); From b8c3d572a9492ab737e9a9603c60c51f420ebf97 Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Thu, 14 Nov 2024 17:49:24 +0800 Subject: [PATCH 3/7] feat(imagePreview): Added image preview ability to rotate images --- packages/vant/src/image-preview/ImagePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vant/src/image-preview/ImagePreview.tsx b/packages/vant/src/image-preview/ImagePreview.tsx index 505e91b2df9..571661c920f 100644 --- a/packages/vant/src/image-preview/ImagePreview.tsx +++ b/packages/vant/src/image-preview/ImagePreview.tsx @@ -79,7 +79,7 @@ export const imagePreviewProps = { closeOnClickOverlay: truthProp, closeIconPosition: makeStringProp('top-right'), teleport: [String, Object] as PropType, - rotate: truthProp, + rotate: Boolean, rotationAngle: makeNumberProp(90), }; From 502dbde03fa4b069ec477be36dc74afbb77ac17b Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Thu, 14 Nov 2024 17:52:53 +0800 Subject: [PATCH 4/7] feat(imagePreview): Added image preview ability to rotate images --- packages/vant/src/image-preview/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vant/src/image-preview/types.ts b/packages/vant/src/image-preview/types.ts index 36caf1cb81d..be2f72c521f 100644 --- a/packages/vant/src/image-preview/types.ts +++ b/packages/vant/src/image-preview/types.ts @@ -44,7 +44,6 @@ export type ImagePreviewScaleEventParams = { type ImagePreviewItemExpose = { resetScale: () => void; - updateRotate: (angle: number) => void; }; export type ImagePreviewItemInstance = ComponentPublicInstance< From 58df44a8133839a73ace46733561ba8f1c644ea5 Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Mon, 18 Nov 2024 14:20:31 +0800 Subject: [PATCH 5/7] feat(imagePreview): Optimization Recommendation Adjustments --- .../vant/src/image-preview/ImagePreview.tsx | 9 ++-- .../src/image-preview/ImagePreviewItem.tsx | 52 +++++++++++++++++-- packages/vant/src/image-preview/README.md | 6 +-- .../vant/src/image-preview/README.zh-CN.md | 6 +-- .../vant/src/image-preview/test/index.spec.ts | 29 +++++++---- 5 files changed, 75 insertions(+), 27 deletions(-) diff --git a/packages/vant/src/image-preview/ImagePreview.tsx b/packages/vant/src/image-preview/ImagePreview.tsx index 571661c920f..663f570bb2d 100644 --- a/packages/vant/src/image-preview/ImagePreview.tsx +++ b/packages/vant/src/image-preview/ImagePreview.tsx @@ -79,8 +79,7 @@ export const imagePreviewProps = { closeOnClickOverlay: truthProp, closeIconPosition: makeStringProp('top-right'), teleport: [String, Object] as PropType, - rotate: Boolean, - rotationAngle: makeNumberProp(90), + rotate: makeNumberProp(0), }; export type ImagePreviewProps = ExtractPropTypes; @@ -105,8 +104,8 @@ export default defineComponent({ }); const handleRotate = (direction: 'left' | 'right') => { - if (!props.rotate) return; - const angle = props.rotationAngle * (direction === 'left' ? -1 : 1); + if (props.rotate === 0) return; + const angle = props.rotate * (direction === 'left' ? -1 : 1); // 更新当前图片的旋转角度 state.rotateAngles[state.active] = (state.rotateAngles[state.active] || 0) + angle; @@ -167,7 +166,7 @@ export default defineComponent({ }; const renderRotateButtons = () => { - if (!props.rotate) return null; + if (props.rotate === 0) return null; return (
diff --git a/packages/vant/src/image-preview/ImagePreviewItem.tsx b/packages/vant/src/image-preview/ImagePreviewItem.tsx index ac68ffbc3fd..0b6747d1926 100644 --- a/packages/vant/src/image-preview/ImagePreviewItem.tsx +++ b/packages/vant/src/image-preview/ImagePreviewItem.tsx @@ -89,21 +89,60 @@ export default defineComponent({ let initialMoveY = 0; + const getRotatedDimensions = ( + width: number, + height: number, + angle: number, + ) => { + const radians = (Math.abs(angle) * Math.PI) / 180; + const sin = Math.sin(radians); + const cos = Math.cos(radians); + const rotatedWidth = Math.abs(width * cos) + Math.abs(height * sin); + const rotatedHeight = Math.abs(width * sin) + Math.abs(height * cos); + return { width: rotatedWidth, height: rotatedHeight }; + }; + + const getContainScale = computed(() => { + if (!state.imageRatio) return 1; + const { rootWidth, rootHeight, rotateAngle } = props; + const naturalWidth = rootWidth; + const naturalHeight = naturalWidth * state.imageRatio; + const rotated = getRotatedDimensions( + naturalWidth, + naturalHeight, + rotateAngle, + ); + const scaleX = rootWidth / rotated.width; + const scaleY = rootHeight / rotated.height; + return Math.min(scaleX, scaleY); + }); + const imageStyle = computed(() => { const { scale, moveX, moveY, moving, zooming, initializing } = state; const style: CSSProperties = { transitionDuration: zooming || moving || initializing ? '0s' : '.3s', }; + const transforms: string[] = []; - if (scale !== 1 || isLongImage.value) { - transforms.push(`matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY})`); + const actualScale = scale * getContainScale.value; + + if (actualScale !== 1 || isLongImage.value) { + transforms.push( + `matrix(${actualScale}, 0, 0, ${actualScale}, ${moveX}, ${moveY})`, + ); } + if (props.rotateAngle !== 0) { - transforms.push(`rotate(${props.rotateAngle}deg)`); + const angle = (props.rotateAngle * Math.PI) / 180; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + transforms.push(`matrix(${cos}, ${sin}, ${-sin}, ${cos}, 0, 0)`); } + if (transforms.length) { style.transform = transforms.join(' '); } + return style; }); @@ -134,7 +173,12 @@ export default defineComponent({ }); const setScale = (scale: number, center?: { x: number; y: number }) => { - scale = clamp(scale, +props.minZoom, +props.maxZoom + 1); + const baseScale = getContainScale.value; + scale = clamp( + scale, + +props.minZoom * baseScale, + +props.maxZoom * baseScale, + ); if (scale !== state.scale) { const ratio = scale / state.scale; diff --git a/packages/vant/src/image-preview/README.md b/packages/vant/src/image-preview/README.md index ac002c11201..e78b7343430 100644 --- a/packages/vant/src/image-preview/README.md +++ b/packages/vant/src/image-preview/README.md @@ -238,8 +238,7 @@ Vant exports following ImagePreview utility functions: | overlayClass | Custom overlay class | _string \| Array \| object_ | - | | overlayStyle | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | -| rotate | Whether to display the rotate picture button | _boolean_ | `false` | -| rotationAngle | The Angle of each rotation of the picture | _number_ | `90` | +| rotate | When the value is not 0, the picture rotation function is enabled, and the rotation angle is the size of the value | _number_ | `0` | ### Props @@ -268,8 +267,7 @@ Vant exports following ImagePreview utility functions: | overlay-class | Custom overlay class | _string \| Array \| object_ | - | | overlay-style | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | -| rotate | Whether to display the rotate picture button | _boolean_ | `false` | -| rotation-angle | The Angle of each rotation of the picture | _number_ | `90` | +| rotate | When the value is not 0, the picture rotation function is enabled, and the rotation angle is the size of the value | _number_ | `0` | ### Events diff --git a/packages/vant/src/image-preview/README.zh-CN.md b/packages/vant/src/image-preview/README.zh-CN.md index 865d8e08435..03132843547 100644 --- a/packages/vant/src/image-preview/README.zh-CN.md +++ b/packages/vant/src/image-preview/README.zh-CN.md @@ -241,8 +241,7 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlayClass | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlayStyle | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | -| rotate | 是否展示旋转图片按钮 | _boolean_ | `false` | -| rotationAngle | 每次旋转图片的角度 | _number_ | `90` | +| rotate | 值非0时,开启图片旋转功能,旋转角度为值的大小 | _number_ | `0` | ### Props @@ -273,8 +272,7 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlay-class | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlay-style | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | -| rotate | 是否展示旋转图片按钮 | _boolean_ | `false` | -| rotation-angle | 每次旋转图片的角度 | _number_ | `90` | +| rotate | 值非0时,开启图片旋转功能,旋转角度为值的大小 | _number_ | `0` | ### Events diff --git a/packages/vant/src/image-preview/test/index.spec.ts b/packages/vant/src/image-preview/test/index.spec.ts index e2be8ba46d4..71d39bfd46c 100644 --- a/packages/vant/src/image-preview/test/index.spec.ts +++ b/packages/vant/src/image-preview/test/index.spec.ts @@ -462,12 +462,12 @@ test('should reset scale after calling the resetScale method', async () => { expect(image.style.transform).toBeFalsy(); }); -test('should render rotate buttons when rotate prop is true', async () => { +test('should render rotate buttons when rotate prop is number', async () => { const wrapper = mount(ImagePreview, { props: { show: true, images, - rotate: true, + rotate: 90, }, }); @@ -480,12 +480,11 @@ test('should render rotate buttons when rotate prop is true', async () => { ).toBeTruthy(); }); -test('should not render rotate buttons when rotate prop is false', () => { +test('should not render rotate buttons when rotate prop is null', () => { const wrapper = mount(ImagePreview, { props: { show: true, images, - rotate: false, }, }); @@ -499,17 +498,27 @@ test('should respect custom rotation angle', async () => { props: { show: true, images, - rotate: true, - rotationAngle: 45, + rotate: 45, }, }); - await later(); const rotateButtons = wrapper.findAll('.van-image-preview__rotate-button'); await rotateButtons[1].trigger('click'); await nextTick(); - expect( - wrapper.find('.van-image-preview__image').element.style.transform, - ).toContain('rotate(45deg)'); + + const transformStyle = wrapper.find('.van-image-preview__image').element.style + .transform; + const matches = transformStyle.match( + /matrix\(([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+)\)/, + ); + const actualMatrix = matches + ? matches + .slice(1, 7) + .map((n: string) => Number(n).toFixed(6)) + .join(', ') + : ''; + expect(actualMatrix).toBe( + '0.707107, 0.707107, -0.707107, 0.707107, 0.000000, 0.000000', + ); }); From 2edd5a022155bfd2004c8125283906b954f75115 Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Tue, 26 Nov 2024 21:40:33 +0800 Subject: [PATCH 6/7] feat(imagePreview): Added image preview ability to rotate images --- .../vant/src/image-preview/ImagePreview.tsx | 9 +++---- packages/vant/src/image-preview/README.md | 4 +-- .../vant/src/image-preview/README.zh-CN.md | 4 +-- .../vant/src/image-preview/test/index.spec.ts | 27 ++++++++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/vant/src/image-preview/ImagePreview.tsx b/packages/vant/src/image-preview/ImagePreview.tsx index 663f570bb2d..337d42c3be6 100644 --- a/packages/vant/src/image-preview/ImagePreview.tsx +++ b/packages/vant/src/image-preview/ImagePreview.tsx @@ -25,7 +25,6 @@ import { callInterceptor, createNamespace, HAPTICS_FEEDBACK, - makeNumberProp, } from '../utils'; // Composables @@ -79,7 +78,7 @@ export const imagePreviewProps = { closeOnClickOverlay: truthProp, closeIconPosition: makeStringProp('top-right'), teleport: [String, Object] as PropType, - rotate: makeNumberProp(0), + rotate: Boolean, }; export type ImagePreviewProps = ExtractPropTypes; @@ -104,8 +103,8 @@ export default defineComponent({ }); const handleRotate = (direction: 'left' | 'right') => { - if (props.rotate === 0) return; - const angle = props.rotate * (direction === 'left' ? -1 : 1); + if (!props.rotate) return; + const angle = 90 * (direction === 'left' ? -1 : 1); // 更新当前图片的旋转角度 state.rotateAngles[state.active] = (state.rotateAngles[state.active] || 0) + angle; @@ -166,7 +165,7 @@ export default defineComponent({ }; const renderRotateButtons = () => { - if (props.rotate === 0) return null; + if (!props.rotate) return null; return (
diff --git a/packages/vant/src/image-preview/README.md b/packages/vant/src/image-preview/README.md index e78b7343430..905e0e03b23 100644 --- a/packages/vant/src/image-preview/README.md +++ b/packages/vant/src/image-preview/README.md @@ -238,7 +238,7 @@ Vant exports following ImagePreview utility functions: | overlayClass | Custom overlay class | _string \| Array \| object_ | - | | overlayStyle | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | -| rotate | When the value is not 0, the picture rotation function is enabled, and the rotation angle is the size of the value | _number_ | `0` | +| rotate | Whether to enable the image rotation function | _boolean_ | `false` | ### Props @@ -267,7 +267,7 @@ Vant exports following ImagePreview utility functions: | overlay-class | Custom overlay class | _string \| Array \| object_ | - | | overlay-style | Custom overlay style | _object_ | - | | teleport | Specifies a target element where ImagePreview will be mounted | _string \| Element_ | - | -| rotate | When the value is not 0, the picture rotation function is enabled, and the rotation angle is the size of the value | _number_ | `0` | +| rotate | Whether to enable the image rotation function | _boolean_ | `false` | ### Events diff --git a/packages/vant/src/image-preview/README.zh-CN.md b/packages/vant/src/image-preview/README.zh-CN.md index 03132843547..5e72b234d05 100644 --- a/packages/vant/src/image-preview/README.zh-CN.md +++ b/packages/vant/src/image-preview/README.zh-CN.md @@ -241,7 +241,7 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlayClass | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlayStyle | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | -| rotate | 值非0时,开启图片旋转功能,旋转角度为值的大小 | _number_ | `0` | +| rotate | 是否开启图片旋转功能 | _boolean_ | `false` | ### Props @@ -272,7 +272,7 @@ Vant 中导出了以下 ImagePreview 相关的辅助函数: | overlay-class | 自定义遮罩层类名 | _string \| Array \| object_ | - | | overlay-style | 自定义遮罩层样式 | _object_ | - | | teleport | 指定挂载的节点,等同于 Teleport 组件的 [to 属性](https://cn.vuejs.org/api/built-in-components.html#teleport) | _string \| Element_ | - | -| rotate | 值非0时,开启图片旋转功能,旋转角度为值的大小 | _number_ | `0` | +| rotate | 是否开启图片旋转功能 | _boolean_ | `false` | ### Events diff --git a/packages/vant/src/image-preview/test/index.spec.ts b/packages/vant/src/image-preview/test/index.spec.ts index 71d39bfd46c..1b0959fe1c0 100644 --- a/packages/vant/src/image-preview/test/index.spec.ts +++ b/packages/vant/src/image-preview/test/index.spec.ts @@ -498,7 +498,7 @@ test('should respect custom rotation angle', async () => { props: { show: true, images, - rotate: 45, + rotate: true, }, }); await later(); @@ -507,18 +507,25 @@ test('should respect custom rotation angle', async () => { await rotateButtons[1].trigger('click'); await nextTick(); - const transformStyle = wrapper.find('.van-image-preview__image').element.style - .transform; + const image = wrapper.find('.van-image-preview__image'); + const transformStyle = image.element.style.transform; + const matches = transformStyle.match( - /matrix\(([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+)\)/, + /matrix\(([-\d.e+-]+), ([-\d.e+-]+), ([-\d.e+-]+), ([-\d.e+-]+), ([-\d.e+-]+), ([-\d.e+-]+)\)/, ); + if (!matches) { + console.log('No matches found for transform style'); + return; + } const actualMatrix = matches - ? matches - .slice(1, 7) - .map((n: string) => Number(n).toFixed(6)) - .join(', ') - : ''; + .slice(1, 7) + .map((n: string) => { + const num = Number(n); + return Math.abs(num) < 1e-10 ? '0.000000' : num.toFixed(6); + }) + .join(', '); + expect(actualMatrix).toBe( - '0.707107, 0.707107, -0.707107, 0.707107, 0.000000, 0.000000', + '0.000000, 1.000000, -1.000000, 0.000000, 0.000000, 0.000000', ); }); From 648dbac99e52d8a65d78b6a22f35d212f71d2dde Mon Sep 17 00:00:00 2001 From: lizhen <1154651716@qq.com> Date: Mon, 2 Dec 2024 10:42:37 +0800 Subject: [PATCH 7/7] feat(imagePreview): Added image preview ability to rotate images --- .../src/image-preview/ImagePreviewItem.tsx | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/vant/src/image-preview/ImagePreviewItem.tsx b/packages/vant/src/image-preview/ImagePreviewItem.tsx index 0b6747d1926..849a263fd11 100644 --- a/packages/vant/src/image-preview/ImagePreviewItem.tsx +++ b/packages/vant/src/image-preview/ImagePreviewItem.tsx @@ -116,6 +116,29 @@ export default defineComponent({ const scaleY = rootHeight / rotated.height; return Math.min(scaleX, scaleY); }); + watch( + () => props.rotateAngle, + () => { + adjustPositionAfterRotate(); + }, + ); + + const adjustPositionAfterRotate = () => { + if (state.scale === 1) { + state.moveX = 0; + state.moveY = isLongImage.value ? initialMoveY : 0; + return; + } + state.moveX = clamp(state.moveX, -maxMoveX.value, maxMoveX.value); + state.moveY = clamp(state.moveY, -maxMoveY.value, maxMoveY.value); + if ( + Math.abs(state.moveX) > maxMoveX.value || + Math.abs(state.moveY) > maxMoveY.value + ) { + state.moveX = 0; + state.moveY = 0; + } + }; const imageStyle = computed(() => { const { scale, moveX, moveY, moving, zooming, initializing } = state; @@ -146,39 +169,44 @@ export default defineComponent({ return style; }); + // 添加旋转后的尺寸计算 + const getRotatedImageSize = computed(() => { + const { rootWidth } = props; + const naturalWidth = rootWidth; + const naturalHeight = naturalWidth * state.imageRatio; + const { width: rotatedWidth, height: rotatedHeight } = + getRotatedDimensions(naturalWidth, naturalHeight, props.rotateAngle); + return { + width: rotatedWidth, + height: rotatedHeight, + }; + }); + const maxMoveX = computed(() => { if (state.imageRatio) { - const { rootWidth, rootHeight } = props; - const displayWidth = vertical.value - ? rootHeight / state.imageRatio - : rootWidth; + const { rootWidth } = props; + const { width: rotatedWidth } = getRotatedImageSize.value; + const scaledWidth = rotatedWidth * state.scale * getContainScale.value; - return Math.max(0, (state.scale * displayWidth - rootWidth) / 2); + return Math.max(0, (scaledWidth - rootWidth) / 2); } - return 0; }); const maxMoveY = computed(() => { if (state.imageRatio) { - const { rootWidth, rootHeight } = props; - const displayHeight = vertical.value - ? rootHeight - : rootWidth * state.imageRatio; + const { rootHeight } = props; + const { height: rotatedHeight } = getRotatedImageSize.value; + const scaledHeight = + rotatedHeight * state.scale * getContainScale.value; - return Math.max(0, (state.scale * displayHeight - rootHeight) / 2); + return Math.max(0, (scaledHeight - rootHeight) / 2); } - return 0; }); const setScale = (scale: number, center?: { x: number; y: number }) => { - const baseScale = getContainScale.value; - scale = clamp( - scale, - +props.minZoom * baseScale, - +props.maxZoom * baseScale, - ); + scale = clamp(scale, +props.minZoom, +props.maxZoom + 1); if (scale !== state.scale) { const ratio = scale / state.scale;