Browse Source

feat: [Image] renderPreviewMenu API of Image supports menuItem param… (#1718)

* feat: [Image]  renderPreviewMenu API of Image supports menuItem parameter

* feat: [Image] renderPreviewMenu API of Image supports menuItem parameter

* chore: upate image e2e test

* feat: [Image] add setDownload API for Image & PreviewImage

* docs: add setDownloadName API in API reference of Iamge

* chore: remove unnessary console log

* chore: Fix failed e2e test case in Upload component

---------

Co-authored-by: pointhalo <[email protected]>
YyumeiZhang 2 years ago
parent
commit
e90339bf84

+ 65 - 21
content/show/image/index-en-US.md

@@ -41,6 +41,7 @@ You can customize the placeholder for failed loading through `fallback`, which s
 ```jsx live=true
 import React from 'react';
 import { Image } from '@douyinfe/semi-ui';
+import { IconUploadError } from '@douyinfe/semi-icons';
 
 () => (
     <div style={{ display: 'flex', alignItem: 'center', flexDirection: 'column' }}>
@@ -352,7 +353,6 @@ import { IconChevronLeft, IconChevronRight, IconMinus, IconPlus, IconRotate, Ico
             </div>);
     }, []);
 
-
     return ( 
         <ImagePreview renderPreviewMenu={renderPreviewMenu}>
             {srcList.map((src, index) => {
@@ -371,6 +371,48 @@ import { IconChevronLeft, IconChevronRight, IconMinus, IconPlus, IconRotate, Ico
 };
 ```
 
+If you want to customize the preview bottom operation area based on the default bottom operation area, you can get the default ReactNodes through the menuItems of renderPreviewMenu. menuItems is an array of ReactNodes, and the order is consistent with the content order of the default bottom operation bar area. The menuItems parameter is supported from v2.40.0.
+
+```jsx live=true dir="column"
+import React, { useMemo, useCallback } from 'react';
+import { Image, ImagePreview, Divider, Tooltip } from '@douyinfe/semi-ui';
+import { IconInfoCircle } from '@douyinfe/semi-icons';
+
+() => {
+    const srcList = useMemo(() => ([
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/sky.jpg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/greenleaf.jpg",
+    ]), []);
+
+    const renderPreviewMenu = useCallback((props) => {
+        const { menuItems } = props;
+        const customNode = <Tooltip content='I is a custom action'><IconInfoCircle size="large" /></Tooltip>;
+        return (
+            <div style={{ display: 'flex', backgroundColor: 'rgba(0, 0, 0, 0.75)', alignItems: 'center', padding: '5px 16px', borderRadius: 4 }}>
+                {menuItems.slice(0, 3)}
+                <Divider layout="vertical" />
+                {menuItems.slice(3, 7)}
+                <Divider layout="vertical" />
+                {menuItems.slice(7)}
+                <Divider layout="vertical" />
+                {customNode}
+            </div>
+        );
+    }, []);
+
+    return (
+        <>  
+            <ImagePreview
+                renderPreviewMenu={renderPreviewMenu}
+            >
+                {srcList.map((src, index) => (<Image key={index} src={src} width={200} alt={`lamp${index + 1}`} />))}
+            </ImagePreview>
+        </>
+    );
+};
+```
+
 ### Customize the preview top display area
 
 You can customize the preview top display area through `renderHeader`
@@ -435,6 +477,7 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | src              | Image acquisition address            | string            | - | |
 | style            | custom style                         | CSSProperties     | - | |
 | width            | Image display width                  | number            | - | |
+| setDownloadName  | Set the name of the downloaded image | (src: string) => string | - | 2.40.0 |
 
 ### ImagePreview
 
@@ -481,28 +524,29 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | zoomInTip        | Zoom in action button tips                                                                                                                                               | string | "Zoom in" | |
 | zoomOutTip       | Zoom out action button prompt                                                                                                                                            | string | "Zoom out" | |
 | zoomStep         | Image reduction/enlargement ratio each time                                                                                                                              | number | 0.1 | |
-
+| setDownloadName   | Set the name of the downloaded image            | (src: string) => string | - | 2.40.0 |
 ### MenuProps
 
-| Properties       | Instructions            | Type             |
-|------------------|-------------------------|------------------|
-| curPage          | Current image page subscript         | number |
-| disabledPrev     | Whether to disable the left toggle button  | boolean |
-| disabledNext     | Whether to disable the right toggle button | boolean |
-| disableDownload  | Whether to disable the download button     | boolean |
-| max              | The maximum ratio of image zoom      | number |
-| min              | The minimum ratio of image scaling   | number |
-| onDownload       | Call function when the image is downloaded | () => void |
-| onZoomIn         | Call function when the image is zoomed in  | () => void |
-| onZoomOut        | Call function when the image is zoomed out | () => void |
-| onPrev           | Call function to switch the picture forward  | () => void |
-| onNext           | Call function to switch the picture backward | () => void |
-| onRotateLeft     | Call function to rotate the image counterclockwise | () => void |
-| onRotateRight    | Call function to rotate the image clockwise | () => void |
-| ratio            | Original size or Fit to page button state  | "adaptation" \| "realSize" |
-| step             | Step size of scaling                 | number |
-| totalNum         | The total number of images that can be previewed | number |
-| zoom             | Current image magnification ratio    | number |
+| Properties       | Instructions            | Type             | Version |
+|------------------|-------------------------|------------------|-----|
+| curPage          | Current image page subscript         | number |  |
+| disabledPrev     | Whether to disable the left toggle button  | boolean |  |
+| disabledNext     | Whether to disable the right toggle button | boolean |  |
+| disableDownload  | Whether to disable the download button     | boolean |  |
+| max              | The maximum ratio of image zoom      | number |  |
+| min              | The minimum ratio of image scaling   | number |  |
+| onDownload       | Call function when the image is downloaded | () => void |  |
+| onZoomIn         | Call function when the image is zoomed in  | () => void |  |
+| onZoomOut        | Call function when the image is zoomed out | () => void |  |
+| onPrev           | Call function to switch the picture forward  | () => void |  |
+| onNext           | Call function to switch the picture backward | () => void |  |
+| onRotateLeft     | Call function to rotate the image counterclockwise | () => void |  |
+| onRotateRight    | Call function to rotate the image clockwise | () => void |  |
+| ratio            | Original size or Fit to page button state  | "adaptation" \| "realSize" |  |
+| step             | Step size of scaling                 | number |  |
+| totalNum         | The total number of images that can be previewed | number |  |
+| zoom             | Current image magnification ratio    | number |  |
+| menuItems        | Default bottom preview operation area function button ReactNode array | ReactNode[] | 2.40.0 |
 
 ## Design Token
 

+ 65 - 19
content/show/image/index.md

@@ -41,6 +41,7 @@ import { Image } from '@douyinfe/semi-ui';
 ```jsx live=true
 import React from 'react';
 import { Image } from '@douyinfe/semi-ui';
+import { IconUploadError } from '@douyinfe/semi-icons';
 
 () => (
     <div style={{ display: 'flex', alignItem: 'center', flexDirection: 'column' }}>
@@ -371,6 +372,48 @@ import { IconChevronLeft, IconChevronRight, IconMinus, IconPlus, IconRotate, Ico
 };
 ```
 
+如果想基于默认底部操作区域自定义预览底部操作区域, 可以通过 renderPreviewMenu 的 menuItems 获取默认的 ReactNode, menuItems 是一个 ReactNode 数组,顺序和默认底部操作栏功能区域内容顺序一致,menuItems 参数从 v2.40.0 开始支持
+
+```jsx live=true dir="column"
+import React, { useMemo, useCallback } from 'react';
+import { Image, ImagePreview, Divider, Tooltip } from '@douyinfe/semi-ui';
+import { IconInfoCircle } from '@douyinfe/semi-icons';
+
+() => {
+    const srcList = useMemo(() => ([
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/sky.jpg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/greenleaf.jpg",
+    ]), []);
+
+    const renderPreviewMenu = useCallback((props) => {
+        const { menuItems } = props;
+        const customNode = <Tooltip content='我是一个自定义操作'><IconInfoCircle size="large" /></Tooltip>;
+        return (
+            <div style={{ display: 'flex', backgroundColor: 'rgba(0, 0, 0, 0.75)', alignItems: 'center', padding: '5px 16px', borderRadius: 4 }}>
+                {menuItems.slice(0, 3)}
+                <Divider layout="vertical" />
+                {menuItems.slice(3, 7)}
+                <Divider layout="vertical" />
+                {menuItems.slice(7)}
+                <Divider layout="vertical" />
+                {customNode}
+            </div>
+        );
+    }, []);
+
+    return (
+        <>  
+            <ImagePreview
+                renderPreviewMenu={renderPreviewMenu}
+            >
+                {srcList.map((src, index) => (<Image key={index} src={src} width={200} alt={`lamp${index + 1}`} />))}
+            </ImagePreview>
+        </>
+    );
+};
+```
+
 ### 自定义预览顶部展示区
 
 通过 `renderHeader` 可以自定义预览顶部展示区
@@ -435,6 +478,7 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | src               | 图片获取地址                             | string            | - | |
 | style             | 自定义样式                              | CSSProperties     | - | |
 | width             | 图片显示宽度                             | number            | - | |
+| setDownloadName   | 设置图片下载名称                         | (src: string) => string | - | 2.40.0 |
 
 ### ImagePreview
 
@@ -482,28 +526,30 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | zoomInTip         | 放大操作按钮提示                                                                                                                                         | string         | "放大" | |
 | zoomOutTip        | 缩小操作按钮提示                                                                                                                                         | string        | "缩小" | |
 | zoomStep          | 图片每次缩小/放大比例                                                                                                                                      | number        | 0.1 | |
+| setDownloadName   | 设置图片下载名称                         | (src: string) => string | - | 2.40.0 |
 
 ### MenuProps
 
-| 属性               | 说明                     | 类型    |
-|-------------------|--------------------------|--------|
-| curPage           | 当前图片页下标              | number |
-| disabledPrev      | 是否禁用向左切换按钮         | boolean |
-| disabledNext      | 是否禁用向右切换按钮         | boolean |
-| disableDownload   | 是否禁用下载按钮            | boolean |
-| max               | 图片缩放最大比例            | number |
-| min               | 图片缩放最小比例            | number |
-| onDownload        | 图片下载的调用函数           | () => void |
-| onZoomIn          | 图片放大时的调用函数         | () => void |
-| onZoomOut         | 图片缩小时的调用函数         | () => void |
-| onPrev            | 向前切换图片的调用函数       | () => void |
-| onNext            | 向后切换图片的调用函数       | () => void |
-| onRotateLeft      | 逆时针旋转图片的调用函数     | () => void |
-| onRotateRight     | 顺时针旋转图片的调用函数     | () => void |
-| ratio             | 原始尺寸或适应页面按钮状态  | "adaptation" \| "realSize"|
-| step              | 缩放的比例步长              | number |
-| totalNum          | 可预览的总图片数            | number |
-| zoom              | 当前图片缩放比例            | number |
+| 属性               | 说明                     | 类型    | 版本 |
+|-------------------|--------------------------|--------|-----|
+| curPage           | 当前图片页下标              | number | |
+| disabledPrev      | 是否禁用向左切换按钮         | boolean | |
+| disabledNext      | 是否禁用向右切换按钮         | boolean | |
+| disableDownload   | 是否禁用下载按钮            | boolean | |
+| max               | 图片缩放最大比例            | number | |
+| min               | 图片缩放最小比例            | number | |
+| onDownload        | 图片下载的调用函数           | () => void | |
+| onZoomIn          | 图片放大时的调用函数         | () => void | |
+| onZoomOut         | 图片缩小时的调用函数         | () => void | |
+| onPrev            | 向前切换图片的调用函数       | () => void | |
+| onNext            | 向后切换图片的调用函数       | () => void | |
+| onRotateLeft      | 逆时针旋转图片的调用函数     | () => void | |
+| onRotateRight     | 顺时针旋转图片的调用函数     | () => void | |
+| ratio             | 原始尺寸或适应页面按钮状态  | "adaptation" \| "realSize"| |
+| step              | 缩放的比例步长              | number | |
+| totalNum          | 可预览的总图片数            | number | |
+| zoom              | 当前图片缩放比例            | number | |
+| menuItems         | 默认底部预览操作区域功能按钮 ReactNode 数组 | ReactNode[] | 2.40.0 |
 
 ## 设计变量
 

+ 2 - 2
cypress/e2e/image.spec.js

@@ -57,12 +57,12 @@ describe('image', () => {
         // 切换到下一张图片
         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-footer-page').contains('2/3');
         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');
+        cy.get('.semi-image-preview-footer-page').contains('1/3');
     });
 
     // 测试鼠标拖拽图片

+ 8 - 4
packages/semi-foundation/image/image.scss

@@ -123,10 +123,14 @@ $module: #{$prefix}-image;
     &-footer {
         display: flex;
         align-items: center;
-        padding: $spacing-image_preview_footer-paddingY $spacing-image_preview_footer-paddingX;
-        background: $color-image_preview_footer-bg;
-        border-radius: $radius-image_preview_footer;
-        height: $height-image_preview_footer;
+
+        &-content {
+            padding: $spacing-image_preview_footer-paddingY $spacing-image_preview_footer-paddingX;
+            background: $color-image_preview_footer-bg;
+            border-radius: $radius-image_preview_footer;
+            height: $height-image_preview_footer;
+        }
+        
 
         &-wrapper {
             position: absolute;

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

@@ -21,7 +21,8 @@ export interface PreviewInnerAdapter<P = Record<string, any>, S = Record<string,
     setStartMouseDown: (x: number, y: number) => void;
     setMouseActiveTime: (time: number) => void;
     disabledBodyScroll: () => void;
-    enabledBodyScroll: () => void
+    enabledBodyScroll: () => void;
+    getSetDownloadFunc: () => (src: string) => string
 }
 
 
@@ -129,8 +130,9 @@ export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<
 
     handleDownload = () => {
         const { currentIndex, imgSrc } = this.getStates();
+        const setDownloadName = this._adapter.getSetDownloadFunc();
         const downloadSrc = imgSrc[currentIndex];
-        const downloadName = downloadSrc.slice(downloadSrc.lastIndexOf("/") + 1);
+        const downloadName = setDownloadName ? setDownloadName(downloadSrc) : downloadSrc.slice(downloadSrc.lastIndexOf("/") + 1);
         downloadImage(downloadSrc, downloadName);
         this._adapter.notifyDownload(downloadSrc, currentIndex);
     }

+ 73 - 2
packages/semi-ui/image/_story/image.stories.jsx

@@ -7,7 +7,9 @@ import {
     Col,
     Icon,
     Switch,
-    Input
+    Input,
+    Divider,
+    Tooltip
 } from "../../index";
 import { 
     IconChevronLeft, 
@@ -19,6 +21,7 @@ import {
     IconWindowAdaptionStroked,
     IconRealSizeStroked,
     IconUploadError,
+    IconInfoCircle
 } from "@douyinfe/semi-icons";
 
 export default {
@@ -445,7 +448,6 @@ export const customRenderFooterMenu = () => {
             style={{ 
                 background: "grey", 
                 height: 40, 
-                width: 280, 
                 display: "flex",
                 alignItems: "center",
                 justifyContent: "space-around",
@@ -514,6 +516,34 @@ export const customRenderFooterMenu = () => {
     );
 }
 
+export const customRenderFooterMenuByNode = () => {
+    const renderPreviewMenu = useCallback((props) => {
+        const { menuItems } = props;
+        const customNode = <Tooltip content='我是一个自定义操作'><IconInfoCircle size="large" /></Tooltip>;
+        return (
+            <div style={{ display: 'flex', backgroundColor: 'rgba(0, 0, 0, 0.75)', alignItems: 'center', padding: '5px 16px', borderRadius: 4 }}>
+                {menuItems.slice(0, 3)}
+                <Divider layout="vertical" />
+                {menuItems.slice(3, 7)}
+                <Divider layout="vertical" />
+                {menuItems.slice(7)}
+                <Divider layout="vertical" />
+                {customNode}
+            </div>
+        );
+    }, []);
+
+    return (
+        <>  
+            <ImagePreview
+                renderPreviewMenu={renderPreviewMenu}
+            >
+                {srcList1.map((src, index) => (<Image key={index} src={src} width={200} alt={`lamp${index + 1}`} />))}
+            </ImagePreview>
+        </>
+    );
+}
+
 export const CustomRenderTitle = () => (
     <>  
         <ImagePreview
@@ -582,4 +612,45 @@ export const issue1526 = () => {
             ))}
         </ImagePreview>
     )
+}
+
+export const SetDownloadName = () => {
+    const srcList = useMemo(() => ([
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg?timestap=1",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/sky.jpg?timestap=1",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/greenleaf.jpg?timestap=1",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/colorful.jpg?timestap=1",
+    ]), []);
+
+    const setDownloadName = (src) => {
+        let newSrc = src.slice(src.lastIndexOf("/") + 1);
+        newSrc = newSrc.slice(0, newSrc.indexOf('?'));
+        return newSrc;
+    }
+   return  (
+    <>
+        <Image 
+            width={360}
+            height={200}
+            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg?test"
+            setDownloadName={setDownloadName}
+        />
+        <br/>
+        <br />
+        <ImagePreview
+            setDownloadName={setDownloadName}
+        >
+            {srcList.map((src, index) => {
+                return (
+                    <Image 
+                        key={index} 
+                        src={src} 
+                        width={200} 
+                        alt={`lamp${index + 1}`} 
+                        style={{ marginRight: 5 }}
+                    />
+                );
+            })}
+        </ImagePreview>
+    </>);
 }

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

@@ -173,7 +173,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
 
     render() {
         const { src, loadStatus, previewVisible } = this.state;
-        const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, fallback, placeholder, imageID, ...restProps } = this.props;
+        const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, fallback, placeholder, imageID, setDownloadName, ...restProps } = this.props;
         const outerStyle = Object.assign({ width, height }, style);
         const outerCls = cls(prefixCls, className);
         const canPreview = loadStatus === "success" && preview && !this.isInGroup();
@@ -210,6 +210,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
                         visible={previewVisible}
                         onVisibleChange={this.handlePreviewVisibleChange}
                         crossOrigin={!isUndefined(crossOrigin) ? crossOrigin : previewProps?.crossOrigin}
+                        setDownloadName={setDownloadName}
                     />
                 }
             </div>

+ 4 - 2
packages/semi-ui/image/interface.tsx

@@ -23,7 +23,8 @@ export interface ImageProps extends BaseProps{
     onLoad?: (event: Event) => void;
     crossOrigin?: "anonymous"| "use-credentials";
     children?: ReactNode;
-    imageID?: number
+    imageID?: number;
+    setDownloadName?: (src: string) => string
 }
 
 export interface PreviewProps extends BaseProps {
@@ -68,7 +69,8 @@ export interface PreviewProps extends BaseProps {
     onNext?: (index: number) => void;
     onRatioChange?: (type: RatioType) => void;
     onRotateLeft?: (angle: number) => void;
-    onDownload?: (src: string, index: number) => void
+    onDownload?: (src: string, index: number) => void;
+    setDownloadName?: (src: string) => string
 }
 
 export interface MenuProps {

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

@@ -192,7 +192,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
     };
 
     render() {
-        const { src, className, style, lazyLoad, ...restProps } = this.props;
+        const { src, className, style, lazyLoad, setDownloadName, ...restProps } = this.props;
         const { currentIndex, visible } = this.state;
         const { srcListInChildren, newChildren, titles } = this.loopImageIndex();
         const srcArr = Array.isArray(src) ? src : (typeof src === "string" ? [src] : []);
@@ -209,6 +209,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
                     previewObserver: this.previewObserver,
                     setCurrentIndex: this.handleCurrentIndexChange,
                     handleVisibleChange: this.handleVisibleChange,
+                    setDownloadName: setDownloadName,
                 }}
             >
                 <div id={this.previewGroupId} style={style} className={cls(`${prefixCls}-preview-group`, className)}>

+ 2 - 1
packages/semi-ui/image/previewContext.tsx

@@ -9,7 +9,8 @@ export interface PreviewContextProps {
     visible: boolean;
     previewObserver: IntersectionObserver;
     setCurrentIndex: (current: number) => void;
-    handleVisibleChange: (visible: boolean, preVisible?: boolean) => void
+    handleVisibleChange: (visible: boolean, preVisible?: boolean) => void;
+    setDownloadName: (src: string) => string
 }
 
 export const PreviewContext = createContext<PreviewContextProps>({} as any);

+ 71 - 52
packages/semi-ui/image/previewFooter.tsx

@@ -113,16 +113,17 @@ export default class Footer extends BaseComponent<FooterProps> {
             onRatioClick: this.handleRatioClick,
             onZoomIn: this.handlePlusClick,
             onZoomOut: this.handleMinusClick,
+            menuItems: this.getMenu()
         };
         return renderPreviewMenu(props);
     }
 
     // According to showTooltip in props, decide whether to use Tooltip to pack a layer
     // 根据 props 中的 showTooltip 决定是否使用 Tooltip 包一层
-    getFinalIconElement = (element: ReactNode, content: ReactNode) => {
+    getFinalIconElement = (element: ReactNode, content: ReactNode, key: string) => {
         const { showTooltip } = this.props;
         return showTooltip ? (
-            <Tooltip content={content}>
+            <Tooltip content={content} key={`tooltip-${key}`}>
                 {element}
             </Tooltip>
         ): element;
@@ -137,52 +138,57 @@ export default class Footer extends BaseComponent<FooterProps> {
     getIconChevronLeft = () => {
         const { disabledPrev, onPrev, prevTip } = this.props;
         const icon = <IconChevronLeft
+            key="chevron-left"
             size="large"
             className={disabledPrev ? `${footerPrefixCls}-disabled` : ""}
             onClick={!disabledPrev ? onPrev : undefined}
         />;
         const content = prevTip ?? this.getLocalTextByKey("prevTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'chevron-left');
     }
 
     getIconChevronRight = () => {
         const { disabledNext, onNext, nextTip } = this.props;
         const icon = <IconChevronRight
+            key="chevron-right"
             size="large"
             className={disabledNext ? `${footerPrefixCls}-disabled` : ""}
             onClick={!disabledNext ? onNext : undefined}
         />;
         const content = nextTip ?? this.getLocalTextByKey("nextTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'chevron-right');
     }
 
     getIconMinus = () => {
         const { zoomOutTip, zoom, min } = this.props;
         const disabledZoomOut = zoom === min;
-        const icon = <IconMinus 
+        const icon = <IconMinus
+            key="minus"
             size="large" 
             onClick={!disabledZoomOut ? this.handleMinusClick : undefined} 
             className={disabledZoomOut ? `${footerPrefixCls}-disabled` : ""}
         />;
         const content = zoomOutTip ?? this.getLocalTextByKey("zoomOutTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'minus');
     }
 
     getIconPlus = () => {
         const { zoomInTip, zoom, max } = this.props;
         const disabledZoomIn = zoom === max;
-        const icon = <IconPlus 
+        const icon = <IconPlus
+            key="plus"
             size="large" 
             onClick={!disabledZoomIn ? this.handlePlusClick : undefined}  
             className={disabledZoomIn ? `${footerPrefixCls}-disabled` : ""}
         />;
         const content = zoomInTip ?? this.getLocalTextByKey("zoomInTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'plus');
     }
 
     getIconRatio = () => {
         const { ratio, originTip, adaptiveTip } = this.props;
         const props = {
+            key: "ratio",
             size: "large" as IconSize,
             className: cls(`${footerPrefixCls}-gap`),
             onClick: this.handleRatioClick,
@@ -194,22 +200,24 @@ export default class Footer extends BaseComponent<FooterProps> {
         } else {
             content = adaptiveTip ?? this.getLocalTextByKey("adaptiveTip");
         }
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'ratio');
     }
 
     getIconRotate = () => {
         const { rotateTip } = this.props;
         const icon = <IconRotate
+            key="rotate"
             size="large"
             onClick={this.handleRotateLeft}
         />;
         const content = rotateTip ?? this.getLocalTextByKey("rotateTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'rotate');
     }
 
     getIconDownload = () => {
         const { downloadTip, onDownload, disableDownload } = this.props;
         const icon = <IconDownload
+            key='download'
             size="large"
             onClick={!disableDownload ? onDownload : undefined}
             className={cls(`${footerPrefixCls}-gap`,
@@ -219,54 +227,65 @@ export default class Footer extends BaseComponent<FooterProps> {
             )}
         />;
         const content = downloadTip ?? this.getLocalTextByKey("downloadTip");
-        return this.getFinalIconElement(icon, content);
+        return this.getFinalIconElement(icon, content, 'download');
     }
 
+    getNumberInfo = () => {
+        const { curPage, totalNum } = this.props;
+        return (
+            <div className={`${footerPrefixCls}-page`} key={'info'} >
+                {curPage}/{totalNum}
+            </div>
+        );
+    }
+
+    getSlider = () => {
+        const { zoom, min, max, step, showTooltip } = this.props;
+        return (
+            <Slider
+                key={'slider'}
+                value={zoom}
+                min={min}
+                max={max}
+                step={step}
+                tipFormatter={(v): string => `${v}%`}
+                tooltipVisible={showTooltip ? undefined : false }
+                onChange={this.handleSlideChange}
+            />
+        );
+    }
+
+    getMenu = () => ([
+        this.getIconChevronLeft(),
+        this.getNumberInfo(),
+        this.getIconChevronRight(),
+        this.getIconMinus(),
+        this.getSlider(),
+        this.getIconPlus(),
+        this.getIconRatio(),
+        this.getIconRotate(),
+        this.getIconDownload()
+    ]);
+
+    getFooterMenu = () => {
+        const menuItems = this.getMenu();
+        menuItems.splice(3, 0, <Divider layout="vertical" key={"divider-first"}/>);
+        menuItems.splice(8, 0, <Divider layout="vertical" key={"divider-second"} />);
+        return menuItems;
+    }
 
     render() {
-        const { 
-            min, 
-            max,
-            step,
-            curPage,
-            totalNum,
-            zoom,
-            showTooltip,
-            className,
-            renderPreviewMenu,
-        } = this.props;
-
-        if (renderPreviewMenu) {
-            return (
-                <div className={`${footerPrefixCls}-wrapper`}>
-                    {this.customRenderViewMenu()}
-                </div>
-            ); 
-        }
+        const { className, renderPreviewMenu } = this.props;
+
+        const menuCls = cls(footerPrefixCls, `${footerPrefixCls}-wrapper`, className,
+            {
+                [`${footerPrefixCls}-content`]: !Boolean(renderPreviewMenu),
+            },
+        );
 
         return (
-            <section className={cls(footerPrefixCls, `${footerPrefixCls}-wrapper`, className)}>
-                {this.getIconChevronLeft()}
-                <div className={`${footerPrefixCls}-page`}>
-                    <span>{curPage}</span><span>/</span><span>{totalNum}</span>
-                </div>
-                {this.getIconChevronRight()}
-                <Divider layout="vertical" />
-                {this.getIconMinus()}
-                <Slider
-                    value={zoom}
-                    min={min}
-                    max={max}
-                    step={step}
-                    tipFormatter={(v): string => `${v}%`}
-                    tooltipVisible={showTooltip ? undefined : false }
-                    onChange={this.handleSlideChange}
-                />
-                {this.getIconPlus()}
-                {this.getIconRatio()}
-                <Divider layout="vertical" />
-                {this.getIconRotate()}
-                {this.getIconDownload()}
+            <section className={menuCls} >
+                {renderPreviewMenu ? this.customRenderViewMenu() : this.getFooterMenu()}
             </section>
         );
     }

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

@@ -164,6 +164,9 @@ export default class PreviewInner extends BaseComponent<PreviewInnerProps, Previ
             setMouseActiveTime: (time: number) => {
                 mouseActiveTime = time;
             },
+            getSetDownloadFunc: () => {
+                return this.context?.setDownloadName ?? this.props.setDownloadName;
+            }
         };
 
     }

+ 8 - 8
packages/semi-ui/upload/_story/upload.stories.jsx

@@ -963,7 +963,7 @@ export const TestReplaceFunc = () => (
       {...commonProps}
       action={action}
       accept=".md,image/*,video/*"
-      maxSize={mb1}
+      maxSize={1024}
       minSize={0}
       transformFile={(fileInstance)=>{return fileInstance;}}
     >
@@ -975,7 +975,7 @@ export const TestReplaceFunc = () => (
       {...commonProps}
       action={action}
       accept="image/*"
-      maxSize={mb1}
+      maxSize={1024}
       minSize={0}
       transformFile={(fileInstance)=>{return fileInstance;}}
     >
@@ -987,8 +987,8 @@ export const TestReplaceFunc = () => (
       {...commonProps}
       action={action}
       accept=".md,image/*,video/*"
-      maxSize={mb1}
-      minSize={kb2}
+      maxSize={1024}
+      minSize={200}
       transformFile={(fileInstance)=>{return fileInstance;}}
     >
       <Button icon={<IconUpload />} theme="light">
@@ -1046,7 +1046,7 @@ class InsertUpload extends React.Component {
                     onSuccess={(...v) => console.log(...v)}
                     onError={(...v) => console.log(...v)}
                     onFileChange={this.onFileChange}
-                    maxSize={mb1}
+                    maxSize={1024}
                     minSize={0}
                     limit={1}
                     transformFile={(fileInstance)=>{return fileInstance;}}
@@ -1062,7 +1062,7 @@ class InsertUpload extends React.Component {
                     onSuccess={(...v) => console.log(...v)}
                     onError={(...v) => console.log(...v)}
                     onFileChange={this.onFileChange}
-                    maxSize={mb1}
+                    maxSize={1024}
                     minSize={0}
                     limit={2}
                     transformFile={(fileInstance)=>{return fileInstance;}}
@@ -1079,8 +1079,8 @@ class InsertUpload extends React.Component {
                     onSuccess={(...v) => console.log(...v)}
                     onError={(...v) => console.log(...v)}
                     onFileChange={this.onFileChange}
-                    maxSize={mb1}
-                    minSize={kb2}
+                    maxSize={1024}
+                    minSize={200}
                     limit={1}
                     transformFile={(fileInstance)=>{return fileInstance;}}
                 >