瀏覽代碼

test: [Image] add test cases for Image (#1236)

YyumeiZhang 2 年之前
父節點
當前提交
f8d62f4c66

+ 1 - 0
.gitignore

@@ -26,6 +26,7 @@ storybook-static/
 *.zip
 cypress/videos/
 cypress/screenshots/
+cypress/downloads/
 
 # misc
 .env.local

+ 501 - 0
cypress/integration/image.spec.js

@@ -0,0 +1,501 @@
+describe('image', () => {
+    it('basic image', () => {
+        // 1. Image 基础功能测试,点击图片,打开预览,点击预览区域,关闭预览
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        // 测试图片宽高是否和设置一致, width, height API
+        cy.get('.semi-image-img-preview').should('have.css', 'width').and('eq', '360px');
+        cy.get('.semi-image-img-preview').should('have.css', 'height').and('eq', '200px');
+        // 点击图片区域触发预览
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        // 点击预览非操作区域触发预览关闭
+        cy.get('.semi-image-preview').click();
+        cy.get('.semi-image-preview').should('not.exist');
+    
+        // 2. Image 设置 preview = {false}, 不可预览
+        cy.get('#preview').children('.semi-switch').click();
+        cy.get('.semi-image-img').click();
+        cy.get('.semi-image-preview').should('not.exist');
+    });
+
+    it('load error Image', () => {
+        // 图片加载失败显示加载失败样式
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--load-error-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-image-status').eq(0).children('.semi-icon-upload_error').should('exist');
+        // 测试自定义加载失败占位图 fallback API
+        cy.get('.semi-image-status').eq(1).children('span').should('have.attr', 'style').should('contain', 'font-size: 50px;');
+    });
+
+    it('controlled Preview single', () => {
+        // 测试单独使用 ImagePreview 的 visible 受控显示功能
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--controlled-preview-single&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-button').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('.semi-image-preview').click();
+        cy.get('.semi-image-preview').should('not.exist');
+    });
+
+    it('image show controlled', () => {
+        // 测试 Image 的 visible 受控显示功能
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--image-show-controlled&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('.semi-image-preview').click();
+        cy.get('.semi-image-preview').should('not.exist');
+    });
+
+    it('controlled preview multiple', () => {
+        // 测试 多个 Image 的 visbile 受控显示功能
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--controlled-preview-multiple&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-button').click();
+        cy.get('.semi-image-preview').should('exist');
+        // 切换到下一张图片
+        cy.get('.semi-image-preview-next').should('be.visible');
+        cy.get('.semi-image-preview-next').click();
+        cy.get('.semi-image-preview-footer-page').children('span').eq(0).contains('2');
+        cy.get('.semi-image-preview').should('exist');
+        // 切换到上一张图片
+        cy.get('.semi-image-preview-prev').should('be.visible');
+        cy.get('.semi-image-preview-prev').click();
+        cy.get('.semi-image-preview-footer-page').children('span').eq(0).contains('1');
+    });
+
+    // 测试鼠标拖拽图片
+    it('mouse drag', () => {
+        cy.viewport(1440, 800);
+        // 设置的视宽1440px, 高800px,根据 ImagePreview 的计算规则(以宽高中最大 padding 为80px(平均分到左右,上下)为准),
+        // 图片的原始尺寸为1440px, 高800px
+        // 对于当前图片而言,会以高为基准,则缩放比例 zoom = (800 - 80)/ 800 = 0.9开始
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.wait(200);
+        // 验证打开预览时候的 zoom 为0.9, 即宽1296px,高为720px
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1296px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '720px');
+        // 点击放大按钮
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').click();
+        cy.wait(200);
+        // 默认的单次点击步长是0.1,预期单次点击之后的 zoom 为 1, 即宽度为和原来的尺寸一致,宽1440px, 高800px
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1440px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '800px');
+        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
+        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
+        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 }).trigger('mouseup');
+        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');
+    });
+
+    // 测试鼠标滚动滚轮放大,缩小图片
+    it('mouse wheel', () => {
+        cy.viewport(1440, 800);
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-image-img-preview').click();
+        // zoom = 0.9, width = '1296px',height = '720px'
+        cy.get('.semi-image-preview').should('exist');
+        cy.wait(200);
+        // 验证滚轮向上滚动放大图片
+        cy.get('.semi-image-preview-image-img').trigger('mouseover', { clientX: 720, clientY: 400 });
+        // 触发 wheel 事件, 参数 deltaY 表示纵向滚动量, 
+        cy.get('.semi-image-preview-image-img').trigger('wheel', { deltaY: -10, bubbles: true });
+        // 单次滚动向上滚动滚轮,zoom = zoom + zoomStep = 1, width = '1440px', height = '800px'
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1440px');
+        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 表示纵向滚动量, 验证放大
+        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');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '720px');
+    });
+
+    // 测试自定义预览图片功能: 预览图片和 Image 中的图片 src 不同
+    it('custom preview Image', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--custom-preview-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('.semi-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-small.jpeg');
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-big.png');
+    });
+
+    // 通过回调函数 API
+    it('test callback func', () => {
+        cy.viewport(1200, 800);
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--test-call-back-func&args=&viewMode=story', {
+            onBeforeLoad(win) {
+                cy.stub(win.console, 'log').as('consoleLog');
+            },
+        });
+        cy.wait(3000);
+
+        cy.get('.semi-image-img-preview').eq(1).click();
+        cy.wait(100);
+        
+        // 测试点击 footer 上一张按钮回调
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_left').click();
+        cy.get('@consoleLog').should('be.calledWith', 'prev');
+
+        // 测试点击 footer 操作区域下一张按钮回调
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_right').click();
+        cy.get('@consoleLog').should('be.calledWith', 'next');
+
+        // 测试点击 middle 下一张按钮回调
+        cy.get('.semi-icon-arrow_right').click();
+        cy.get('@consoleLog').should('be.calledWith', 'next');
+
+        // 测试点击 middle 上一张按钮回调
+        cy.get('.semi-icon-arrow_left').click();
+        cy.get('@consoleLog').should('be.calledWith', 'prev');
+
+        // 测试预览 footer 操作区域的放大,缩小回调
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').click();
+        cy.get('.semi-image-preview-footer').children('.semi-icon-minus').click();
+        cy.get('@consoleLog').should('be.calledWith', 'zoom out');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').click();
+        cy.get('@consoleLog').should('be.calledWith', 'zoom in');
+
+        // 测试拖拽按钮是否触发回调
+        const sliderHandleSelector ='.semi-slider-handle';
+        // test knob slide
+        cy.get(sliderHandleSelector)
+            .trigger('mousedown')
+            .trigger('mousemove', { pageX: 1200, pageY: 0 })
+            .trigger('mouseup', { force: true });
+        cy.get('@consoleLog').should('be.calledWith', 'zoom in');
+
+        cy.get(sliderHandleSelector)
+            .trigger('mousedown')
+            .trigger('mousemove', { pageX: 0, pageY: 0 })
+            .trigger('mouseup', { force: true });
+        cy.get('@consoleLog').should('be.calledWith', 'zoom out');
+        
+        // 测试 footer 下载回调
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').click();
+        cy.get('@consoleLog').should('be.calledWith', 'download');
+
+        // 测试 footer 旋转回调
+        cy.get('.semi-image-preview-footer').children('.semi-icon-rotate').click();
+        cy.get('@consoleLog').should('be.calledWith', 'rotate change');
+    });
+
+    // 测试 infinite API
+    it('infinite load', () => {
+        // 设置 infinite = true 
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-preview&args=&viewMode=story');
+        cy.wait(3000);
+        cy.get('#infinite').children('.semi-switch').click();
+        cy.get('.semi-image-img-preview').eq(1).click();
+        cy.wait(100);
+        cy.get('.semi-image-preview-prev').click();
+        cy.wait(100);
+        // 到第一张图片时候,可以通过 prev 按钮 切换到最后一张
+        cy.get('.semi-image-preview-prev').should('exist').click();
+        cy.wait(100);
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/beach.jpeg');
+        // 从最后一张图片,可以切换到前一张图片
+        cy.get('.semi-image-preview-next').should('exist').click();
+        cy.wait(100);
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/lion.jpeg');
+    });
+
+    // 测试 closeOnEsc API
+    it('basic image', () => {
+        // 1. Image 基础功能测试,点击图片,打开预览,点击预览区域,关闭预览
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        // 测试 esc 退出预览功能
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('body').type('{esc}', { force: true });
+        cy.get('.semi-image-preview').should('not.exist');
+        // closeOnEsc = false, 关闭点击 esc 退出预览功能
+        cy.get('#escOut').children('.semi-switch').click();
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('body').type('{esc}', { force: true });
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('.semi-image-preview').click();
+    });
+
+    // 测试 disableDownload API
+    it('disable download', () => {
+        // 设置 disableDownload,无法下载
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('#disableDownload').children('.semi-switch').click();
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').should('have.class', 'semi-image-preview-footer-disabled');
+    });
+
+    // 测试 maskClosable API
+    it('maskClosable', () => {
+        // 设置 disableDownload,无法下载
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-image&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('#maskClosable').children('.semi-switch').click();
+        cy.get('.semi-image-img-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('.semi-image-preview').click();
+        cy.get('.semi-image-preview').should('exist');
+        cy.get('.semi-icon-close').click();
+        cy.get('.semi-image-preview').should('not.exist');
+    });
+
+    // 测试 getPopupContainer API
+    it('custom container', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--custom-container&args=&viewMode=story');
+        cy.wait(3000);
+        cy.get('.semi-image-img-preview').eq(0).click();
+        cy.get('.semi-image-preview').parent('.semi-portal').parent().should('have.attr', 'id', 'container');
+    });  
+
+    // 测试自定义预览顶部信息 renderHeader API
+    it('custom render top', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--custom-render-title&args=&viewMode=story');
+        cy.wait(3000);
+        cy.get('.semi-image-img-preview').eq(0).click();
+        cy.get('.semi-image-preview').should('exist');
+        // 点击预览后,title 部分的信息为自定义信息
+        cy.get('.semi-image-preview-header-title').children('div').contains('lamp1');
+        cy.get('.semi-image-preview-next').should('be.visible');
+        cy.get('.semi-image-preview-next').click();
+        // 切换图片后,title 部分会切换为对应的自定义信息
+        cy.get('.semi-image-preview-header-title').children('div').contains('lamp2');
+    });
+
+    // 测试自定义预览操作区 renderPreviewMenu API
+    it('custom render footer', () => {
+        cy.viewport(1200, 800);
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--custom-render-footer-menu&args=&viewMode=story', {
+            onBeforeLoad(win) {
+                cy.stub(win.console, 'log').as('consoleLog');
+            },
+        });
+        cy.wait(3000);
+        cy.get('.semi-image-img-preview').eq(0).click();
+        cy.get('.semi-image-preview').should('exist');
+        
+        // 点击预览后,footer操作区域信息为自定义页脚信息
+        cy.get('.semi-icon-chevron_right').click();
+        cy.wait(500);
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/seaside.jpeg');
+        cy.get('.semi-icon-chevron_left').click();
+        cy.wait(500);
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/lion.jpeg');
+        
+        // 对于 lion.jpeg(原尺寸1080 * 720), 因为container(1200 * 800),初始 zoom 为 1 (1080 * 720)
+        cy.get('.semi-icon-minus').click();
+        cy.wait(500);
+        // zoom = 0.9, width * height = 972 * 648
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '972px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '648px');
+
+        cy.get('.semi-icon-plus').click();
+        cy.wait(500);
+        // zoom = 1, width * height = 1080 * 720
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1080px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '720px');
+
+        cy.get('.semi-icon-plus').click();
+        cy.get('.semi-icon-plus').click();
+        cy.wait(500);
+        // zoom = 1.2,
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1296px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '864px');
+
+        // 点击原始尺寸按钮,zoom = 1, width * height = 1080 * 720
+        cy.get('.semi-icon-real_size_stroked').click();
+        cy.wait(500);
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1080px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '720px');
+
+        cy.get('.semi-icon-minus').click();
+        cy.get('.semi-icon-minus').click();
+        cy.wait(500);
+        // zoom = 0.8, width * height = 864 * 576
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '864px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '576px');
+        
+        // 在 container 1200 * 800时, 适应页面尺寸为 zoom 为 1, width * height = 1080 * 720
+        cy.get('.semi-icon-window_adaption_stroked').click();
+        cy.wait(500);
+        cy.get('.semi-image-preview-image-img').should('have.css', 'width').and('eq', '1080px');
+        cy.get('.semi-image-preview-image-img').should('have.css', 'height').and('eq', '720px');
+
+        // 测试点击向右旋转,向左旋转按键
+        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-icon-rotate').eq(1).click();
+        cy.get('.semi-image-preview-image-img').should('have.attr', 'style').should('contain', 'transform: rotate(0deg)');
+
+        // 测试下载按键
+        cy.get('.semi-icon-download').click();
+        cy.get('@consoleLog').should('be.calledWith', 'download');
+    });
+
+    // 测试 showTooltip API
+    it('show operation tooltip', () => {
+        // 测试 Image 的 预览 footer 操作区域 hover 到相关图标是否 tooltip 提示
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-preview&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('#showTooltip').children('.semi-switch').click();
+        cy.wait(1000);
+        cy.get('.semi-image-img-preview').eq(1).click();
+        cy.get('.semi-image-preview').should('exist');
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_left').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('上一张');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_left').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_right').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('下一张');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_right').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-minus').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('缩小');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-minus').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('放大');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('原始尺寸');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').click();
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-window_adaption_stroked').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('适应页面');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-window_adaption_stroked').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-rotate').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('旋转');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-rotate').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('下载');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').trigger('mouseout');
+    });
+
+    // 测试 xxxTooltip API
+    it('custom operation tooltip', () => {
+        // 测试 Image 的 预览 footer 操作区域 hover 到相关图标是否 自定义 tooltip 提示
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-preview&args=&viewMode=story');
+        cy.wait(2000);
+        cy.get('#showTooltip').children('.semi-switch').click();
+        cy.get('#customTooltip').children('.semi-switch').click();
+        cy.wait(1000);
+        cy.get('.semi-image-img-preview').eq(1).click();
+        cy.get('.semi-image-preview').should('exist');
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_left').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Prev');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_left').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_right').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Next');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-chevron_right').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-minus').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('ZoomOut');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-minus').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('ZoomIn');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-plus').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Original size');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-real_size_stroked').click();
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-window_adaption_stroked').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Adaption');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-window_adaption_stroked').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-rotate').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Rotate');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-rotate').trigger('mouseout');
+        cy.wait(1000);
+
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').trigger('mouseover');
+        cy.wait(1000);
+        cy.get('.semi-tooltip-wrapper').contains('Download');
+        cy.get('.semi-image-preview-footer').children('.semi-icon-download').trigger('mouseout');
+    });
+});

+ 6 - 0
cypress/support/index.js

@@ -20,3 +20,9 @@ import '@cypress/code-coverage/support';
 require('cypress-plugin-tab');
 // Alternatively you can use CommonJS syntax:
 // require('./commands')
+
+Cypress.on('uncaught:exception', (err, runnable) => {
+    // returning false here prevents Cypress from
+    // failing the test
+    return false;
+});

+ 1 - 1
packages/semi-foundation/image/imageFoundation.ts

@@ -50,7 +50,7 @@ export default class ImageFoundation<P = Record<string, any>, S = Record<string,
         if (isObject(preview)) {
             const { onVisibleChange } = preview as any;
             onVisibleChange && onVisibleChange(newVisible);
-            if (!("visible" in this.getProps())) {
+            if (!("visible" in preview)) {
                 this.setState({
                     previewVisible: newVisible,
                 } as any);

+ 2 - 2
packages/semi-foundation/image/previewFooterFoundation.ts

@@ -20,9 +20,9 @@ export default class PreviewFooterFoundation<P = Record<string, any>, S = Record
     handleValueChange = (value: number): void => {
         const { onZoomIn, onZoomOut, zoom } = this.getProps();
         if (value > zoom) {
-            onZoomIn(value / 100);
+            onZoomIn(Number((value / 100).toFixed(2)));
         } else {
-            onZoomOut(value / 100);
+            onZoomOut(Number((value / 100).toFixed(2)));
         }
         this._adapter.setStartMouseOffset(value);
     };

+ 0 - 1
packages/semi-foundation/image/previewImageFoundation.ts

@@ -257,7 +257,6 @@ export default class PreviewImageFoundation<P = Record<string, any>, S = Record<
             }
             if (canDragVertical) {
                 newY = newY > 0 ? 0 : newY < extremeTop ? extremeTop : newY;
-
             }
             const _offset = {
                 x: newX,

+ 2 - 2
packages/semi-foundation/image/previewInnerFoundation.ts

@@ -4,7 +4,7 @@ import { getPreloadImagArr, downloadImage, isTargetEmit } from "./utils";
 
 export interface PreviewInnerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
     getIsInGroup: () => boolean;
-    notifyChange: (index: number) => void;
+    notifyChange: (index: number, direction: string) => void;
     notifyZoom: (zoom: number, increase: boolean) => void;
     notifyClose: () => void;
     notifyVisibleChange: (visible: boolean) => void;
@@ -119,7 +119,7 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
                 currentIndex: newIndex,
             } as any);
         }
-        this._adapter.notifyChange(newIndex);
+        this._adapter.notifyChange(newIndex, direction);
         this.setState({
             direction,
             rotation: 0,

+ 227 - 50
packages/semi-ui/image/_story/image.stories.jsx

@@ -6,6 +6,8 @@ import {
     Row,
     Col,
     Icon,
+    Switch,
+    Input
 } from "../../index";
 import { 
     IconChevronLeft, 
@@ -16,6 +18,7 @@ import {
     IconDownload,
     IconWindowAdaptionStroked,
     IconRealSizeStroked,
+    IconUploadError,
 } from "@douyinfe/semi-icons";
 
 export default {
@@ -26,37 +29,117 @@ export default {
 }
 
 const srcList1 = [
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/lion.jpeg",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/seaside.jpeg",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/beach.jpeg",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/lion.jpeg",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/seaside.jpeg",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/beach.jpeg",
 ];
 
 const srcList2 = [
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag1.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag2.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag3.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag4.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag5.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag6.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag7.png",
-    "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/imag8.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag1.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag2.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag3.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag4.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag5.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag6.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag7.png",
+    "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/imag8.png",
 ];
 
-export const basicImage = () => (
-    <Image 
-        width={360}
-        height={200}
-        src="https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/lion.jpeg"
-    />
+export const basicImage = () => {
+    const [escOut, setEscOut] = useState(true);
+    const [disableDownload, setDisableDownload] = useState(false);
+    const [maskClosable, setMaskClosable] = useState(true);
+    const [preview, setPreview] = useState(true);
+
+    const itemStyle = { display: 'flex', alignItems: 'center', flexShrink: 0, width: 'fit-content', margin: '10px 20px 0 0' };
+    const menuStyle = { marginBottom: 20, display: 'flex', flexWrap: 'wrap' };
+
+    return (
+    <>
+        <div style={menuStyle}>
+            <div style={itemStyle} id='preview'>
+                <span >是否可预览:</span>
+                <Switch checked={preview} checkedText="是" uncheckedText="否" onChange={setPreview}/>
+            </div>
+            <div style={itemStyle} id='escOut'>
+                <span>点击 esc 是否关闭预览:</span>
+                <Switch checked={escOut} checkedText="是" uncheckedText="否" onChange={setEscOut}/>
+            </div>
+            <div style={itemStyle} id='disableDownload'>
+                <span >是否禁用下载:</span>
+                <Switch checked={disableDownload} checkedText="是" uncheckedText="否" onChange={setDisableDownload}/>
+            </div>
+            <div style={itemStyle} id='maskClosable'>
+                <span >点击遮罩层是否关闭预览:</span>
+                <Switch checked={maskClosable} checkedText="是" uncheckedText="否" onChange={setMaskClosable}/>
+            </div>
+        </div>
+        <Image
+            width={360}
+            height={200}
+            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-lite.jpeg"
+            preview={preview ? {
+                closeOnEsc: escOut,
+                disableDownload,
+                maskClosable
+            } : false}
+        />
+    </>
+)}
+
+export const LoadErrorImage = () => (
+    <>
+        <p>加载失败默认样式</p>
+        <Image 
+            width={200}
+            height={200}
+            src="https://load-error.jpeg"
+        />
+        <br />
+        <p>自定义加载失败占位图</p>
+        <Image 
+            width={200}
+            height={200}
+            src="https://load-error.jpeg"
+            fallback={<IconUploadError style={{ fontSize: 50 }} />}
+        />
+    </>
 )
 
-export const ShowOperationTooltip = () => (
-    <Image 
-        width={360}
+export const ProgressiveLoading = () => {
+    const [timestamp, setTimestamp] = React.useState('');
+    return (  
+        <>
+            <Image 
+                width={300}
+                height={200}
+                src={`https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-big.png?${timestamp}`}
+                placeholder={<Image 
+                    src='https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-small.jpeg'
+                    width={300}
+                    height={200}
+                    preview={false}
+                />}
+            />
+            <br />
+            <Button 
+                theme={'solid'}
+                onClick={() => {
+                    setTimestamp(Date.now());
+                }}
+                style={{ marginTop: 10 }}
+            >Reload</Button>
+        </>
+    );
+}
+
+export const CustomPreviewImage = () => (
+    <Image
+        width={300}
         height={200}
-        src="https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/lion.jpeg"
+        src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-small.jpeg'}
         preview={{
-            showTooltip: true,
+            src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-big.png'
         }}
     />
 )
@@ -76,7 +159,7 @@ export const ControlledPreviewSingle = () => {
         <>
             <Button onClick={handleClick}>{visible ? "hide" : "show single"}</Button>
             <ImagePreview 
-                src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/lion.jpeg"}
+                src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-lite.jpeg"}
                 visible={visible}
                 onVisibleChange={handlePreviewVisibleChange}
             />
@@ -84,6 +167,26 @@ export const ControlledPreviewSingle = () => {
     )
 }
 
+export const ImageShowControlled = () => {
+    const [visible, setVisible] = useState(false);
+
+    const handlePreviewVisibleChange = useCallback((v) => {
+        setVisible(v);
+    }, []);
+
+    return (
+        <Image 
+            width={360}
+            height={200}
+            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract-lite.jpeg"
+            preview={{
+                visible: visible,
+                onVisibleChange: handlePreviewVisibleChange
+            }}
+        />
+    );
+}
+
 export const ControlledPreviewMultiple = () => {
     const [visible, setVisible] = useState(false);
 
@@ -107,19 +210,68 @@ export const ControlledPreviewMultiple = () => {
     )
 }
 
-export const BasicPreview = () => (
-    <ImagePreview>
-        {srcList1.map((src, index) => {
-            return (
-                <Image 
-                    key={index} 
-                    src={src} 
-                    width={200} 
-                    alt={`lamp${index + 1}`}
-                />
-        )})}
-    </ImagePreview>
-);
+export const BasicPreview = () => {
+    const [showTooltip, setShowTooltip] = useState(false);
+    const [customTooltip, setCustomTooltip] = useState(false);
+    const [infinite, setInfinite] = useState(false);
+
+    const customTooltipProps = {
+        prevTip: "Prev",
+        nextTip: "Next",
+        zoomInTip: "ZoomIn",
+        zoomOutTip: "ZoomOut",
+        rotateTip: "Rotate",
+        downloadTip: "Download",
+        adaptiveTip: "Adaption",
+        originTip: "Original size"
+    };
+
+    const props = useMemo(() => {
+        let props = {};
+        if (showTooltip) {
+            props = { showTooltip: true };
+            if (customTooltip) {
+                props = {...props, ...customTooltipProps}
+            }
+        }
+        if (infinite) {
+            props.infinite = true;
+        }
+        return props;
+    }, [showTooltip, customTooltip, infinite])
+
+    const itemStyle = { display: 'flex', alignItems: 'center', flexShrink: 0, width: 'fit-content', margin: '10px 20px 0 0' };
+    const menuStyle = { marginBottom: 20, display: 'flex', flexWrap: 'wrap' };
+
+    return (
+        <>
+            <div style={menuStyle}>
+                <div style={itemStyle} id='showTooltip'>
+                    <span>是否show tooltip:</span>
+                    <Switch checked={showTooltip} checkedText="是" uncheckedText="否" onChange={setShowTooltip}/>
+                </div>
+                <div style={itemStyle} id='customTooltip'>
+                    <span>是否custom tooltip:</span>
+                    <Switch checked={customTooltip} checkedText="是" uncheckedText="否" onChange={setCustomTooltip}/>
+                </div>
+                <div style={itemStyle} id='infinite'>
+                    <span >是否无限循环:</span>
+                    <Switch checked={infinite} checkedText="是" uncheckedText="否" onChange={setInfinite}/>
+                </div>
+            </div>
+            <ImagePreview {...props}>
+                {srcList1.map((src, index) => {
+                    return (
+                        <Image 
+                            key={index}
+                            src={src}
+                            width={200}
+                            alt={`lamp${index + 1}`}
+                        />
+                )})}
+            </ImagePreview>
+    </>
+)};
 
 // test all call back function
 export const TestCallBackFunc = () => {
@@ -164,14 +316,13 @@ export const TestCallBackFunc = () => {
         <>  
             <ImagePreview
                 onVisibleChange={visibleChange}
-                onChange={change}
+                // onChange={change}
                 onClose={close}
                 onZoomIn={zoomIn}
                 onZoomOut={zoomOut}
                 onPrev={prev}
                 onNext={next}
-                onRatioChange={ratioChange}
-                onRotateChange={rotateChange}
+                onRotateLeft={rotateChange}
                 onDownload={download}
             >
                 <div >
@@ -185,14 +336,29 @@ export const TestCallBackFunc = () => {
     )
 };
 
-export const GridImage= () => (
+export const GridImage= () => {
+    const [gap, setGap] = useState(3);
+    const [infinite, setInfinite] = useState(true);
+
+    const switchChange = useCallback((value) => {
+        setInfinite(value);
+    }, []);
+
+    const onInputChange = useCallback((value) => {
+        setGap(value)
+    }, []);
+
+    return (
     <>  
+        <span>是否开启 infinite:</span>
+        <Switch checked={infinite} checkedText="是" uncheckedText="否" onChange={switchChange}/>
+        <span style={{ marginLeft: 50 }}>输入 preLoadGap: </span>
+        <Input style={{ width: 150 }} value={gap} onChange={onInputChange} />
         <ImagePreview
-            preview={{
-                preLoad: true,
-                preLoadGap: 3,
-                infinite: true,
-            }}
+            key={gap}
+            preLoad={true}
+            preLoadGap={Number(gap)}         
+            infinite={infinite}
         >
             <Row style={{ width: 800 }}>
                 {srcList2.map((src, index) => {
@@ -204,13 +370,13 @@ export const GridImage= () => (
             </Row>
         </ImagePreview>
     </>
-);
+)};
 
 export const CustomContainer = () => {
     const srcList = useMemo(() => ([
-        "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/flower.jpeg",
-        "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/duck.jpeg",
-        "https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/swan.jpeg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/flower.jpeg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/duck.jpeg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/swan.jpeg",
     ]), []);
 
     return ( 
@@ -253,6 +419,10 @@ export const CustomContainer = () => {
 }
 
 export const customRenderFooterMenu = () => {
+    const download = useCallback((src, index) =>{
+        console.log("download", src, index);
+    }, []);
+
     const renderPreviewMenu = useCallback((props) => {
         const {
             ratio,
@@ -265,6 +435,7 @@ export const customRenderFooterMenu = () => {
             onNext,
             onPrev,
             onRotateLeft,
+            onRotateRight,
             onRatioClick,
             onZoomIn,
             onZoomOut,
@@ -296,7 +467,7 @@ export const customRenderFooterMenu = () => {
             <Button
                 icon={<IconMinus  size="large" />}
                 type="tertiary"
-                onClick={disableZoomOut ? onZoomOut : undefined}
+                onClick={!disableZoomOut ? onZoomOut : undefined}
                 disabled={disableZoomOut} 
             />
             <Button
@@ -310,6 +481,11 @@ export const customRenderFooterMenu = () => {
                 type="tertiary"
                 onClick={onRatioClick} 
             />
+            <Button
+                icon={<IconRotate size="large" style={{ transform: 'scale(-1,1)'}}/>}
+                type="tertiary"
+                onClick={onRotateRight}
+            />
             <Button
                 icon={<IconRotate size="large" />}
                 type="tertiary"
@@ -328,6 +504,7 @@ export const customRenderFooterMenu = () => {
         <>  
             <ImagePreview
                 renderPreviewMenu={renderPreviewMenu}
+                onDownload={download}
             >
                 {srcList1.map((src, index) => {
                     return <Image key={index} src={src} width={200} alt={`lamp${index + 1}`} />

+ 8 - 1
packages/semi-ui/image/image.tsx

@@ -12,7 +12,7 @@ import { PreviewContext, PreviewContextProps } from "./previewContext";
 import ImageFoundation, { ImageAdapter } from "@douyinfe/semi-foundation/image/imageFoundation";
 import LocaleConsumer from "../locale/localeConsumer";
 import { Locale } from "../locale/interface";
-import { isObject } from "lodash";
+import { isBoolean, isObject } from "lodash";
 import Skeleton from "../skeleton";
 import "@douyinfe/semi-foundation/image/image.scss";
 
@@ -69,6 +69,13 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
             willUpdateStates.src = props.src;
             willUpdateStates.loadStatus = "loading";
         }
+        
+        if (isObject(props.preview)) {
+            const { visible } = props.preview;
+            if (isBoolean(visible)) {
+                willUpdateStates.previewVisible = visible;
+            }
+        }
 
         return willUpdateStates;
     }

+ 1 - 1
packages/semi-ui/image/interface.tsx

@@ -63,7 +63,7 @@ export interface PreviewProps extends BaseProps {
     onPrev?: (index: number) => void;
     onNext?: (index: number) => void;
     onRatioChange?: (type: RatioType) => void;
-    onRotateChange?: (angle: number) => void;
+    onRotateLeft?: (angle: number) => void;
     onDownload?: (src: string, index: number) => void
 }
 

+ 1 - 1
packages/semi-ui/image/preview.tsx

@@ -51,8 +51,8 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
         onPrev: PropTypes.func,
         onNext: PropTypes.func,
         onDownload: PropTypes.func,
+        onRotateLeft: PropTypes.func,
         onRatioChange: PropTypes.func,
-        onRotateChange: PropTypes.func,
     }
 
     static defaultProps = {

+ 1 - 1
packages/semi-ui/image/previewImage.tsx

@@ -24,7 +24,7 @@ export default class PreviewImage extends BaseComponent<PreviewImageProps, Previ
         zoomStep: PropTypes.number,
         zoom: PropTypes.number,
         ratio: PropTypes.string,
-        disableDownload: PropTypes.number,
+        disableDownload: PropTypes.bool,
         clickZoom: PropTypes.number,
         setRatio: PropTypes.func,
         onZoom: PropTypes.func,

+ 10 - 5
packages/semi-ui/image/previewInner.tsx

@@ -65,7 +65,7 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
         onNext: PropTypes.func,
         onDownload: PropTypes.func,
         onRatioChange: PropTypes.func,
-        onRotateChange: PropTypes.func,
+        onRotateLeft: PropTypes.func,
     }
 
     static defaultProps = {
@@ -85,9 +85,14 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
         return {
             ...super.adapter,
             getIsInGroup: () => this.isInGroup(),
-            notifyChange: (index: number) => {
-                const { onChange } = this.props;
+            notifyChange: (index: number, direction: string) => {
+                const { onChange, onPrev, onNext } = this.props;
                 isFunction(onChange) && onChange(index);
+                if (direction === "prev") {
+                    onPrev && onPrev(index);
+                } else {
+                    onNext && onNext(index);
+                }
             },
             notifyZoom: (zoom: number, increase: boolean) => {
                 const { onZoomIn, onZoomOut } = this.props;
@@ -110,8 +115,8 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
                 isFunction(onRatioChange) && onRatioChange(type);
             },
             notifyRotateChange: (angle: number) => {
-                const { onRotateChange } = this.props;
-                isFunction(onRotateChange) && onRotateChange(angle);   
+                const { onRotateLeft } = this.props;
+                isFunction(onRotateLeft) && onRotateLeft(angle);   
             },
             notifyDownload: (src: string, index: number) => {
                 const { onDownload } = this.props;