浏览代码

feat: [Upload]support insert api and showPicInfo (#342)

* feat: [Upload]support insert api and showPicInfo

* feat: add en-US comment and version require
Neptune 3 年之前
父节点
当前提交
75962a325c

+ 39 - 6
content/input/upload/index-en-US.md

@@ -637,6 +637,36 @@ import { IconPlus } from '@douyinfe/semi-icons';
 };
 };
 ```
 ```
 
 
+Set `showPicInfo`, you can view the basic information of the picture
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload } from '@douyinfe/semi-ui';
+import { IconPlus } from '@douyinfe/semi-icons';
+
+() => {
+    let action = '//semi.design/api/upload';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    return (
+        <>
+            <Upload action={action} listType="picture" showPicInfo accept="image/*" multiple defaultFileList={defaultFileList}>
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
 ### Disabled
 ### Disabled
 
 
 ```jsx live=true width=48%
 ```jsx live=true width=48%
@@ -1102,7 +1132,10 @@ import { IconUpload } from '@douyinfe/semi-icons';
 |prompt | Custom slot, which can be used to insert prompt text. Different from writing directly in `children`, the content of `prompt` will not trigger upload when clicked.<br/>(In the picture wall mode, the incoming prompt is only supported after v1.3.0) | ReactNode | | |
 |prompt | Custom slot, which can be used to insert prompt text. Different from writing directly in `children`, the content of `prompt` will not trigger upload when clicked.<br/>(In the picture wall mode, the incoming prompt is only supported after v1.3.0) | ReactNode | | |
 |promptPosition | The position of the prompt text. When the listType is list, the reference object is the children element; when the listType is picture, the reference object is the picture list. Optional values ​​`left`, `right`, `bottom`<br/> (In picture wall mode, promptPosition is only supported after v1.3.0) | string |'right' | |
 |promptPosition | The position of the prompt text. When the listType is list, the reference object is the children element; when the listType is picture, the reference object is the picture list. Optional values ​​`left`, `right`, `bottom`<br/> (In picture wall mode, promptPosition is only supported after v1.3.0) | string |'right' | |
 |renderFileItem | Custom rendering of fileCard | (renderProps: RenderFileItemProps) => ReactNode | | 1.0.0 |
 |renderFileItem | Custom rendering of fileCard | (renderProps: RenderFileItemProps) => ReactNode | | 1.0.0 |
+|renderPicInfo| Custom photo wall information, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
+|renderThumbnail| Custom picture wall thumb, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |showClear | When limit is not 1 and the current number of uploaded files is greater than 1, whether to show the clear button | boolean | true | 1.0.0 |
 |showClear | When limit is not 1 and the current number of uploaded files is greater than 1, whether to show the clear button | boolean | true | 1.0.0 |
+|showPicInfo| Whether to display picture information, only valid in photo wall mode | boolean| false | 2.2.0 |
 |showReplace | When the upload is successful, whether to display the replace button inside the fileCard | boolean | false | 1.21.0 |
 |showReplace | When the upload is successful, whether to display the replace button inside the fileCard | boolean | false | 1.21.0 |
 |showRetry | When uploading fails, whether to display the retry button inside the fileCard | boolean | true | 1.0.0 |
 |showRetry | When uploading fails, whether to display the retry button inside the fileCard | boolean | true | 1.0.0 |
 |showUploadList | Whether to display the file list | boolean | true | |
 |showUploadList | Whether to display the file list | boolean | true | |
@@ -1141,6 +1174,12 @@ interface FileItem {
 }
 }
 ```
 ```
 
 
+## Methods
+|Name | Description | Type | Version|
+|----|----|----|----|
+| insert | Upload file, when index is passed, it will be inserted at the specified position, if not passed, it will be inserted at the end | (files: Array<File\>, index?: number) => void | 2.2.0 |
+| upload | Start upload manually, use with uploadTrigger="custom" | () => void | |
+
 ## Design Tokens
 ## Design Tokens
 <DesignToken/>
 <DesignToken/>
 
 
@@ -1156,9 +1195,3 @@ interface FileItem {
     - If you set `accept`, you can try to remove the accept attribute, and then see if the modified method is called. After removing it, the method is called to explain that the file type obtained by accept in the current environment does not match the set accept, and the upload behavior is terminated early. You can make a breakpoint to upload/foundation.js checkFileFormat function to see if the actual value of file.type obtained meets expectations.
     - If you set `accept`, you can try to remove the accept attribute, and then see if the modified method is called. After removing it, the method is called to explain that the file type obtained by accept in the current environment does not match the set accept, and the upload behavior is terminated early. You can make a breakpoint to upload/foundation.js checkFileFormat function to see if the actual value of file.type obtained meets expectations.
 
 
 <Notice title={"About the progress bar"}>The progress bar indicates the upload progress. The upload progress is divided into two parts: data upload and server return. If all the data has been sent, but the server does not return a response, the progress bar will stay at 90%. The user upload is not completed. At this time, the request in the developer tool will be pending, which is normal. </Notice>
 <Notice title={"About the progress bar"}>The progress bar indicates the upload progress. The upload progress is divided into two parts: data upload and server return. If all the data has been sent, but the server does not return a response, the progress bar will stay at 90%. The user upload is not completed. At this time, the request in the developer tool will be pending, which is normal. </Notice>
-
-<!-- ## Related Material
-
-```material
-82
-``` -->

+ 38 - 24
content/input/upload/index.md

@@ -624,6 +624,36 @@ import { IconPlus } from '@douyinfe/semi-icons';
 };
 };
 ```
 ```
 
 
+设置 `showPicInfo`,可以查看图片基础信息
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload } from '@douyinfe/semi-ui';
+import { IconPlus } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    return (
+        <>
+            <Upload action={action} listType="picture" showPicInfo accept="image/*" multiple defaultFileList={defaultFileList}>
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
 ### 禁用
 ### 禁用
 
 
 ```jsx live=true width=48%
 ```jsx live=true width=48%
@@ -1089,7 +1119,10 @@ import { IconUpload } from '@douyinfe/semi-icons';
 |prompt | 自定义插槽,可用于插入提示文本。与直接在 `children` 中写的区别时,`prompt` 的内容在点击时不会触发上传<br/>(图片墙模式下,v1.3.0 后才支持传入 prompt) | ReactNode |  |  |
 |prompt | 自定义插槽,可用于插入提示文本。与直接在 `children` 中写的区别时,`prompt` 的内容在点击时不会触发上传<br/>(图片墙模式下,v1.3.0 后才支持传入 prompt) | ReactNode |  |  |
 |promptPosition | 提示文本的位置,当 listType 为 list 时,参照物为 children 元素;当 listType 为 picture 时,参照物为图片列表。可选值 `left`、`right`、`bottom`<br/>(图片墙模式下,v1.3.0 后才支持使用 promptPosition) | string | 'right' |  |
 |promptPosition | 提示文本的位置,当 listType 为 list 时,参照物为 children 元素;当 listType 为 picture 时,参照物为图片列表。可选值 `left`、`right`、`bottom`<br/>(图片墙模式下,v1.3.0 后才支持使用 promptPosition) | string | 'right' |  |
 |renderFileItem | fileCard 的自定义渲染 | (renderProps: RenderFileItemProps) => ReactNode |  | 1.0.0 |
 |renderFileItem | fileCard 的自定义渲染 | (renderProps: RenderFileItemProps) => ReactNode |  | 1.0.0 |
+|renderPicInfo| 自定义照片墙信息,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
+|renderThumbnail| 自定义图片墙缩略图,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |showClear | 在 limit 不为 1 且当前已上传文件数大于 1 时,是否展示清空按钮 | boolean | true | 1.0.0 |
 |showClear | 在 limit 不为 1 且当前已上传文件数大于 1 时,是否展示清空按钮 | boolean | true | 1.0.0 |
+|showPicInfo| 是否显示图片信息,只在照片墙模式下有效| boolean| false | 2.2.0 |
 |showReplace | 上传成功时,是否展示在 fileCard 内部展示替换按钮 | boolean | false | 1.21.0 |
 |showReplace | 上传成功时,是否展示在 fileCard 内部展示替换按钮 | boolean | false | 1.21.0 |
 |showRetry | 上传失败时,是否展示在 fileCard 内部展示重试按钮 | boolean | true | 1.0.0 |
 |showRetry | 上传失败时,是否展示在 fileCard 内部展示重试按钮 | boolean | true | 1.0.0 |
 |showUploadList | 是否显示文件列表 | boolean | true |  |
 |showUploadList | 是否显示文件列表 | boolean | true |  |
@@ -1128,23 +1161,11 @@ interface FileItem {
 }
 }
 ```
 ```
 
 
-### RenderFileItemProps Interface
-
-```ts
-interface RenderFileItemProps extends FileItem {
-    previewFile: (fileItem: FileItem) => ReactNode; // 自定义预览元素
-    listType: 'picture' | 'list'; // 文件列表展示类型
-    onRemove: () => void; // 移除
-    onRetry: () => void; // 重试
-    onReplace: () => void; // 替换文件
-    key: string; // Item key
-    showRetry: boolean; // 是否展示重试
-    showReplace: boolean; // 是否展示替换
-    style: CSSProperties; // 传入的itemStyle
-    disabled: boolean; // 是否禁用
-    onPreviewClick: () => void; // 点击预览
-}
-```
+## Methods
+|名称 | 描述 | 类型 | 版本 |
+|----|----|----|----|
+| insert | 上传文件,当index传入时,会插入到指定位置,不传则插入到最后 | (files: Array<File\>, index?: number) => void | 2.2.0 |
+| upload | 手动开始上传,配合uploadTrigger="custom"使用 | () => void | |
 
 
 ## 设计变量
 ## 设计变量
 <DesignToken/>
 <DesignToken/>
@@ -1164,10 +1185,3 @@ interface RenderFileItemProps extends FileItem {
     - 如果你设置了 `accept`,可以尝试把 accept 属性去掉,然后再看是否调用了改方法。去掉后调用了该方法说明,accept 在当前环境下获取的 file type 与设置的 accept 不符,上传行为提前终止。可以打个断点到 upload/foundation.js checkFileFormat 函数,看下获取的 file.type 真实值是否符合预期。
     - 如果你设置了 `accept`,可以尝试把 accept 属性去掉,然后再看是否调用了改方法。去掉后调用了该方法说明,accept 在当前环境下获取的 file type 与设置的 accept 不符,上传行为提前终止。可以打个断点到 upload/foundation.js checkFileFormat 函数,看下获取的 file.type 真实值是否符合预期。
 
 
 <Notice title={"关于进度条"}>进度条表示上传进度,上传进度分为数据上载和服务器返回两部分,如果数据已经全部发出,但是服务器没有返回响应,进度条会停留在90%提示用户上传并没有完成,此时开发者工具中请求会处于 pending, 这是正常现象。仅当服务器返回响应,上传流程才真正结束,上传进度会达到100%</Notice>
 <Notice title={"关于进度条"}>进度条表示上传进度,上传进度分为数据上载和服务器返回两部分,如果数据已经全部发出,但是服务器没有返回响应,进度条会停留在90%提示用户上传并没有完成,此时开发者工具中请求会处于 pending, 这是正常现象。仅当服务器返回响应,上传流程才真正结束,上传进度会达到100%</Notice>
-
-
-<!-- ## 相关物料
-
-```material
-82
-``` -->

+ 81 - 0
packages/semi-foundation/upload/foundation.ts

@@ -320,6 +320,87 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
         });
         });
     }
     }
 
 
+    // 插入多个文件到指定位置
+    // Insert files to the specified location
+    insertFileToList(files: Array<CustomFile>, index:number): void {
+        const { limit, transformFile, accept, uploadTrigger } = this.getProps();
+        const { fileList } = this.getStates();
+
+        const unAcceptFileList = [];
+
+        // 当次选中的文件
+        // current selected file
+        let currentFileList = Array.from(files);
+        if (typeof accept !== 'undefined') {
+            currentFileList = currentFileList.filter(item => {
+                const isValid = this.checkFileFormat(accept, item);
+                if (!isValid) {
+                    unAcceptFileList.push(item);
+                }
+                return isValid;
+            });
+            if (unAcceptFileList.length !== 0) {
+                this._adapter.notifyAcceptInvalid(unAcceptFileList);
+            }
+            if (currentFileList.length === 0) {
+                return;
+            }
+        }
+        currentFileList = currentFileList.map(file => {
+            if (!file.uid) {
+                file.uid = getUuidv4();
+            }
+
+            if (this.checkFileSize(file)) {
+                file._sizeInvalid = true;
+                file.status = FILE_STATUS_VALID_FAIL;
+                this._adapter.notifySizeError(file, fileList);
+            }
+
+            if (transformFile) {
+                file = transformFile(file);
+            }
+            return file;
+        });
+        const total = fileList.length + currentFileList.length;
+        if (typeof limit !== 'undefined') {
+            // 判断是否超出限制
+            // Determine whether the limit is exceeded
+            if (total > limit) {
+                if (limit === 1) {
+                    // 使用最后面的文件对当前文件进行替换
+                    // Use the last file to replace the current file
+                    currentFileList = currentFileList.slice(-1);
+                    this._adapter.notifyFileSelect(currentFileList);
+                    this._adapter.resetInput();
+                    this.replaceFileList(currentFileList);
+                    return;
+                }
+                // 如果超出了限制,则计算还能添加几个文件,将剩余的文件继续上传
+                // If the limit is exceeded, several files can be added to the calculation, and the remaining files will continue to be uploaded
+                const restNum = limit - fileList.length;
+                currentFileList = currentFileList.slice(0, restNum);
+                this._adapter.notifyExceed(currentFileList);
+            }
+        }
+
+        const fileItemList = currentFileList.map(file => this.buildFileItem(file, uploadTrigger));
+        const newFileList = fileList.slice();
+        if (typeof index !== 'undefined') {
+            newFileList.splice(index, 0, ...fileItemList);
+        } else {
+            newFileList.push(...fileItemList);
+        }
+
+        this._adapter.notifyFileSelect(currentFileList);
+        this._adapter.notifyChange({ fileList: newFileList, currentFile: null });
+        this._adapter.updateFileList(newFileList, () => {
+            if (uploadTrigger === TRIGGER_AUTO) {
+                this.startUpload(fileItemList);
+            }
+        });
+    }
+
     manualUpload(): void {
     manualUpload(): void {
         // find the list of files that have not been uploaded
         // find the list of files that have not been uploaded
         const waitToUploadFileList = this.getState('fileList').filter((item: BaseFileItem) => item.status === FILE_STATUS_WAIT_UPLOAD);
         const waitToUploadFileList = this.getState('fileList').filter((item: BaseFileItem) => item.status === FILE_STATUS_WAIT_UPLOAD);

+ 0 - 4
packages/semi-foundation/upload/rtl.scss

@@ -22,8 +22,6 @@ $module: #{$prefix}-upload;
         }
         }
 
 
         &-file-card {
         &-file-card {
-            margin-right: 0;
-            margin-left: $spacing-upload_picture_file_card-marginRight;
 
 
             &-info {
             &-info {
 
 
@@ -53,8 +51,6 @@ $module: #{$prefix}-upload;
         &-picture {
         &-picture {
 
 
             &-file-card {
             &-file-card {
-                margin-right: 0;
-                margin-left: $spacing-upload_picture_file_card-marginRight;
 
 
                 &-close {
                 &-close {
                     right: auto;
                     right: auto;

+ 31 - 8
packages/semi-foundation/upload/upload.scss

@@ -99,6 +99,8 @@ $module: #{$prefix}-upload;
             display: flex;
             display: flex;
             flex-wrap: wrap;
             flex-wrap: wrap;
             flex-shrink: 0;
             flex-shrink: 0;
+            gap: $spacing-upload_picture_file_card-gap;
+            margin-bottom: $spacing-upload_picture_file_card-marginBottom;
 
 
             p {
             p {
                 @include ver-center;
                 @include ver-center;
@@ -130,8 +132,6 @@ $module: #{$prefix}-upload;
         justify-content: space-between;
         justify-content: space-between;
         height: $height-upload_file_card;
         height: $height-upload_file_card;
         width: $width-upload_file_card;
         width: $width-upload_file_card;
-        margin-right: $spacing-tight;
-        margin-bottom: $spacing-tight;
         background-color: $color-upload_card-bg;
         background-color: $color-upload_card-bg;
         cursor: pointer;
         cursor: pointer;
 
 
@@ -292,21 +292,21 @@ $module: #{$prefix}-upload;
             flex-direction: column;
             flex-direction: column;
 
 
             .#{$module}-prompt {
             .#{$module}-prompt {
-                order: 2;
+                order: 1;
             }
             }
 
 
             .#{$module}-add {
             .#{$module}-add {
-                order: 1;
+                order: 0;
             }
             }
         }
         }
 
 
         &[x-prompt-pos="right"] {
         &[x-prompt-pos="right"] {
             .#{$module}-prompt {
             .#{$module}-prompt {
-                order: 2;
+                order: 1;
             }
             }
 
 
             .#{$module}-add {
             .#{$module}-add {
-                order: 1;
+                order: 0;
             }
             }
         }
         }
 
 
@@ -353,11 +353,14 @@ $module: #{$prefix}-upload;
         }
         }
 
 
         &-file-card {
         &-file-card {
+            display: flex;
+            align-items: center;
+            justify-content: center;
             height: $height-upload_file_pic_card;
             height: $height-upload_file_pic_card;
             width: $width-upload_file_pic_card;
             width: $width-upload_file_pic_card;
+            border-radius: $radius-upload_picture_file_card_img;
             position: relative;
             position: relative;
-            margin-right: $spacing-upload_picture_file_card-marginRight;
-            margin-bottom: $spacing-upload_picture_file_card-marginBottom;
+            overflow: hidden;
 
 
             img {
             img {
                 height: $width-upload_picture_file_card_img;
                 height: $width-upload_picture_file_card_img;
@@ -429,6 +432,22 @@ $module: #{$prefix}-upload;
                 color: $color-upload_replace-text;
                 color: $color-upload_replace-text;
                 transform: translate3D(-50%, -50%, 0);
                 transform: translate3D(-50%, -50%, 0);
             }
             }
+            &-pic-info {
+                display: inline-flex;
+                box-sizing: border-box;
+                justify-content: space-between;
+                align-items: center;
+                position: absolute;
+                width: 100%;
+                height: 24px;
+                padding: 0 10px;
+                bottom: 0;
+                left: 0;
+                color: $color-upload_picture_file_card_pic_info-text;
+                font-size: $font-upload_picture_file_card_pic_info-fontSize;
+                font-weight: $font-upload_picture_file_card_pic_info-fontWeight;
+                background: linear-gradient(0deg, rgba(22, 22, 26, 0.3) 0%, rgba(22, 22, 26, 0) 77.08%);
+            }
 
 
             &-icon-loading,
             &-icon-loading,
             &-icon-error {
             &-icon-error {
@@ -445,6 +464,10 @@ $module: #{$prefix}-upload;
             &-show-pointer {
             &-show-pointer {
                 cursor: pointer;
                 cursor: pointer;
             }
             }
+
+            &-error {
+                outline: 1px solid $color-upload_picture_file_card_error-border;
+            }
         }
         }
     }
     }
 
 

+ 5 - 1
packages/semi-foundation/upload/variables.scss

@@ -24,6 +24,8 @@ $color-upload_pic_add-bg-hover: var(--semi-color-fill-1); // 图片墙上传背
 $color-upload_pic_add-bg: var(--semi-color-fill-0); // 图片墙上传背景色 - 默认
 $color-upload_pic_add-bg: var(--semi-color-fill-0); // 图片墙上传背景色 - 默认
 $color-upload_pic_remove-bg: var(--semi-overlay-bg); // 图片墙上传移除图标颜色
 $color-upload_pic_remove-bg: var(--semi-overlay-bg); // 图片墙上传移除图标颜色
 $color-upload_picture_file_card_loading_error-icon: var(--semi-color-danger); // 图片墙上传移除图标颜色
 $color-upload_picture_file_card_loading_error-icon: var(--semi-color-danger); // 图片墙上传移除图标颜色
+$color-upload_picture_file_card_error-border: var(--semi-color-danger); // 图片墙上传移除图标颜色
+$color-upload_picture_file_card_pic_info-text: var(--semi-color-white); // 图片墙图片信息(序号)文字颜色
 $color-upload_preview-icon: var(--semi-color-text-2); // 上传文件卡片文本颜色
 $color-upload_preview-icon: var(--semi-color-text-2); // 上传文件卡片文本颜色
 $color-upload_retry-text: var(--semi-color-primary); // 上传文件卡片重新上传按钮文本颜色
 $color-upload_retry-text: var(--semi-color-primary); // 上传文件卡片重新上传按钮文本颜色
 $color-upload_replace-text: var(--semi-color-white); // 上传文件卡片重新替换按钮文本颜色
 $color-upload_replace-text: var(--semi-color-white); // 上传文件卡片重新替换按钮文本颜色
@@ -56,7 +58,7 @@ $spacing-upload_file_card_info_progress-marginTop: 4px; // 上传文件卡片进
 $spacing-upload_file_card_close-marginLeft: $spacing-tight; // 上传文件卡片删除按钮左侧外边距
 $spacing-upload_file_card_close-marginLeft: $spacing-tight; // 上传文件卡片删除按钮左侧外边距
 $spacing-upload_file_card_close-marginRight: $spacing-tight; // 上传文件卡片删除按钮右侧外边距
 $spacing-upload_file_card_close-marginRight: $spacing-tight; // 上传文件卡片删除按钮右侧外边距
 $spacing-upload_file_card_icon-marginRight: $spacing-super-tight; // 上传文件卡片图标右侧外边距
 $spacing-upload_file_card_icon-marginRight: $spacing-super-tight; // 上传文件卡片图标右侧外边距
-$spacing-upload_picture_file_card-marginRight: $spacing-tight; // 图片墙上传卡片右侧外边距
+$spacing-upload_picture_file_card-gap: $spacing-tight; // 图片墙卡片之间边距
 $spacing-upload_picture_file_card-marginBottom: $spacing-tight; // 图片墙上传卡片底部外边距
 $spacing-upload_picture_file_card-marginBottom: $spacing-tight; // 图片墙上传卡片底部外边距
 $spacing-upload_picture_file_card_close-top: 8px; // 图片墙上传卡片删除按钮顶部位置
 $spacing-upload_picture_file_card_close-top: 8px; // 图片墙上传卡片删除按钮顶部位置
 $spacing-upload_picture_file_card_close-right: 8px; // 图片墙上传卡片删除右侧位置
 $spacing-upload_picture_file_card_close-right: 8px; // 图片墙上传卡片删除右侧位置
@@ -79,3 +81,5 @@ $radius-upload_drag_area: var(--semi-border-radius-small); // 可拖拽上传拖
 $font-upload_file_card_info_name-fontWeight: $font-weight-bold; // 上传文件卡片文件名字重
 $font-upload_file_card_info_name-fontWeight: $font-weight-bold; // 上传文件卡片文件名字重
 $font-upload_file_card_info_size-fontWeight: $font-weight-regular; // 上传文件卡片文件尺寸字重
 $font-upload_file_card_info_size-fontWeight: $font-weight-regular; // 上传文件卡片文件尺寸字重
 $font-upload_drag_area_tips-fontWeight: 600; // 可拖拽上传提示文本字重
 $font-upload_drag_area_tips-fontWeight: 600; // 可拖拽上传提示文本字重
+$font-upload_picture_file_card_pic_info-fontSize: 12px; // 图片墙图片信息字体大小
+$font-upload_picture_file_card_pic_info-fontWeight: 600; // 图片墙图片信息文本字重

+ 1 - 1
packages/semi-scss-compile/src/index.ts

@@ -15,7 +15,7 @@ export interface Options {
 
 
 const compile = ({foundationPath, themePath, iconPath, outputPath, isMin = false}: Options) => {
 const compile = ({foundationPath, themePath, iconPath, outputPath, isMin = false}: Options) => {
     const scssMap = generateScssMap(foundationPath, themePath, iconPath);
     const scssMap = generateScssMap(foundationPath, themePath, iconPath);
-    const tempDir = writeFile(scssMap)
+    const tempDir = writeFile(scssMap);
     const result = compilerFromScssMap(path.join(tempDir, 'index.scss'), isMin);
     const result = compilerFromScssMap(path.join(tempDir, 'index.scss'), isMin);
     fs.outputFileSync(outputPath, result.css);
     fs.outputFileSync(outputPath, result.css);
 };
 };

+ 1 - 1
packages/semi-ui/modal/Modal.tsx

@@ -264,7 +264,7 @@ class Modal extends BaseComponent<ModalReactProps, ModalState> {
         if (!visible && !hidden) {
         if (!visible && !hidden) {
             this.foundation.toggleHidden(true, () => this.foundation.afterClose());
             this.foundation.toggleHidden(true, () => this.foundation.afterClose());
         } else if (visible && this.state.hidden) {
         } else if (visible && this.state.hidden) {
-            this.foundation.toggleHidden(false)
+            this.foundation.toggleHidden(false);
         }
         }
     }
     }
 
 

+ 1 - 1
packages/semi-ui/popover/Arrow.tsx

@@ -55,7 +55,7 @@ const Arrow: React.FC<ArrowProps> = (props = {}) => {
             <path d="M0 0L1 0C1 4, 2 5.5, 4 7.5S7,10 7,12S6 14.5, 4 16.5S1,20 1,24L0 24L0 0z" fill={bgColor} />
             <path d="M0 0L1 0C1 4, 2 5.5, 4 7.5S7,10 7,12S6 14.5, 4 16.5S1,20 1,24L0 24L0 0z" fill={bgColor} />
         </svg>
         </svg>
     );
     );
-}
+};
 
 
 Arrow.propTypes = {
 Arrow.propTypes = {
     position: PropTypes.string,
     position: PropTypes.string,

+ 50 - 1
packages/semi-ui/upload/__test__/upload.test.js

@@ -869,7 +869,7 @@ describe('Upload', () => {
             beforeClear: () => Promise.reject(),
             beforeClear: () => Promise.reject(),
             onChange: spyOnChangeReject,
             onChange: spyOnChangeReject,
             onClear: spyOnClearReject,
             onClear: spyOnClearReject,
-        })
+        });
 
 
         const clearBtn = upload.find(`.${BASE_CLASS_PREFIX}-upload-file-list-title-clear`).at(0);
         const clearBtn = upload.find(`.${BASE_CLASS_PREFIX}-upload-file-list-title-clear`).at(0);
         const clearBtnPass = uploadPass.find(`.${BASE_CLASS_PREFIX}-upload-file-list-title-clear`).at(0);
         const clearBtnPass = uploadPass.find(`.${BASE_CLASS_PREFIX}-upload-file-list-title-clear`).at(0);
@@ -893,4 +893,53 @@ describe('Upload', () => {
             expect(spyOnClearReject.callCount).toEqual(0);
             expect(spyOnClearReject.callCount).toEqual(0);
         });
         });
     });
     });
+
+    it('insert method', () => {
+        const props = {
+            defaultFileList: [],
+        };
+        const upload = getUpload(props);
+        const uploadInstance = upload.instance();
+
+        const file_0 = new File([new ArrayBuffer(1024)], 'chucknorris_0.png', { type: 'image/png' });
+        const file_1 = new File([new ArrayBuffer(1024)], 'chucknorris_1.png', { type: 'image/png' });
+        const file_2 = new File([new ArrayBuffer(1024)], 'chucknorris_2.png', { type: 'image/png' });
+
+        expect(uploadInstance instanceof Upload).toEqual(true);
+        expect(Object.prototype.hasOwnProperty.call(uploadInstance, 'insert')).toEqual(true);
+
+        /**
+         * test fileList state should be [] => [file_0] => [file_1, file_0] => [file_1, file_2, file_0]
+         */
+        upload.instance().insert([file_0]);
+        upload.instance().insert([file_1], 0);
+        upload.instance().insert([file_2], 1);
+
+        expect(Array.isArray(upload.state('fileList'))).toEqual(true);
+        expect(upload.state('fileList').length).toEqual(3);
+        expect(upload.state('fileList')[0].name).toEqual('chucknorris_1.png');
+        expect(upload.state('fileList')[1].name).toEqual('chucknorris_2.png');
+        expect(upload.state('fileList')[2].name).toEqual('chucknorris_0.png');
+    });
+
+    it('showPicInfo works', () => {
+        const props = {
+            listType: 'picture',
+            defaultFileList: [
+                {
+                    uid: '1',
+                    name: 'jiafang1.jpeg',
+                    status: 'success',
+                    size: '130kb',
+                    url: 'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg',
+                },
+            ],
+            showPicInfo: true,
+        };
+        const upload = getUpload(props);
+
+        expect(upload.exists(`.${BASE_CLASS_PREFIX}-upload`)).toEqual(true);
+        expect(upload.exists(`.${BASE_CLASS_PREFIX}-upload-file-list-main`)).toEqual(true);
+        expect(upload.exists(`.${BASE_CLASS_PREFIX}-upload-picture-file-card-pic-info`)).toEqual(true);
+    });
 });
 });

+ 110 - 95
packages/semi-ui/upload/fileCard.tsx

@@ -3,7 +3,7 @@ import cls from 'classnames';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/upload/constants';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/upload/constants';
 import { getFileSize } from '@douyinfe/semi-foundation/upload/utils';
 import { getFileSize } from '@douyinfe/semi-foundation/upload/utils';
-import { BaseFileItem } from '@douyinfe/semi-foundation/upload/foundation';
+import { IconAlertCircle, IconClose, IconFile, IconRefresh } from '@douyinfe/semi-icons';
 import LocaleConsumer from '../locale/localeConsumer';
 import LocaleConsumer from '../locale/localeConsumer';
 import { Locale } from '../locale/interface';
 import { Locale } from '../locale/interface';
 
 
@@ -13,7 +13,6 @@ import Tooltip from '../tooltip/index';
 import Spin from '../spin/index';
 import Spin from '../spin/index';
 import { isElement } from '../_base/reactUtils';
 import { isElement } from '../_base/reactUtils';
 import { RenderFileItemProps } from './interface';
 import { RenderFileItemProps } from './interface';
-import { IconAlertCircle, IconClose, IconFile, IconRefresh } from '@douyinfe/semi-icons';
 
 
 const prefixCls = cssClasses.PREFIX;
 const prefixCls = cssClasses.PREFIX;
 
 
@@ -69,6 +68,7 @@ class FileCard extends PureComponent<FileCardProps> {
         style: PropTypes.object,
         style: PropTypes.object,
         url: PropTypes.string,
         url: PropTypes.string,
         validateMessage: PropTypes.node,
         validateMessage: PropTypes.node,
+        index: PropTypes.number
     };
     };
 
 
     static defaultProps = {
     static defaultProps = {
@@ -92,10 +92,10 @@ class FileCard extends PureComponent<FileCardProps> {
         let content = null;
         let content = null;
         switch (true) {
         switch (true) {
             case typeof validateMessage === 'string' && status === strings.FILE_STATUS_VALIDATING:
             case typeof validateMessage === 'string' && status === strings.FILE_STATUS_VALIDATING:
-                content = (<><Spin size="small" wrapperClassName={`${prefixCls }-file-card-icon-loading`} />{validateMessage}</>);
+                content = (<><Spin size="small" wrapperClassName={`${prefixCls}-file-card-icon-loading`} />{validateMessage}</>);
                 break;
                 break;
             case typeof validateMessage === 'string':
             case typeof validateMessage === 'string':
-                content = (<><IconAlertCircle className={`${prefixCls }-file-card-icon-error`} />{validateMessage}</>);
+                content = (<><IconAlertCircle className={`${prefixCls}-file-card-icon-error`} />{validateMessage}</>);
                 break;
                 break;
             case isElement(validateMessage):
             case isElement(validateMessage):
                 content = validateMessage;
                 content = validateMessage;
@@ -111,10 +111,10 @@ class FileCard extends PureComponent<FileCardProps> {
         let icon = null;
         let icon = null;
         switch (true) {
         switch (true) {
             case validateMessage && status === strings.FILE_STATUS_VALIDATING:
             case validateMessage && status === strings.FILE_STATUS_VALIDATING:
-                icon = (<Spin size="small" wrapperClassName={`${prefixCls }-picture-file-card-icon-loading`} />);
+                icon = (<Spin size="small" wrapperClassName={`${prefixCls}-picture-file-card-icon-loading`} />);
                 break;
                 break;
             case validateMessage && (status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL):
             case validateMessage && (status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL):
-                icon = (<div className={`${prefixCls }-picture-file-card-icon-error`}><ErrorSvg /></div>);
+                icon = (<div className={`${prefixCls}-picture-file-card-icon-error`}><ErrorSvg /></div>);
                 break;
                 break;
             default:
             default:
                 break;
                 break;
@@ -123,41 +123,50 @@ class FileCard extends PureComponent<FileCardProps> {
     }
     }
 
 
     renderPic(locale: Locale['Upload']): ReactNode {
     renderPic(locale: Locale['Upload']): ReactNode {
-        const { url, percent, status, disabled, style, onPreviewClick } = this.props;
-        const filePicCardCls = cls({
-            [`${prefixCls }-picture-file-card`]: true,
-            [`${prefixCls }-picture-file-card-disabled`]: disabled,
-            [`${prefixCls }-picture-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
-        });
+        const { url, percent, status, disabled, style, onPreviewClick, showPicInfo, renderPicInfo, renderThumbnail, name, index } = this.props;
         const showProgress = status === strings.FILE_STATUS_UPLOADING && percent !== 100;
         const showProgress = status === strings.FILE_STATUS_UPLOADING && percent !== 100;
         const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && this.props.showRetry;
         const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && this.props.showRetry;
         const showReplace = status === strings.FILE_STATUS_SUCCESS && this.props.showReplace;
         const showReplace = status === strings.FILE_STATUS_SUCCESS && this.props.showReplace;
-        const closeCls = `${prefixCls }-picture-file-card-close`;
+        const filePicCardCls = cls({
+            [`${prefixCls}-picture-file-card`]: true,
+            [`${prefixCls}-picture-file-card-disabled`]: disabled,
+            [`${prefixCls}-picture-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
+            [`${prefixCls}-picture-file-card-error`]: status === strings.FILE_STATUS_UPLOAD_FAIL,
+            [`${prefixCls}-picture-file-card-uploading`]: showProgress
+        });
+        const closeCls = `${prefixCls}-picture-file-card-close`;
         const retry = (
         const retry = (
             <div
             <div
-                className={`${prefixCls }-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
-                <IconRefresh className={`${prefixCls }-picture-file-card-icon-retry`} />
+                className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
+                <IconRefresh className={`${prefixCls}-picture-file-card-icon-retry`} />
             </div>
             </div>
         );
         );
         const replace = (
         const replace = (
             <Tooltip trigger="hover" position="top" content={locale.replace} showArrow={false} spacing={4}>
             <Tooltip trigger="hover" position="top" content={locale.replace} showArrow={false} spacing={4}>
                 <div
                 <div
-                    className={`${prefixCls }-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
-                    <ReplaceSvg className={`${prefixCls }-picture-file-card-icon-replace`} />
+                    className={`${prefixCls}-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
+                    <ReplaceSvg className={`${prefixCls}-picture-file-card-icon-replace`} />
                 </div>
                 </div>
             </Tooltip>
             </Tooltip>
 
 
         );
         );
 
 
+        const picInfo = typeof renderPicInfo === 'function' ? renderPicInfo(this.props) : (
+            <div className={`${prefixCls }-picture-file-card-pic-info`}>{index + 1}</div>
+        );
+
+        const thumbnail = typeof renderThumbnail === 'function' ? renderThumbnail(this.props) : <img src={url} alt={`picture of ${name}`} />;
+
         return (
         return (
             <div className={filePicCardCls} style={style} onClick={onPreviewClick}>
             <div className={filePicCardCls} style={style} onClick={onPreviewClick}>
-                <img src={url} />
+                {thumbnail}
                 {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} /> : null}
                 {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} /> : null}
                 {showRetry ? retry : null}
                 {showRetry ? retry : null}
                 {showReplace && replace}
                 {showReplace && replace}
-                {disabled ? null : (
-                    <div className={closeCls} onClick={e => this.onRemove(e)}>
-                        <IconClose size="extra-small" />
+                {showPicInfo && picInfo}
+                {!disabled && (
+                    <div className={closeCls}>
+                        <IconClose size="extra-small" onClick={e => this.onRemove(e)} />
                     </div>
                     </div>
                 )}
                 )}
                 {this.renderPicValidateMsg()}
                 {this.renderPicValidateMsg()}
@@ -165,6 +174,77 @@ class FileCard extends PureComponent<FileCardProps> {
         );
         );
     }
     }
 
 
+    renderFile(locale: Locale["Upload"]) {
+        const { name, size, percent, url, showRetry: propsShowRetry, showReplace: propsShowReplace, preview, previewFile, status, style, onPreviewClick } = this.props;
+        const fileCardCls = cls({
+            [`${prefixCls}-file-card`]: true,
+            [`${prefixCls}-file-card-fail`]: status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL,
+            [`${prefixCls}-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
+        });
+        const previewCls = cls({
+            [`${prefixCls}-file-card-preview`]: true,
+            [`${prefixCls}-file-card-preview-placeholder`]: !preview || previewFile
+        });
+        const infoCls = `${prefixCls}-file-card-info`;
+        const closeCls = `${prefixCls}-file-card-close`;
+        const replaceCls = `${prefixCls}-file-card-replace`;
+        const showProgress = !(percent === 100 || typeof percent === 'undefined') && status === strings.FILE_STATUS_UPLOADING;
+        // only show retry when upload fail & showRetry is true, no need to show during validate fail
+        const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && propsShowRetry;
+        const showReplace = status === strings.FILE_STATUS_SUCCESS && propsShowReplace;
+        const fileSize = this.transSize(size);
+        let previewContent: ReactNode = preview ? (<img src={url} />) : (<IconFile size="large" />);
+        if (previewFile) {
+            previewContent = previewFile(this.props);
+        }
+        return (
+            <div className={fileCardCls} style={style} onClick={onPreviewClick}>
+                <div className={previewCls}>
+                    {previewContent}
+                </div>
+                <div className={`${infoCls}-main`}>
+                    <div className={`${infoCls}-main-text`}>
+                        <span className={`${infoCls}-name`}>
+                            {name}
+                        </span>
+                        <span>
+                            <span className={`${infoCls}-size`}>{fileSize}</span>
+                            {showReplace && (
+                                <Tooltip trigger="hover" position="top" showArrow={false} content={locale.replace}>
+                                    <IconButton
+                                        onClick={e => this.onReplace(e)}
+                                        type="tertiary"
+                                        theme="borderless"
+                                        size="small"
+                                        icon={<DirectorySvg />}
+                                        className={replaceCls}
+                                    />
+                                </Tooltip>
+                            )}
+
+                        </span>
+
+                    </div>
+                    {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} />) : null}
+                    <div className={`${infoCls}-main-control`}>
+                        <span className={`${infoCls}-validate-message`}>
+                            {this.renderValidateMessage()}
+                        </span>
+                        {showRetry ? <span className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
+                    </div>
+                </div>
+                <IconButton
+                    onClick={e => this.onRemove(e)}
+                    type="tertiary"
+                    icon={<IconClose />}
+                    theme="borderless"
+                    size="small"
+                    className={closeCls}
+                />
+            </div>
+        );
+    }
+
     onRemove(e: MouseEvent): void {
     onRemove(e: MouseEvent): void {
         e.stopPropagation();
         e.stopPropagation();
         this.props.onRemove(this.props, e);
         this.props.onRemove(this.props, e);
@@ -181,89 +261,24 @@ class FileCard extends PureComponent<FileCardProps> {
     }
     }
 
 
     render() {
     render() {
-        const { name, size, percent, url, listType, preview, previewFile, status, style, onPreviewClick } = this.props;
-        const fileCardCls = cls({
-            [`${prefixCls}-file-card`]: true,
-            [`${prefixCls}-file-card-fail`]: status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL,
-            [`${prefixCls}-file-card-show-pointer`]: typeof onPreviewClick !== 'undefined',
-        });
-        const previewCls = cls({
-            [`${prefixCls}-file-card-preview`]: true,
-            [`${prefixCls}-file-card-preview-placeholder`]: !preview || previewFile
-        });
-        const infoCls = `${prefixCls}-file-card-info`;
-        const closeCls = `${prefixCls}-file-card-close`;
-        const replaceCls = `${prefixCls}-file-card-replace`;
-        const showProgress = !(percent === 100 || typeof percent === 'undefined') && status === strings.FILE_STATUS_UPLOADING;
-        // only show retry when upload fail & showRetry is true, no need to show during validate fail
-        const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && this.props.showRetry;
-        const showReplace = status === strings.FILE_STATUS_SUCCESS && this.props.showReplace;
-
+        const { listType } = this.props;
         if (listType === strings.FILE_LIST_PIC) {
         if (listType === strings.FILE_LIST_PIC) {
             return (
             return (
                 <LocaleConsumer componentName="Upload">
                 <LocaleConsumer componentName="Upload">
-                    {(locale: Locale['Upload']): ReactNode => (this.renderPic(locale))}
+                    {(locale: Locale["Upload"]) => (this.renderPic(locale))}
                 </LocaleConsumer>
                 </LocaleConsumer>
             );
             );
         }
         }
 
 
-        const fileSize = this.transSize(size);
-        let previewContent: ReactNode = preview ? (<img src={url} />) : (<IconFile size="large" />);
-        if (previewFile) {
-            previewContent = previewFile(this.props);
+        if (listType === strings.FILE_LIST_DEFAULT) {
+            return (
+                <LocaleConsumer componentName="Upload">
+                    {(locale: Locale["Upload"]) => (this.renderFile(locale))}
+                </LocaleConsumer>
+            );
         }
         }
-        return (
-            <LocaleConsumer componentName="Upload">
-                {(locale: Locale['Upload']): ReactNode => (
-                    <div className={fileCardCls} style={style} onClick={onPreviewClick}>
-                        {/* <a target='_blank' href={url} className={infoCls} rel="noopener noreferrer"> */}
-                        <div className={previewCls}>
-                            {previewContent}
-                        </div>
-                        <div className={`${infoCls}-main`}>
-                            <div className={`${infoCls}-main-text`}>
-                                <span className={`${infoCls}-name`}>
-                                    {name}
-                                </span>
-                                <span>
-                                    <span className={`${infoCls}-size`}>{fileSize}</span>
-                                    {showReplace && (
-                                        <Tooltip trigger="hover" position="top" showArrow={false} content={locale.replace}>
-                                            <IconButton
-                                                onClick={(e): void => this.onReplace(e)}
-                                                type="tertiary"
-                                                theme="borderless"
-                                                size="small"
-                                                icon={<DirectorySvg />}
-                                                className={replaceCls}
-                                            />
-                                        </Tooltip>
-                                    )}
 
 
-                                </span>
-
-                            </div>
-                            {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} />) : null}
-                            <div className={`${infoCls}-main-control`}>
-                                <span className={`${infoCls}-validate-message`}>
-                                    {this.renderValidateMessage()}
-                                </span>
-                                {showRetry ? <span className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
-                            </div>
-                        </div>
-                        {/* </a> */}
-                        <IconButton
-                            onClick={(e): void => this.onRemove(e)}
-                            type="tertiary"
-                            icon={<IconClose />}
-                            theme="borderless"
-                            size="small"
-                            className={closeCls}
-                        />
-                    </div>
-                )}
-            </LocaleConsumer>
-        );
+        return null;
     }
     }
 }
 }
 
 

+ 147 - 53
packages/semi-ui/upload/index.tsx

@@ -3,7 +3,7 @@ import React, { ReactNode, CSSProperties, RefObject, ChangeEvent, DragEvent } fr
 import cls from 'classnames';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { noop } from 'lodash';
 import { noop } from 'lodash';
-import UploadFoundation, { BaseFileItem, UploadAdapter, BeforeUploadObjectResult, AfterUploadResult } from '@douyinfe/semi-foundation/upload/foundation';
+import UploadFoundation, { CustomFile, UploadAdapter, BeforeUploadObjectResult, AfterUploadResult } from '@douyinfe/semi-foundation/upload/foundation';
 import { strings, cssClasses } from '@douyinfe/semi-foundation/upload/constants';
 import { strings, cssClasses } from '@douyinfe/semi-foundation/upload/constants';
 import FileCard from './fileCard';
 import FileCard from './fileCard';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
@@ -64,7 +64,10 @@ export interface UploadProps {
     prompt?: ReactNode;
     prompt?: ReactNode;
     promptPosition?: PromptPositionType;
     promptPosition?: PromptPositionType;
     renderFileItem?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
     renderFileItem?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
+    renderPicInfo?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
+    renderThumbnail?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
     showClear?: boolean;
     showClear?: boolean;
+    showPicInfo?: boolean; // Show pic info in picture wall
     showReplace?: boolean; // Display replacement function
     showReplace?: boolean; // Display replacement function
     showRetry?: boolean;
     showRetry?: boolean;
     showUploadList?: boolean;
     showUploadList?: boolean;
@@ -133,7 +136,10 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         prompt: PropTypes.node,
         prompt: PropTypes.node,
         promptPosition: PropTypes.oneOf<UploadProps['promptPosition']>(strings.PROMPT_POSITION),
         promptPosition: PropTypes.oneOf<UploadProps['promptPosition']>(strings.PROMPT_POSITION),
         renderFileItem: PropTypes.func,
         renderFileItem: PropTypes.func,
+        renderPicInfo: PropTypes.func,
+        renderThumbnail: PropTypes.func,
         showClear: PropTypes.bool,
         showClear: PropTypes.bool,
+        showPicInfo: PropTypes.bool,
         showReplace: PropTypes.bool,
         showReplace: PropTypes.bool,
         showRetry: PropTypes.bool,
         showRetry: PropTypes.bool,
         showUploadList: PropTypes.bool, // whether to show fileList
         showUploadList: PropTypes.bool, // whether to show fileList
@@ -168,6 +174,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         onSuccess: noop,
         onSuccess: noop,
         promptPosition: 'right' as const,
         promptPosition: 'right' as const,
         showClear: true,
         showClear: true,
+        showPicInfo: false,
         showReplace: false,
         showReplace: false,
         showRetry: true,
         showRetry: true,
         showUploadList: true,
         showUploadList: true,
@@ -297,14 +304,29 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         this.foundation.handleRemove(fileItem);
         this.foundation.handleRemove(fileItem);
     };
     };
 
 
+    /**
+     * ref method
+     * insert files at index
+     * @param files Array<CustomFile>
+     * @param index number
+     * @returns 
+     */
+    insert = (files: Array<CustomFile>, index: number): void => {
+        return this.foundation.insertFileToList(files, index);
+    }
+
+    /**
+     * ref method
+     * manual upload by user
+     */
     upload = (): void => {
     upload = (): void => {
         const { fileList } = this.state;
         const { fileList } = this.state;
         this.foundation.startUpload(fileList);
         this.foundation.startUpload(fileList);
     };
     };
 
 
     renderFile = (file: FileItem, index: number, locale: Locale['Upload']): ReactNode => {
     renderFile = (file: FileItem, index: number, locale: Locale['Upload']): ReactNode => {
-        const { name, status, validateMessage, _sizeInvalid } = file;
-        const { previewFile, listType, itemStyle, showRetry, renderFileItem, disabled, onPreviewClick, showReplace } = this.props;
+        const { name, status, validateMessage, _sizeInvalid, uid } = file;
+        const { previewFile, listType, itemStyle, showRetry, showPicInfo, renderPicInfo, renderFileItem, renderThumbnail, disabled, onPreviewClick, showReplace } = this.props;
         const onRemove = (): void => this.remove(file);
         const onRemove = (): void => this.remove(file);
         const onRetry = (): void => {
         const onRetry = (): void => {
             this.foundation.retry(file);
             this.foundation.retry(file);
@@ -318,10 +340,14 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             listType,
             listType,
             onRemove,
             onRemove,
             onRetry,
             onRetry,
-            key: `${name}${index}`,
+            index,
+            key: uid || `${name}${index}`,
             showRetry: typeof file.showRetry !== 'undefined' ? file.showRetry : showRetry,
             showRetry: typeof file.showRetry !== 'undefined' ? file.showRetry : showRetry,
             style: itemStyle,
             style: itemStyle,
             disabled,
             disabled,
+            showPicInfo,
+            renderPicInfo,
+            renderThumbnail,
             showReplace: typeof file.showReplace !== 'undefined' ? file.showReplace : showReplace,
             showReplace: typeof file.showReplace !== 'undefined' ? file.showReplace : showReplace,
             onReplace,
             onReplace,
             onPreviewClick: typeof onPreviewClick !== 'undefined' ? (): void => this.foundation.handlePreviewClick(file) : undefined,
             onPreviewClick: typeof onPreviewClick !== 'undefined' ? (): void => this.foundation.handlePreviewClick(file) : undefined,
@@ -343,17 +369,54 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
     };
     };
 
 
     renderFileList = (): ReactNode => {
     renderFileList = (): ReactNode => {
-        const { showUploadList, listType, limit, disabled, children } = this.props;
-        const { fileList: stateFileList } = this.state;
+        const { listType } = this.props;
+        if (listType === strings.FILE_LIST_PIC) {
+            return this.renderFileListPic();
+        }
+
+        if (listType === strings.FILE_LIST_DEFAULT) {
+            return this.renderFileListDefault();
+        }
+
+        return null;
+    };
+
+    renderFileListPic = () => {
+        const { showUploadList, limit, disabled, children, draggable } = this.props;
+        const { fileList: stateFileList, dragAreaStatus } = this.state;
         const fileList = this.props.fileList || stateFileList;
         const fileList = this.props.fileList || stateFileList;
-        const isPicType = listType === strings.FILE_LIST_PIC;
-        const showAddTriggerInList = isPicType && (limit ? limit > fileList.length : true);
-        const uploadAddCls = cls(`${prefixCls }-add`, {
-            [`${prefixCls }-picture-add`]: isPicType,
-            [`${prefixCls}-picture-add-disabled`]: disabled
+        const showAddTriggerInList = limit ? limit > fileList.length : true;
+        const dragAreaBaseCls = `${prefixCls}-drag-area`;
+        const uploadAddCls = cls(`${prefixCls}-add`, {
+            [`${prefixCls}-picture-add`]: true,
+            [`${prefixCls}-picture-add-disabled`]: disabled,
         });
         });
+        const fileListCls = cls(`${prefixCls}-file-list`, {
+            [`${prefixCls}-picture-file-list`]: true,
+        });
+        const dragAreaCls = cls({
+            [`${dragAreaBaseCls}-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
+            [`${dragAreaBaseCls}-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL
+        });
+        const mainCls = `${prefixCls}-file-list-main`;
+        const addContentProps = {
+            className: uploadAddCls,
+            onClick: this.onClick,
+        };
+        const containerProps = {
+            className: fileListCls
+        };
+        const draggableProps = {
+            onDrop: this.onDrop,
+            onDragOver: this.onDragOver,
+            onDragLeave: this.onDragLeave,
+            onDragEnter: this.onDragEnter,
+        };
+        if (draggable) {
+            Object.assign(addContentProps, draggableProps, { className: cls(uploadAddCls, dragAreaCls) });
+        }
         const addContent = (
         const addContent = (
-            <div className={uploadAddCls} onClick={this.onClick}>
+            <div {...addContentProps}>
                 {children}
                 {children}
             </div>
             </div>
         );
         );
@@ -365,23 +428,46 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             return null;
             return null;
         }
         }
 
 
-        const fileListCls = cls(`${prefixCls }-file-list`, {
-            [`${prefixCls }-picture-file-list`]: isPicType,
-        });
-        const titleCls = `${prefixCls }-file-list-title`;
-        const mainCls = `${prefixCls }-file-list-main`;
-        const showTitle = limit !== 1 && fileList.length && listType !== strings.FILE_LIST_PIC;
+        return (
+            <LocaleConsumer componentName="Upload">
+                {(locale: Locale['Upload']) => (
+                    <div {...containerProps}>
+                        <div className={mainCls}>
+                            {fileList.map((file, index) => this.renderFile(file, index, locale))}
+                            {showAddTriggerInList ? addContent : null}
+                        </div>
+                    </div>
+                )}
+            </LocaleConsumer>
+        );
+    }
+
+    renderFileListDefault = () => {
+        const { showUploadList, limit, disabled } = this.props;
+        const { fileList: stateFileList } = this.state;
+        const fileList = this.props.fileList || stateFileList;
+        const fileListCls = cls(`${prefixCls}-file-list`);
+        const titleCls = `${prefixCls}-file-list-title`;
+        const mainCls = `${prefixCls}-file-list-main`;
+        const showTitle = limit !== 1 && fileList.length;
         const showClear = this.props.showClear && !disabled;
         const showClear = this.props.showClear && !disabled;
+        const containerProps = {
+            className: fileListCls
+        };
+
+        if (!showUploadList || !fileList.length) {
+            return null;
+        }
 
 
         return (
         return (
             <LocaleConsumer componentName="Upload">
             <LocaleConsumer componentName="Upload">
-                {(locale: Locale['Upload']): ReactNode => (
-                    <div className={fileListCls}>
+                {(locale: Locale['Upload']) => (
+                    <div {...containerProps}>
                         {showTitle ? (
                         {showTitle ? (
                             <div className={titleCls}>
                             <div className={titleCls}>
-                                <span className={`${titleCls }-choosen`}>{locale.selectedFiles}</span>
+                                <span className={`${titleCls}-choosen`}>{locale.selectedFiles}</span>
                                 {showClear ? (
                                 {showClear ? (
-                                    <span onClick={this.clear} className={`${titleCls }-clear`}>
+                                    <span onClick={this.clear} className={`${titleCls}-clear`}>
                                         {locale.clear}
                                         {locale.clear}
                                     </span>
                                     </span>
                                 ) : null}
                                 ) : null}
@@ -390,13 +476,12 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
 
 
                         <div className={mainCls}>
                         <div className={mainCls}>
                             {fileList.map((file, index) => this.renderFile(file, index, locale))}
                             {fileList.map((file, index) => this.renderFile(file, index, locale))}
-                            {showAddTriggerInList ? addContent : null}
                         </div>
                         </div>
                     </div>
                     </div>
                 )}
                 )}
             </LocaleConsumer>
             </LocaleConsumer>
         );
         );
-    };
+    }
 
 
     onDrop = (e: DragEvent<HTMLDivElement>): void => {
     onDrop = (e: DragEvent<HTMLDivElement>): void => {
         this.foundation.handleDrop(e);
         this.foundation.handleDrop(e);
@@ -415,14 +500,30 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         this.foundation.handleDragEnter(e);
         this.foundation.handleDragEnter(e);
     };
     };
 
 
+    renderAddContent = () => {
+        const { draggable, children, listType } = this.props;
+        const uploadAddCls = cls(`${prefixCls}-add`);
+        if (listType === strings.FILE_LIST_PIC) {
+            return null;
+        }
+        if (draggable) {
+            return this.renderDragArea();
+        }
+        return (
+            <div className={uploadAddCls} onClick={this.onClick}>
+                {children}
+            </div>
+        );
+    }
+
     renderDragArea = (): ReactNode => {
     renderDragArea = (): ReactNode => {
         const { dragAreaStatus } = this.state;
         const { dragAreaStatus } = this.state;
         const { children, dragIcon, dragMainText, dragSubText } = this.props;
         const { children, dragIcon, dragMainText, dragSubText } = this.props;
-        const dragAreaBaseCls = `${prefixCls }-drag-area`;
+        const dragAreaBaseCls = `${prefixCls}-drag-area`;
         const dragAreaCls = cls(dragAreaBaseCls, {
         const dragAreaCls = cls(dragAreaBaseCls, {
-            [`${dragAreaBaseCls }-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
-            [`${dragAreaBaseCls }-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL,
-            [`${dragAreaBaseCls }-custom`]: children,
+            [`${dragAreaBaseCls}-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
+            [`${dragAreaBaseCls}-illegal`]: dragAreaStatus === strings.DRAG_AREA_ILLEGAL,
+            [`${dragAreaBaseCls}-custom`]: children,
         });
         });
 
 
         return (
         return (
@@ -440,20 +541,20 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
                             children
                             children
                         ) : (
                         ) : (
                             <>
                             <>
-                                <div className={`${dragAreaBaseCls }-icon`}>
+                                <div className={`${dragAreaBaseCls}-icon`}>
                                     {dragIcon || <IconUpload size="extra-large" />}
                                     {dragIcon || <IconUpload size="extra-large" />}
                                 </div>
                                 </div>
-                                <div className={`${dragAreaBaseCls }-text`}>
-                                    <div className={`${dragAreaBaseCls }-main-text`}>
+                                <div className={`${dragAreaBaseCls}-text`}>
+                                    <div className={`${dragAreaBaseCls}-main-text`}>
                                         {dragMainText || locale.mainText}
                                         {dragMainText || locale.mainText}
                                     </div>
                                     </div>
-                                    <div className={`${dragAreaBaseCls }-sub-text`}>{dragSubText}</div>
-                                    <div className={`${dragAreaBaseCls }-tips`}>
+                                    <div className={`${dragAreaBaseCls}-sub-text`}>{dragSubText}</div>
+                                    <div className={`${dragAreaBaseCls}-tips`}>
                                         {dragAreaStatus === strings.DRAG_AREA_LEGAL && (
                                         {dragAreaStatus === strings.DRAG_AREA_LEGAL && (
-                                            <span className={`${dragAreaBaseCls }-tips-legal`}>{locale.legalTips}</span>
+                                            <span className={`${dragAreaBaseCls}-tips-legal`}>{locale.legalTips}</span>
                                         )}
                                         )}
                                         {dragAreaStatus === strings.DRAG_AREA_ILLEGAL && (
                                         {dragAreaStatus === strings.DRAG_AREA_ILLEGAL && (
-                                            <span className={`${dragAreaBaseCls }-tips-illegal`}>
+                                            <span className={`${dragAreaBaseCls}-tips-illegal`}>
                                                 {locale.illegalTips}
                                                 {locale.illegalTips}
                                             </span>
                                             </span>
                                         )}
                                         )}
@@ -485,27 +586,20 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             directory,
             directory,
         } = this.props;
         } = this.props;
         const uploadCls = cls(prefixCls, {
         const uploadCls = cls(prefixCls, {
-            [`${prefixCls }-picture`]: listType === strings.FILE_LIST_PIC,
-            [`${prefixCls }-disabled`]: disabled,
-            [`${prefixCls }-default`]: validateStatus === 'default',
-            [`${prefixCls }-error`]: validateStatus === 'error',
-            [`${prefixCls }-warning`]: validateStatus === 'warning',
-            [`${prefixCls }-success`]: validateStatus === 'success',
+            [`${prefixCls}-picture`]: listType === strings.FILE_LIST_PIC,
+            [`${prefixCls}-disabled`]: disabled,
+            [`${prefixCls}-default`]: validateStatus === 'default',
+            [`${prefixCls}-error`]: validateStatus === 'error',
+            [`${prefixCls}-warning`]: validateStatus === 'warning',
+            [`${prefixCls}-success`]: validateStatus === 'success',
         }, className);
         }, className);
-        const uploadAddCls = cls(`${prefixCls }-add`);
-        const inputCls = cls(`${prefixCls }-hidden-input`);
-        const inputReplaceCls = cls(`${prefixCls }-hidden-input-replace`);
-        const promptCls = cls(`${prefixCls }-prompt`);
-        const validateMsgCls = cls(`${prefixCls }-validate-message`);
+        const inputCls = cls(`${prefixCls}-hidden-input`);
+        const inputReplaceCls = cls(`${prefixCls}-hidden-input-replace`);
+        const promptCls = cls(`${prefixCls}-prompt`);
+        const validateMsgCls = cls(`${prefixCls}-validate-message`);
 
 
         const dirProps = directory ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {};
         const dirProps = directory ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {};
 
 
-        const addContent =
-            listType !== strings.FILE_LIST_PIC ? (
-                <div className={uploadAddCls} onClick={this.onClick}>
-                    {children}
-                </div>
-            ) : null;
         return (
         return (
             <div className={uploadCls} style={style} x-prompt-pos={promptPosition}>
             <div className={uploadCls} style={style} x-prompt-pos={promptPosition}>
                 <input
                 <input
@@ -532,7 +626,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
                     className={inputReplaceCls}
                     className={inputReplaceCls}
                     ref={this.replaceInputRef}
                     ref={this.replaceInputRef}
                 />
                 />
-                {draggable ? this.renderDragArea() : addContent}
+                {this.renderAddContent()}
                 {prompt ? <div className={promptCls}>{prompt}</div> : null}
                 {prompt ? <div className={promptCls}>{prompt}</div> : null}
 
 
                 {validateMessage ? <div className={validateMsgCls}>{validateMessage}</div> : null}
                 {validateMessage ? <div className={validateMsgCls}>{validateMessage}</div> : null}

+ 3 - 0
packages/semi-ui/upload/interface.ts

@@ -45,12 +45,15 @@ export interface FileItem extends BaseFileItem {
 }
 }
 
 
 export interface RenderFileItemProps extends FileItem {
 export interface RenderFileItemProps extends FileItem {
+    index?: number;
     previewFile?: (fileItem: RenderFileItemProps) => ReactNode;
     previewFile?: (fileItem: RenderFileItemProps) => ReactNode;
     listType: UploadListType;
     listType: UploadListType;
     onRemove: (props: RenderFileItemProps, e: MouseEvent) => void;
     onRemove: (props: RenderFileItemProps, e: MouseEvent) => void;
     onRetry: (props: RenderFileItemProps, e: MouseEvent) => void;
     onRetry: (props: RenderFileItemProps, e: MouseEvent) => void;
     onReplace: (props: RenderFileItemProps, e: MouseEvent) => void;
     onReplace: (props: RenderFileItemProps, e: MouseEvent) => void;
     key: string;
     key: string;
+    showPicInfo?: boolean;
+    renderPicInfo?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
     showRetry: boolean;
     showRetry: boolean;
     showReplace: boolean;
     showReplace: boolean;
     style?: CSSProperties;
     style?: CSSProperties;

+ 1 - 1
packages/semi-webpack/src/semi-theme-loader.ts

@@ -16,7 +16,7 @@ export default function SemiThemeLoader(source: string) {
     let componentVariables: string | boolean;
     let componentVariables: string | boolean;
     try {
     try {
         componentVariables = resolve.sync(this.context, `${theme}/scss/local.scss`);
         componentVariables = resolve.sync(this.context, `${theme}/scss/local.scss`);
-    } catch(e) {}
+    } catch (e) {}
 
 
     if (query.include || query.variables || componentVariables) {
     if (query.include || query.variables || componentVariables) {
         let localImport = '';
         let localImport = '';

+ 2 - 2
packages/semi-webpack/src/semi-webpack-plugin.ts

@@ -39,7 +39,7 @@ export default class SemiWebpackPlugin {
                         if (this.options.prefixCls) {
                         if (this.options.prefixCls) {
                             this.customPrefix(module, this.options.prefixCls);
                             this.customPrefix(module, this.options.prefixCls);
                         }
                         }
-                    })
+                    });
                 } else {
                 } else {
                     compilation.hooks.normalModuleLoader.tap('SemiPlugin', (context: any, module: any) => {
                     compilation.hooks.normalModuleLoader.tap('SemiPlugin', (context: any, module: any) => {
                         if (this.options.omitCss) {
                         if (this.options.omitCss) {
@@ -50,7 +50,7 @@ export default class SemiWebpackPlugin {
                         if (this.options.prefixCls) {
                         if (this.options.prefixCls) {
                             this.customPrefix(module, this.options.prefixCls);
                             this.customPrefix(module, this.options.prefixCls);
                         }
                         }
-                    })
+                    });
                 }
                 }
             }
             }
         });
         });