Przeglądaj źródła

fix(image): Fixed issue 2266 (#2266) (#2293)

* fix(image): Fixed issue 2266 (#2266)

* wip: refactor image preveiw component

* wip: zoom image with fixed mouse focus position

* fix: fixed zoom image with fixed mouse focus position on other rotation

* test(image): change the test: mouse drag to verify translate

* refactor(preview-image-foundation): Increased readability of the initialize image part of the logic

* chore(package.json): remove packageManager

* fix: fixed the `translate` CSS property can not be used in low version browser

* chore(image): remove useless code

* refactor(image): use calcBoundingRectSize() instead of document.getBoundingClientRect()

* fix: remove useless packagemanager & simplify logic

---------

Co-authored-by: zhangyumei.0319 <[email protected]>
Elvis Liao 1 rok temu
rodzic
commit
2a5ead2558

+ 9 - 35
cypress/e2e/image.spec.js

@@ -88,44 +88,18 @@ describe('image', () => {
         cy.get('.semi-image-preview-footer').children('.semi-icon-plus').click();
         cy.get('.semi-image-preview-footer').children('.semi-icon-plus').click();
         cy.wait(200);
-        // 再经过两次点击放大后,预期 zoom = 1.2, 宽1728px, 高960px, top-80px, left-144px
+        // 再经过两次点击放大后,预期 zoom = 1.2, 宽1728px, 高960px, translate.x = 0 translate.y = 0
         cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1728px');
         cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '960px'); 
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'top').and('eq', '-80px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'left').and('eq', '-144px');
-
-        // // 测试拖拽,拖拽长度为分别向右,向下拖拽20px
-        // cy.get('.semi-image-preview-image-img').trigger('mousedown', { clientX: 720, clientY: 400 });
-        // cy.wait(200);
-        // cy.get('.semi-image-preview-image-img').trigger('mousemove', { clientX: 740, clientY: 420 }).trigger('mouseup');
-        // // 预期经过拖拽后,宽高保持不变(宽1728px, 高960px), top和left定位增加20px(top-60px, left-124px)
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1728px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '960px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'top').and('eq', '-60px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'left').and('eq', '-124px');
-        // // 测试拖拽边界: 内部逻辑是拖拽过程中,拖拽极限是图片边和容器边重合
-        // // 测试往右下方拖动极限
-        // cy.get('.semi-image-preview-image-img').trigger('mousedown', { clientX: 720, clientY: 400 });
-        // cy.wait(200);
-        // cy.get('.semi-image-preview-image-img').trigger('mousemove', { clientX: 844, clientY: 460 }).trigger('mouseup');
-        // // 鼠标移动距离为向右60px,向下130px,则此刻的top,left应该都是0
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'top').and('eq', '0px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'left').and('eq', '0px');
-        // // 当上下都在图片边缘时候,再次向右下方拖动,因为已经到拖动极限,此时无法再次拖动,top,left值不会再变化
-        // cy.get('.semi-image-preview-image-img').trigger('mousedown', { clientX: 720, clientY: 400 });
-        // cy.wait(200);
-        // cy.get('.semi-image-preview-image-img').trigger('mousemove', { clientX: 730, clientY: 430 }).trigger('mouseup');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'top').and('eq', '0px');
-        // cy.get('.semi-image-preview-image-img').should('have.css', 'left').and('eq', '0px');
-
-        // zoom = 1.2, 宽1728px, 高960px, top-80px, left-144px, 鼠标移动 x * y = 200 * 100, 
-        // 图片拖动策略是图片只能够拖动到图片边缘和容器边缘重合,因此预期top和left都为 0px
+        
+        // zoom = 1.2, 宽1728px, 高960px, translate.x = 0 translate.y = 0, 鼠标移动 x * y = 200 * 100, 
+        // (1728 - 1440) / 2 = 144 | (960 - 800) / 2 = 80
+        // 图片拖动策略是图片只能够拖动到图片边缘和容器边缘重合,因此预期 translate.y 为 144,translate.x 为 80
         cy.get('.semi-image-preview-image-img').trigger('mousedown', { clientX: 0, clientY: 0 });
         cy.wait(200);
         cy.get('.semi-image-preview-image-img').trigger('mousemove', { clientX: 200, clientY: 100, buttons: 1 });
         cy.wait(200);
-        cy.get('.semi-image-preview-image-img').should('have.css', 'top').and('eq', '0px');
-        cy.get('.semi-image-preview-image-img').should('have.css', 'left').and('eq', '0px');
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'translate(144px, 80px)');
     });
 
     // 测试鼠标滚动滚轮放大,缩小图片
@@ -146,7 +120,7 @@ describe('image', () => {
         cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '800px');
         // 验证滚轮向下滚动缩小图片
         cy.get('.semi-image-preview-image-img').trigger('mouseover', { clientX: 720, clientY: 400 });
-        // 触发 wheel 事件, 参数 deltaY 表示纵向滚动量, 验证放大
+        // 触发 wheel 事件, 参数 deltaY 表示纵向滚动量, 验证缩小
         cy.get('.semi-image-preview-image-img').trigger('wheel', { deltaY: 10, bubbles: true });
         // 单次滚动向下滚动滚轮,zoom = zoom - zoomStep = 0.9,  width = '1296px',height = '720px'
         cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1296px');
@@ -369,9 +343,9 @@ describe('image', () => {
         // 测试点击向右旋转,向左旋转按键
         cy.get('.semi-icon-rotate').eq(0).click();
         cy.wait(500);
-        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'transform: rotate(90deg)');
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'rotate(90deg)');
         cy.get('.semi-icon-rotate').eq(1).click();
-        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'transform: rotate(0deg)');
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'rotate(0deg)');
 
         // 测试下载按键
         cy.get('.semi-icon-download').click();

+ 1 - 1
package.json

@@ -241,4 +241,4 @@
         ]
     },
     "license": "MIT"
-}
+}

+ 6 - 1
packages/semi-foundation/image/image.scss

@@ -100,6 +100,7 @@ $module: #{$prefix}-image;
         align-items: center;
         padding: $spacing-image_preview_header-paddingY $spacing-image_preview_header-paddingX;
         z-index: $z-image_preview_header;
+        pointer-events: none;
 
         &-title {
             flex: 1;
@@ -113,6 +114,7 @@ $module: #{$prefix}-image;
             width: $width-image_preview_header_close;
             height: $height-image_preview_header_close;
             border-radius: 50%;
+            pointer-events: auto;
 
             &:hover {
                 background-color: $color-image_header_close-bg;
@@ -192,11 +194,14 @@ $module: #{$prefix}-image;
     &-image {
         position: relative;
         height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
 
         &-img {
             position: absolute;
             transform: scale3d($transform_scale3d-image_preview_image_img) $transform_rotate-image_preview_image_img;
-            transition: transform $transition_duration-image_preview_image_img  $transition_delay-image_preview_image_img;
+            // transition: transform $transition_duration-image_preview_image_img  $transition_delay-image_preview_image_img;
             z-index: 0;
             user-select: none;
         }

+ 233 - 150
packages/semi-foundation/image/previewImageFoundation.ts

@@ -12,60 +12,86 @@ export interface DragDirection {
     canDragHorizontal: boolean
 }
 
-export interface ExtremeBounds {
-    left: number;
-    top: number
+export interface ExtremeTranslate {
+    x: number;
+    y: number
+}
+
+export interface Offset {
+    x: number;
+    y: number
 }
 
-export interface ImageOffset {
+export interface Translate {
     x: number;
     y: number
 }
 
-const DefaultDOMRect = {
-    bottom: 0,
-    height: 0,
-    left: 0,
-    right: 0,
-    top: 0,
-    width: 0,
-    x: 0,
-    y: 0,
-    toJSON: () => ({})
-};
+interface CalcBoundingRectMouseOffset {
+    offset: Offset;
+    width: number;
+    height: number;
+    rotation?: number
+}
+
+export interface BoundingRectSize {
+    width: number;
+    height: number
+}
+
 export default class PreviewImageFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<PreviewImageAdapter<P, S>, P, S> {
     constructor(adapter: PreviewImageAdapter<P, S>) {
         super({ ...adapter });
     }
 
-    startMouseOffset = { x: 0, y: 0 };
+    startMouseClientPosition = { x: 0, y: 0 };
     originImageWidth = null;
     originImageHeight = null;
 
+    containerWidth = 0; 
+    containerHeight = 0;
+
+    init() {
+        this._getContainerBoundingRectSize();
+    }
+
     _isImageVertical = (): boolean => this.getProp("rotation") % 180 !== 0;
 
-    _getImageBounds = (): DOMRect => {
-        const imageDOM = this._adapter.getImage();
-        if (imageDOM) {
-            return imageDOM.getBoundingClientRect();
+    _getContainerBoundingRectSize = () => {
+        const containerDOM = this._adapter.getContainer();
+        if (containerDOM) {
+            this.containerWidth = containerDOM.clientWidth;
+            this.containerHeight = containerDOM.clientHeight;
         }
-        return DefaultDOMRect;
-    };
+    }
 
-    _getContainerBounds = (): DOMRect => {
+    _getAdaptationZoom = () => {
+        let _zoom = 1;
         const containerDOM = this._adapter.getContainer();
-        if (containerDOM) {
-            return containerDOM.getBoundingClientRect();
+        
+        if (containerDOM && this.originImageWidth && this.originImageHeight) {
+            const { rotation } = this.getProps();
+            const { width: imageWidth, height: imageHeight } = this.calcBoundingRectSize(this.originImageWidth, this.originImageHeight, rotation);
+            const reservedWidth = this.containerWidth - 80;
+            const reservedHeight = this.containerHeight - 80;
+            
+            _zoom = Number(
+                Math.min(reservedWidth / imageWidth, reservedHeight / imageHeight).toFixed(2)
+            );
         }
-        return DefaultDOMRect;
+
+        return _zoom;
     }
 
-    _getOffset = (e: any): ImageOffset => {
-        const { left, top } = this._getImageBounds();
-        return {
-            x: e.clientX - left,
-            y: e.clientY - top,
-        };
+    _getInitialZoom = () => {
+        const { ratio } = this.getProps();
+        let _zoom = 1;
+
+        if (ratio === 'adaptation') {
+            _zoom = this._getAdaptationZoom();
+        }
+
+        return _zoom;
     }
 
     setLoading = (loading: boolean) => {
@@ -73,9 +99,8 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
     }
 
     handleWindowResize = (): void => {
-        if (this.originImageWidth && this.originImageHeight) {
-            this.handleResizeImage();
-        }
+        this._getContainerBoundingRectSize();
+        this.initializeImage();
     };
 
     handleLoad = (e: any): void => {
@@ -88,7 +113,7 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
             } as any);
             // 图片初次加载,计算 zoom,zoom 改变不需要通过回调透出
             // When the image is loaded for the first time, zoom is calculated, and zoom changes do not need to be exposed through callbacks.
-            this.handleResizeImage(false);
+            this.initializeImage(false);
         }
         const { src, onLoad } = this.getProps();
         onLoad && onLoad(src);
@@ -102,53 +127,35 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
         onError && onError(src);
     }
 
-    handleResizeImage = (notify: boolean = true) => {
-        const horizontal = !this._isImageVertical();
+    handleRatioChange = () => {
+        this.initializeImage();
+    }
+
+    initializeImageZoom = (notify = true) => {
         const { currZoom } = this.getStates();
-        const imgWidth = horizontal ? this.originImageWidth : this.originImageHeight;
-        const imgHeight = horizontal ? this.originImageHeight : this.originImageWidth;
-        const { onZoom, setRatio, ratio } = this.getProps();
-        const containerDOM = this._adapter.getContainer();
-        if (containerDOM) {
-            const { width: containerWidth, height: containerHeight } = this._getContainerBounds();
-            const reservedWidth = containerWidth - 80;
-            const reservedHeight = containerHeight - 80;
-            let _zoom = 1;
-            if (imgWidth > reservedWidth || imgHeight > reservedHeight) {
-                _zoom = Number(
-                    Math.min(reservedWidth / imgWidth, reservedHeight / imgHeight).toFixed(2)
-                );
-            }
-            if (currZoom === _zoom) {
-                this.calculatePreviewImage(_zoom, null);
-            } else {
-                onZoom(_zoom, notify);
-            }
+        const { onZoom } = this.getProps();
+        
+        const _zoom = this._getInitialZoom();
+        
+        if (currZoom !== _zoom) {
+            onZoom(_zoom, notify);
+        } else {
+            this.changeZoom(_zoom);
         }
     }
 
-    handleRatioChange = () => {
-        if (this.originImageWidth && this.originImageHeight) {
-            const { currZoom } = this.getStates();
-            const { ratio, onZoom } = this.getProps();
-            let _zoom: number;
-            if (ratio === 'adaptation') {
-                const horizontal = !this._isImageVertical();
-                const imgWidth = horizontal ? this.originImageWidth : this.originImageHeight;
-                const imgHeight = horizontal ? this.originImageHeight : this.originImageWidth;
-                const { width: containerWidth, height: containerHeight } = this._getContainerBounds();
-                const reservedWidth = containerWidth - 80;
-                const reservedHeight = containerHeight - 80;
-                _zoom = Number(
-                    Math.min(reservedWidth / imgWidth, reservedHeight / imgHeight).toFixed(2)
-                );
-            } else {
-                _zoom = 1;
-            }
-            if (currZoom !== _zoom) {
-                onZoom(_zoom);
+    initializeTranslate = () => {
+        this.setState({
+            translate: {
+                x: 0,
+                y: 0
             }
-        }
+        } as any);
+    }
+
+    initializeImage = (notify = true) => {
+        this.initializeImageZoom(notify);
+        this.initializeTranslate();
     }
 
     handleRightClickImage = (e: any) => {
@@ -162,111 +169,187 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
         }
     };
 
-    calcCanDragDirection = (): DragDirection => {
-        const { width, height } = this.getStates();
-        const { rotation } = this.getProps();
-        const { width: containerWidth, height: containerHeight } =this._getContainerBounds();
-        let canDragHorizontal = width > containerWidth;
-        let canDragVertical = height > containerHeight;
-        if (this._isImageVertical()) {
-            canDragHorizontal = height > containerWidth;
-            canDragVertical = width > containerHeight;
-        }
+    calcBoundingRectSize(width = 0, height = 0, rotation = 0) {
+        const angleInRadians = rotation * Math.PI / 180;
+        const sinTheta = Math.abs(Math.sin(angleInRadians));
+        const cosTheta = Math.abs(Math.cos(angleInRadians));
+        const boundingWidth = width * cosTheta + height * sinTheta;
+        const boundingHeight = width * sinTheta + height * cosTheta;
+
+        return {
+            width: boundingWidth,
+            height: boundingHeight
+        };
+    }
+
+    getCanDragDirection = (width: number, height: number): DragDirection => {
+        let canDragHorizontal = width > this.containerWidth;
+        let canDragVertical = height > this.containerHeight;
+
         return {
             canDragVertical,
             canDragHorizontal,
         };
     };
 
-    calculatePreviewImage = (newZoom: number, e: any): void => {
+    changeZoom = (newZoom: number, e?: WheelEvent): void => {
         const imageDOM = this._adapter.getImage();
-        const { canDragVertical, canDragHorizontal } = this.calcCanDragDirection();
-        const canDrag = canDragVertical || canDragHorizontal;
-        const { width: containerWidth, height: containerHeight } = this._getContainerBounds();
+        const { currZoom, translate, width, height } = this.getStates();
+        const { rotation } = this.getProps();
+        const changeScale = newZoom / (currZoom || 1);
         const newWidth = Math.floor(this.originImageWidth * newZoom);
         const newHeight = Math.floor(this.originImageHeight * newZoom);
+        let newTranslateX = Math.floor(translate.x * changeScale);
+        let newTranslateY = Math.floor(translate.y * changeScale);
 
-        // debugger;
-        let _offset;
-        const horizontal = !this._isImageVertical();
-        let newTop = 0;
-        let newLeft = 0;
-        if (horizontal) {
-            _offset = {
-                x: 0.5 * (containerWidth - newWidth),
-                y: 0.5 * (containerHeight - newHeight),
-            };
-           
-            newLeft = _offset.x;
-            newTop= _offset.y;
-        } else {
-            _offset = {
-                x: 0.5 * (containerWidth - newHeight),
-                y: 0.5 * (containerHeight - newWidth),
-            };
-            newLeft = _offset.x - (newWidth - newHeight) / 2;
-            newTop = _offset.y + (newWidth - newHeight) / 2;
+        const imageBound = this.calcBoundingRectSize(width, height, rotation);
+        const newImageBound = {
+            width: imageBound.width * changeScale,
+            height: imageBound.height * changeScale
+        };
+
+        if (e && imageDOM && e.target === imageDOM) {
+            const { x: offsetX, y: offsetY } = this.calcBoundingRectMouseOffset({
+                width,
+                height,
+                offset: {
+                    x: e.offsetX,
+                    y: e.offsetY
+                },
+                rotation
+            });
+
+            const imageNewCenterX = e.clientX + (imageBound.width / 2 - offsetX) * changeScale;
+            const imageNewCenterY = e.clientY + (imageBound.height / 2 - offsetY) * changeScale;
+            const containerCenterX = this.containerWidth / 2;
+            const containerCenterY = this.containerHeight / 2;
+
+            newTranslateX = imageNewCenterX - containerCenterX;
+            newTranslateY = imageNewCenterY - containerCenterY;
         }
-        
+
+        const newTranslate = this.getSafeTranslate(newImageBound.width, newImageBound.height, newTranslateX, newTranslateY);
+
         this.setState({
+            translate: newTranslate,
             width: newWidth,
             height: newHeight,
-            offset: _offset,
-            left: newLeft,
-            top: newTop,
             currZoom: newZoom,
         } as any);
         if (imageDOM) {
+            const { canDragVertical, canDragHorizontal } = this.getCanDragDirection(newImageBound.width, newImageBound.height);
+            const canDrag = canDragVertical || canDragHorizontal;
+
             this._adapter.setImageCursor(canDrag);
         }
     };
 
-    calcExtremeBounds = (): ExtremeBounds => {
-        const { width, height } = this.getStates(); 
-        const { width: containerWidth, height: containerHeight } = this._getContainerBounds();
-        let extremeLeft = containerWidth - width;
-        let extremeTop = containerHeight - height;
-        if (this._isImageVertical()) {
-            extremeLeft = containerWidth - height;
-            extremeTop = containerHeight - width;
-        }
+    getExtremeTranslate = (width: number, height: number): ExtremeTranslate => {
         return {
-            left: extremeLeft,
-            top: extremeTop,
+            x: (width - this.containerWidth) / 2,
+            y: (height - this.containerHeight) / 2,
         };
     };
 
-    handleMoveImage = (e: any): void => {
-        const { offset, width, height } = this.getStates();
-        const { canDragVertical, canDragHorizontal } = this.calcCanDragDirection();
+    getSafeTranslate = (width: number, height: number, translateX: number, translateY: number) => {
+        const { x: extremeX, y: extremeY } = this.getExtremeTranslate(width, height);
+        const { canDragVertical, canDragHorizontal } = this.getCanDragDirection(width, height);
+
+        let newTranslateX = 0,
+            newTranslateY = 0;
+
+        if (canDragHorizontal) {
+            newTranslateX = translateX > 0 ? Math.min(translateX, extremeX) : Math.max(translateX, -extremeX);
+        }
+
+        if (canDragVertical) {
+            newTranslateY = translateY > 0 ? Math.min(translateY, extremeY) : Math.max(translateY, -extremeY);
+        }
+
+        return {
+            x: newTranslateX,
+            y: newTranslateY
+        };
+    }
+
+    handleImageMove = (e: MouseEvent): void => {
         // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
         const mouseLeftPress = e.buttons === 1;
-        if (mouseLeftPress && (canDragVertical || canDragHorizontal)) {
-            const { clientX, clientY } = e;
-            const { left: containerLeft, top: containerTop } = this._getContainerBounds();
-            const { left: extremeLeft, top: extremeTop } = this.calcExtremeBounds();
-            let newX = canDragHorizontal ? clientX - containerLeft - this.startMouseOffset.x : offset.x;
-            let newY = canDragVertical ? clientY - containerTop - this.startMouseOffset.y : offset.y;
-            if (canDragHorizontal) {
-                newX = newX > 0 ? 0 : newX < extremeLeft ? extremeLeft : newX;
-            }
-            if (canDragVertical) {
-                newY = newY > 0 ? 0 : newY < extremeTop ? extremeTop : newY;
-            }
-            const _offset = {
-                x: newX,
-                y: newY,
-            };
+
+        if (mouseLeftPress) {
+            this.moveImage(e);
+        }
+    };
+
+    moveImage = (e: MouseEvent) => {
+        const { clientX, clientY } = e;
+        const { width, height, translate } = this.getStates();
+        const { rotation } = this.getProps();
+        const imageBound = this.calcBoundingRectSize(width, height, rotation);
+        const { canDragVertical, canDragHorizontal } = this.getCanDragDirection(imageBound.width, imageBound.height);
+
+        if (canDragVertical || canDragHorizontal) {
+            let newTranslateX = canDragHorizontal ? translate.x + clientX - this.startMouseClientPosition.x : translate.x;
+            let newTranslateY = canDragVertical ? translate.y + clientY - this.startMouseClientPosition.y : translate.y;
+            
+            const newTranslate = this.getSafeTranslate(imageBound.width, imageBound.height, newTranslateX, newTranslateY);
+
             this.setState({
-                offset: _offset,
-                left: this._isImageVertical() ? _offset.x - (width - height) / 2 : _offset.x,
-                top: this._isImageVertical() ? _offset.y + (width - height) / 2 : _offset.y,
+                translate: newTranslate,
             } as any);
+
+            this.startMouseClientPosition = {
+                x: clientX,
+                y: clientY
+            };
         }
     };
 
     handleImageMouseDown = (e: any): void => {
-        this.startMouseOffset = this._getOffset(e);
+        this.startMouseClientPosition = {
+            x: e.clientX,
+            y: e.clientY
+        };
     };
 
+    // 鼠标事件的 e.offset 是以 dom 旋转前左上角为零点的, 这个方法会转换为以旋转后元素的外接矩形左上角为零点的 offset
+    calcBoundingRectMouseOffset = (calcBoundingRectMouseOffset: CalcBoundingRectMouseOffset) => {
+        const {
+            width,
+            height,
+            offset,
+            rotation = 0
+        } = calcBoundingRectMouseOffset;
+
+        let degrees = rotation % 360;
+        degrees = degrees >= 0 ? degrees : 360 + degrees;
+        let boundOffsetX = 0,
+            boundOffsetY = 0;
+
+        switch (degrees) {
+            case 0: 
+                boundOffsetX = offset.x;
+                boundOffsetY = offset.y;
+                break;
+            case 90: 
+                boundOffsetX = height - offset.y;
+                boundOffsetY = offset.x;
+                break;
+            case 180: 
+                boundOffsetX = width - offset.x;
+                boundOffsetY = height - offset.y;
+                break;
+            case 270:
+                boundOffsetX = offset.y;
+                boundOffsetY = width - offset.x;
+                break;
+            default:
+                break;
+        }
+
+        return {
+            x: boundOffsetX,
+            y: boundOffsetY
+        };
+    }
 }

+ 11 - 6
packages/semi-foundation/image/previewInnerFoundation.ts

@@ -20,7 +20,8 @@ export interface PreviewInnerAdapter<P = Record<string, any>, S = Record<string,
     disabledBodyScroll: () => void;
     enabledBodyScroll: () => void;
     getSetDownloadFunc: () => (src: string) => string;
-    isValidTarget: (e: any) => boolean
+    isValidTarget: (e: any) => boolean;
+    changeImageZoom: (zoom: number, e?: WheelEvent) => void
 }
 
 
@@ -90,12 +91,12 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
         }
     }
 
-    handleWheel = (e: any) => {
+    handleWheel = (e: WheelEvent) => {
         this.onWheel(e);
         handlePrevent(e);
     }
 
-    onWheel = (e: any): void => {
+    onWheel = (e: WheelEvent): void => {
         const { zoomStep, maxZoom, minZoom } = this.getProps();
         const { zoom: currZoom } = this.getStates();
         let _zoom: number;
@@ -111,7 +112,7 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
             }
         }
         if (!isUndefined(_zoom)) {
-            this.handleZoomImage(_zoom);
+            this.handleZoomImage(_zoom, true, e);
         }
     };
 
@@ -193,17 +194,21 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
 
     handleRotateImage = (direction: string) => {
         const { rotation } = this.getStates();
-        const newRotation = rotation + (direction === "left" ? 90 : (-90));
+        const ROTATE_STEP = 90;
+        const newRotation = rotation + (direction === "left" ? -ROTATE_STEP : ROTATE_STEP);
+        
         this.setState({
             rotation: newRotation,
         } as any);
         this._adapter.notifyRotateChange(newRotation);
     }
 
-    handleZoomImage = (newZoom: number, notify: boolean = true) => {
+    handleZoomImage = (newZoom: number, notify: boolean = true, e?: WheelEvent) => {
         const { zoom } = this.getStates();
         if (zoom !== newZoom) {
             notify && this._adapter.notifyZoom(newZoom, newZoom > zoom);
+            
+            this._adapter.changeImageZoom(newZoom, e);
             this.setState({
                 zoom: newZoom,
             } as any);

+ 8 - 10
packages/semi-ui/image/interface.tsx

@@ -11,7 +11,7 @@ export interface ImageStates {
     previewVisible: boolean
 }
 
-export interface ImageProps extends BaseProps{
+export interface ImageProps extends BaseProps {
     src?: string;
     width?: string | number;
     height?: string | number;
@@ -22,7 +22,7 @@ export interface ImageProps extends BaseProps{
     onError?: (event: Event) => void;
     onLoad?: (event: Event) => void;
     onClick?: (event: any) => void;
-    crossOrigin?: "anonymous"| "use-credentials";
+    crossOrigin?: "anonymous" | "use-credentials";
     children?: ReactNode;
     imageID?: number;
     setDownloadName?: (src: string) => string;
@@ -59,7 +59,7 @@ export interface PreviewProps extends BaseProps {
     disableDownload?: boolean;
     zIndex?: number;
     children?: ReactNode;
-    crossOrigin?: "anonymous"| "use-credentials";
+    crossOrigin?: "anonymous" | "use-credentials";
     maxZoom?: number;
     minZoom?: number;
     previewCls?: string;
@@ -81,14 +81,14 @@ export interface PreviewProps extends BaseProps {
     setDownloadName?: (src: string) => string
 }
 
-export interface PreviewInnerProps extends Omit<PreviewProps, "previewCls" | "previewStyle"> {}
+export interface PreviewInnerProps extends Omit<PreviewProps, "previewCls" | "previewStyle"> { }
 
 export interface MenuProps {
     min?: number;
     max?: number;
     step?: number;
     curPage?: number;
-    totalNum?: number; 
+    totalNum?: number;
     zoom?: number;
     ratio?: RatioType;
     disabledPrev?: boolean;
@@ -173,14 +173,14 @@ export interface PreviewImageProps {
     ratio?: RatioType;
     disableDownload?: boolean;
     clickZoom?: number;
-    crossOrigin?: "anonymous"| "use-credentials";
+    crossOrigin?: "anonymous" | "use-credentials";
     setRatio?: (type: RatioType) => void;
     onZoom?: (zoom: number) => void;
     onLoad?: (src: string) => void;
     onError?: (src: string) => void
 }
 
-export interface ImageOffset {
+export interface ImageTranslate {
     x: number;
     y: number
 }
@@ -189,10 +189,8 @@ export interface PreviewImageStates {
     loading: boolean;
     width: number;
     height: number;
-    offset: ImageOffset;
+    translate: ImageTranslate;
     currZoom: number;
-    top: number;
-    left: number
 }
 
 export interface DragDirection {

+ 19 - 22
packages/semi-ui/image/previewImage.tsx

@@ -43,7 +43,7 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
             getImage: () => {
                 return this.imageRef.current;
             },
-            setLoading: (loading: boolean) => { 
+            setLoading: (loading: boolean) => {
                 this.setState({
                     loading,
                 });
@@ -64,10 +64,11 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
             width: 0,
             height: 0,
             loading: true,
-            offset: { x: 0, y: 0 },
-            currZoom: 0,
-            top: 0,
-            left: 0,
+            translate: {
+                x: 0,
+                y: 0
+            },
+            currZoom: this.props.zoom,
         };
         this.containerRef = React.createRef<HTMLDivElement>();
         this.imageRef = React.createRef<HTMLImageElement>();
@@ -75,6 +76,7 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
     }
 
     componentDidMount() {
+        this.foundation.init();
         window.addEventListener("resize", this.onWindowResize);
     }
 
@@ -89,10 +91,6 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
         if (srcChange) {
             this.foundation.setLoading(true);
         }
-        // If the incoming zoom changes, other content changes are determined based on the new zoom value
-        if (zoomChange) {
-            this.foundation.calculatePreviewImage(this.props.zoom, null);
-        }
         if (!zoomChange && !srcChange && prevProps) {
             if ("ratio" in this.props && this.props.ratio !== prevProps.ratio) {
                 this.foundation.handleRatioChange();
@@ -100,7 +98,7 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
             if ("rotation" in this.props && this.props.rotation !== prevProps.rotation) {
                 this.onWindowResize();
             }
-        }   
+        }
     }
 
     onWindowResize = (): void => {
@@ -120,28 +118,27 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
         this.foundation.handleError(e);
     }
 
-    handleMoveImage = (e): void => {
-        this.foundation.handleMoveImage(e);
+    handleImageMove = (e): void => {
+        this.foundation.handleImageMove(e);
     };
 
-    onImageMouseDown = (e: React.MouseEvent<HTMLImageElement>): void => {
+    handleMouseDown = (e: React.MouseEvent<HTMLImageElement>): void => {
         this.foundation.handleImageMouseDown(e);
     };
 
     render() {
         const { src, rotation, crossOrigin } = this.props;
-        const { loading, width, height, top, left } = this.state;
+        const { loading, width, height, translate } = this.state;
+
         const imgStyle = {
             position: "absolute",
             visibility: loading ? "hidden" : "visible",
-            transform: `rotate(${-rotation}deg)`,
-            top,
-            left,
+            transform: `translate(${translate.x}px, ${translate.y}px) rotate(${rotation}deg)`,
             width,
-            height,
+            height
         };
         return (
-            <div 
+            <div
                 className={`${preViewImgPrefixCls}`}
                 ref={this.containerRef}
             >
@@ -152,8 +149,8 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
                     alt="previewImag"
                     className={`${preViewImgPrefixCls}-img`}
                     key={src}
-                    onMouseMove={this.handleMoveImage}
-                    onMouseDown={this.onImageMouseDown}
+                    onMouseMove={this.handleImageMove}
+                    onMouseDown={this.handleMouseDown}
                     onContextMenu={this.handleRightClickImage}
                     onDragStart={(e): void => e.preventDefault()}
                     onLoad={this.handleLoad}
@@ -161,7 +158,7 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
                     style={imgStyle as React.CSSProperties}
                     crossOrigin={crossOrigin}
                 />
-                {loading && <Spin size={"large"} wrapperClassName={`${preViewImgPrefixCls}-spin`}/>}
+                {loading && <Spin size={"large"} wrapperClassName={`${preViewImgPrefixCls}-spin`} />}
             </div>
         );
     }

+ 6 - 0
packages/semi-ui/image/previewInner.tsx

@@ -169,6 +169,9 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
                 }
                 // Move in the preview area except the operation area, return true
                 return true;
+            },
+            changeImageZoom: (...args) => {
+                this.imageRef?.current && this.imageRef.current.foundation.changeZoom(...args)
             }
         };
 
@@ -178,6 +181,7 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
     foundation: PreviewInnerFoundation;
     imageWrapRef: React.RefObject<HTMLDivElement>;
     headerRef: React.RefObject<HTMLElement>;
+    imageRef: React.RefObject<PreviewImage>;
     footerRef: React.RefObject<HTMLElement>;
     leftIconRef: React.RefObject<HTMLDivElement>;
     rightIconRef: React.RefObject<HTMLDivElement>;
@@ -201,6 +205,7 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
         this.originBodyWidth = '100%';
         this.scrollBarWidth = 0;
         this.imageWrapRef = null;
+        this.imageRef = React.createRef<PreviewImage>();
         this.headerRef = React.createRef<HTMLElement>();
         this.footerRef= React.createRef<HTMLElement>();
         this.leftIconRef= React.createRef<HTMLDivElement>();
@@ -409,6 +414,7 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
                 >
                     <Header ref={this.headerRef} className={cls(hideViewerCls)} onClose={this.handlePreviewClose} renderHeader={renderHeader} closable={closable}/>
                     <PreviewImage
+                        ref={this.imageRef}
                         src={imgSrc[currentIndex]}
                         onZoom={this.handleZoomImage}
                         disableDownload={disableDownload}