Upload上传

文件选择上传和拖拽上传控件。

何时使用#

上传是将信息(网页、文字、图片、视频等)通过网页或者上传工具发布到远程服务器上的过程。

  • 当需要上传一个或一些文件时。

  • 当需要展现上传的进度时。

  • 当需要使用拖拽交互时。

代码演示

经典款式,用户点击按钮弹出文件选择框。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, message, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  name: 'file',
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  headers: {
    authorization: 'authorization-text',
  },
  onChange(info) {
    if (info.file.status !== 'uploading') {
      console.log(info.file, info.fileList);
    }
    if (info.file.status === 'done') {
      message.success(`${info.file.name} file uploaded successfully`);
    } else if (info.file.status === 'error') {
      message.error(`${info.file.name} file upload failed.`);
    }
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Click to Upload</Button>
  </Upload>
);

export default App;

使用 defaultFileList 设置已上传的内容。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  onChange({ file, fileList }) {
    if (file.status !== 'uploading') {
      console.log(file, fileList);
    }
  },
  defaultFileList: [
    {
      uid: '1',
      name: 'xxx.png',
      status: 'done',
      response: 'Server Error 500', // custom error message to show
      url: 'http://www.baidu.com/xxx.png',
    },
    {
      uid: '2',
      name: 'yyy.png',
      status: 'done',
      url: 'http://www.baidu.com/yyy.png',
    },
    {
      uid: '3',
      name: 'zzz.png',
      status: 'error',
      response: 'Server Error 500', // custom error message to show
      url: 'http://www.baidu.com/zzz.png',
    },
  ],
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Upload</Button>
  </Upload>
);

export default App;

使用 fileList 对列表进行完全控制,可以实现各种自定义功能,以下演示二种情况:

  1. 上传列表数量的限制。

  2. 读取远程路径并显示链接。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Upload } from 'infrad';
import type { UploadFile } from 'infrad/es/upload/interface';
import React, { useState } from 'react';

const App: React.FC = () => {
  const [fileList, setFileList] = useState<UploadFile[]>([
    {
      uid: '-1',
      name: 'xxx.png',
      status: 'done',
      url: 'http://www.baidu.com/xxx.png',
    },
  ]);

  const handleChange: UploadProps['onChange'] = info => {
    let newFileList = [...info.fileList];

    // 1. Limit the number of uploaded files
    // Only to show two recent uploaded files, and old ones will be replaced by the new
    newFileList = fileList.slice(-2);

    // 2. Read from response and show file link
    newFileList = fileList.map(file => {
      if (file.response) {
        // Component will show file.url as link
        file.url = file.response.url;
      }
      return file;
    });

    setFileList(newFileList);
  };

  const props = {
    action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
    onChange: handleChange,
    multiple: true,
  };
  return (
    <Upload {...props} fileList={fileList}>
      <Button icon={<IUpload />}>Upload</Button>
    </Upload>
  );
};

export default App;

支持上传一个文件夹里的所有文件。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import { Button, Upload } from 'infrad';
import React from 'react';

const App: React.FC = () => (
  <Upload action="https://www.mocky.io/v2/5cc8019d300000980a055e76" directory>
    <Button icon={<IUpload />}>Upload Directory</Button>
  </Upload>
);

export default App;

beforeUpload 返回 falsePromise.reject 时,只用于拦截上传行为,不会阻止文件进入上传列表(原因)。如果需要阻止列表展现,可以通过返回 Upload.LIST_IGNORE 实现。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, message, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  beforeUpload: file => {
    const isPNG = file.type === 'image/png';
    if (!isPNG) {
      message.error(`${file.name} is not a png file`);
    }
    return isPNG || Upload.LIST_IGNORE;
  },
  onChange: info => {
    console.log(info.fileList);
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Upload png only</Button>
  </Upload>
);

export default App;

自定义本地预览,用于处理非图片格式文件(例如视频文件)。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  action: '//jsonplaceholder.typicode.com/posts/',
  listType: 'picture',
  previewFile(file) {
    console.log('Your upload file:', file);
    // Your process logic. Here we just mock to the same file
    return fetch('https://next.json-generator.com/api/json/get/4ytyBoLK8', {
      method: 'POST',
      body: file,
    })
      .then(res => res.json())
      .then(({ thumbnail }) => thumbnail);
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Upload</Button>
  </Upload>
);

export default App;

使用 beforeUpload 转换上传的文件(例如添加水印)。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  listType: 'picture',
  beforeUpload(file) {
    return new Promise(resolve => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => {
        const img = document.createElement('img');
        img.src = reader.result as string;
        img.onload = () => {
          const canvas = document.createElement('canvas');
          canvas.width = img.naturalWidth;
          canvas.height = img.naturalHeight;
          const ctx = canvas.getContext('2d')!;
          ctx.drawImage(img, 0, 0);
          ctx.fillStyle = 'red';
          ctx.textBaseline = 'middle';
          ctx.font = '33px Arial';
          ctx.fillText('Infra Design', 20, 20);
          canvas.toBlob(result => resolve(result as any));
        };
      };
    });
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Upload</Button>
  </Upload>
);

export default App;
xxx.png
yyy.png

使用 showUploadList 设置列表交互图标。

expand codeexpand code
import { IUpload, StarOutlined } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  onChange({ file, fileList }) {
    if (file.status !== 'uploading') {
      console.log(file, fileList);
    }
  },
  defaultFileList: [
    {
      uid: '1',
      name: 'xxx.png',
      status: 'done',
      response: 'Server Error 500', // custom error message to show
      url: 'http://www.baidu.com/xxx.png',
    },
    {
      uid: '2',
      name: 'yyy.png',
      status: 'done',
      url: 'http://www.baidu.com/yyy.png',
    },
    {
      uid: '3',
      name: 'zzz.png',
      status: 'error',
      response: 'Server Error 500', // custom error message to show
      url: 'http://www.baidu.com/zzz.png',
    },
  ],
  showUploadList: {
    showDownloadIcon: true,
    downloadIcon: 'Download',
    showRemoveIcon: true,
    removeIcon: <StarOutlined onClick={e => console.log(e, 'custom removeIcon event')} />,
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Upload</Button>
  </Upload>
);

export default App;
+ Upload

配合 antd-img-crop 实现上传前裁切图片。

expand codeexpand code
import ImgCrop from 'antd-img-crop';
import { Upload } from 'infrad';
import type { RcFile, UploadFile, UploadProps } from 'infrad/es/upload/interface';
import React, { useState } from 'react';

const App: React.FC = () => {
  const [fileList, setFileList] = useState<UploadFile[]>([
    {
      uid: '-1',
      name: 'image.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
  ]);

  const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    setFileList(newFileList);
  };

  const onPreview = async (file: UploadFile) => {
    let src = file.url as string;
    if (!src) {
      src = await new Promise(resolve => {
        const reader = new FileReader();
        reader.readAsDataURL(file.originFileObj as RcFile);
        reader.onload = () => resolve(reader.result as string);
      });
    }
    const image = new Image();
    image.src = src;
    const imgWindow = window.open(src);
    imgWindow?.document.write(image.outerHTML);
  };

  return (
    <ImgCrop rotate>
      <Upload
        action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
        listType="picture-card"
        fileList={fileList}
        onChange={onChange}
        onPreview={onPreview}
      >
        {fileList.length < 5 && '+ Upload'}
      </Upload>
    </ImgCrop>
  );
};

export default App;
Upload

点击上传用户头像,并使用 beforeUpload 限制用户上传的图片格式和大小。

beforeUpload 的返回值可以是一个 Promise 以支持异步处理,如服务端校验等:示例

expand codeexpand code
import { LoadingOutlined, PlusOutlined } from 'infra-design-icons';
import { message, Upload } from 'infrad';
import type { UploadChangeParam } from 'infrad/es/upload';
import type { RcFile, UploadFile, UploadProps } from 'infrad/es/upload/interface';
import React, { useState } from 'react';

const getBase64 = (img: RcFile, callback: (url: string) => void) => {
  const reader = new FileReader();
  reader.addEventListener('load', () => callback(reader.result as string));
  reader.readAsDataURL(img);
};

const beforeUpload = (file: RcFile) => {
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
  if (!isJpgOrPng) {
    message.error('You can only upload JPG/PNG file!');
  }
  const isLt2M = file.size / 1024 / 1024 < 2;
  if (!isLt2M) {
    message.error('Image must smaller than 2MB!');
  }
  return isJpgOrPng && isLt2M;
};

const App: React.FC = () => {
  const [loading, setLoading] = useState(false);
  const [imageUrl, setImageUrl] = useState<string>();

  const handleChange: UploadProps['onChange'] = (info: UploadChangeParam<UploadFile>) => {
    if (info.file.status === 'uploading') {
      setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj as RcFile, url => {
        setLoading(false);
        setImageUrl(url);
      });
    }
  };

  const uploadButton = (
    <div>
      {loading ? <LoadingOutlined /> : <PlusOutlined />}
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  );

  return (
    <Upload
      name="avatar"
      listType="picture-card"
      className="avatar-uploader"
      showUploadList={false}
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      beforeUpload={beforeUpload}
      onChange={handleChange}
    >
      {imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
    </Upload>
  );
};

export default App;
文件上传中
image.png
image.png
Upload

用户可以上传图片并在列表中显示缩略图。当上传照片数到达限制后,上传按钮消失。

expand codeexpand code
import { PlusOutlined } from 'infra-design-icons';
import { Modal, Upload } from 'infrad';
import type { RcFile, UploadProps } from 'infrad/es/upload';
import type { UploadFile } from 'infrad/es/upload/interface';
import React, { useState } from 'react';

const getBase64 = (file: RcFile): Promise<string> =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = error => reject(error);
  });

const App: React.FC = () => {
  const [previewVisible, setPreviewVisible] = useState(false);
  const [previewImage, setPreviewImage] = useState('');
  const [previewTitle, setPreviewTitle] = useState('');
  const [fileList, setFileList] = useState<UploadFile[]>([
    {
      uid: '-1',
      name: 'image.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-2',
      name: 'image.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-3',
      name: 'image.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-4',
      name: 'image.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-xxx',
      percent: 50,
      name: 'image.png',
      status: 'uploading',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-5',
      name: 'image.png',
      status: 'error',
    },
  ]);

  const handleCancel = () => setPreviewVisible(false);

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as RcFile);
    }

    setPreviewImage(file.url || (file.preview as string));
    setPreviewVisible(true);
    setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1));
  };

  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
    setFileList(newFileList);

  const uploadButton = (
    <div>
      <PlusOutlined />
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  );
  return (
    <>
      <Upload
        action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
        listType="picture-card"
        fileList={fileList}
        onPreview={handlePreview}
        onChange={handleChange}
      >
        {fileList.length >= 8 ? null : uploadButton}
      </Upload>
      <Modal visible={previewVisible} title={previewTitle} footer={null} onCancel={handleCancel}>
        <img alt="example" style={{ width: '100%' }} src={previewImage} />
      </Modal>
    </>
  );
};

export default App;

Click or drag file to this area to upload

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

把文件拖入指定区域,完成上传,同样支持点击上传。

设置 multiple 后,在 IE10+ 可以一次上传多个文件。

expand codeexpand code
import { IUploadCloud } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { message, Upload } from 'infrad';
import React from 'react';

const { Dragger } = Upload;

const props: UploadProps = {
  name: 'file',
  multiple: true,
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  onChange(info) {
    const { status } = info.file;
    if (status !== 'uploading') {
      console.log(info.file, info.fileList);
    }
    if (status === 'done') {
      message.success(`${info.file.name} file uploaded successfully.`);
    } else if (status === 'error') {
      message.error(`${info.file.name} file upload failed.`);
    }
  },
  onDrop(e) {
    console.log('Dropped files', e.dataTransfer.files);
  },
};

const App: React.FC = () => (
  <Dragger {...props}>
    <p className="ant-upload-drag-icon">
      <IUploadCloud />
    </p>
    <p className="ant-upload-text">Click or drag file to this area to upload</p>
    <p className="ant-upload-hint">
      Support for a single or bulk upload. Strictly prohibit from uploading company data or other
      band files
    </p>
  </Dragger>
);

export default App;

beforeUpload 返回 false 后,手动上传文件。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import { Button, message, Upload } from 'infrad';
import type { RcFile, UploadFile, UploadProps } from 'infrad/es/upload/interface';
import React, { useState } from 'react';

const App: React.FC = () => {
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const [uploading, setUploading] = useState(false);

  const handleUpload = () => {
    const formData = new FormData();
    fileList.forEach(file => {
      formData.append('files[]', file as RcFile);
    });
    setUploading(true);
    // You can use any AJAX library you like
    fetch('https://www.mocky.io/v2/5cc8019d300000980a055e76', {
      method: 'POST',
      body: formData,
    })
      .then(res => res.json())
      .then(() => {
        setFileList([]);
        message.success('upload successfully.');
      })
      .catch(() => {
        message.error('upload failed.');
      })
      .finally(() => {
        setUploading(false);
      });
  };

  const props: UploadProps = {
    onRemove: file => {
      const index = fileList.indexOf(file);
      const newFileList = fileList.slice();
      newFileList.splice(index, 1);
      setFileList(newFileList);
    },
    beforeUpload: file => {
      setFileList([...fileList, file]);

      return false;
    },
    fileList,
  };

  return (
    <>
      <Upload {...props}>
        <Button icon={<IUpload />}>Select File</Button>
      </Upload>
      <Button
        type="primary"
        onClick={handleUpload}
        disabled={fileList.length === 0}
        loading={uploading}
        style={{ marginTop: 16 }}
      >
        {uploading ? 'Uploading' : 'Start Upload'}
      </Button>
    </>
  );
};

export default App;
yyy.png


yyy.png

上传文件为图片,可展示本地缩略图。IE8/9 不支持浏览器本地缩略图展示(Ref),可以写 thumbUrl 属性来代替。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import { Button, Upload } from 'infrad';
import type { UploadFile } from 'infrad/es/upload/interface';
import React from 'react';

const fileList: UploadFile[] = [
  {
    uid: '-1',
    name: 'xxx.png',
    status: 'done',
    url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
  },
  {
    uid: '-2',
    name: 'yyy.png',
    status: 'error',
  },
];

const App: React.FC = () => (
  <>
    <Upload
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      listType="picture"
      defaultFileList={[...fileList]}
    >
      <Button icon={<IUpload />}>Upload</Button>
    </Upload>
    <br />
    <br />
    <Upload
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      listType="picture"
      defaultFileList={[...fileList]}
      className="upload-list-inline"
    >
      <Button icon={<IUpload />}>Upload</Button>
    </Upload>
  </>
);

export default App;
/* tile uploaded pictures */
.upload-list-inline .ant-upload-list-item {
  float: left;
  width: 200px;
  margin-right: 8px;
}

.upload-list-inline [class*='-upload-list-rtl'] .ant-upload-list-item {
  float: right;
}

通过 maxCount 限制上传数量。当为 1 时,始终用最新上传的代替当前。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import { Button, Space, Upload } from 'infrad';
import React from 'react';

const App: React.FC = () => (
  <Space direction="vertical" style={{ width: '100%' }} size="large">
    <Upload
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      listType="picture"
      maxCount={1}
    >
      <Button icon={<IUpload />}>Upload (Max: 1)</Button>
    </Upload>
    <Upload
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      listType="picture"
      maxCount={3}
      multiple
    >
      <Button icon={<IUpload />}>Upload (Max: 3)</Button>
    </Upload>
  </Space>
);

export default App;

使用阿里云 OSS 上传示例.

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, Form, message, Upload } from 'infrad';
import type { UploadFile } from 'infrad/es/upload/interface';
import React, { useEffect, useState } from 'react';

interface OSSDataType {
  dir: string;
  expire: string;
  host: string;
  accessId: string;
  policy: string;
  signature: string;
}

interface AliyunOSSUploadProps {
  value?: UploadFile[];
  onChange?: (fileList: UploadFile[]) => void;
}

const AliyunOSSUpload = ({ value, onChange }: AliyunOSSUploadProps) => {
  const [OSSData, setOSSData] = useState<OSSDataType>();

  // Mock get OSS api
  // https://help.aliyun.com/document_detail/31988.html
  const mockGetOSSData = () => ({
    dir: 'user-dir/',
    expire: '1577811661',
    host: '//www.mocky.io/v2/5cc8019d300000980a055e76',
    accessId: 'c2hhb2RhaG9uZw==',
    policy: 'eGl4aWhhaGFrdWt1ZGFkYQ==',
    signature: 'ZGFob25nc2hhbw==',
  });

  const init = async () => {
    try {
      const result = await mockGetOSSData();
      setOSSData(result);
    } catch (error) {
      message.error(error);
    }
  };

  useEffect(() => {
    init();
  }, []);

  const handleChange: UploadProps['onChange'] = ({ fileList }) => {
    console.log('Aliyun OSS:', fileList);
    onChange?.([...fileList]);
  };

  const onRemove = (file: UploadFile) => {
    const files = (value || []).filter(v => v.url !== file.url);

    if (onChange) {
      onChange(files);
    }
  };

  const getExtraData: UploadProps['data'] = file => ({
    key: file.url,
    OSSAccessKeyId: OSSData?.accessId,
    policy: OSSData?.policy,
    Signature: OSSData?.signature,
  });

  const beforeUpload: UploadProps['beforeUpload'] = async file => {
    if (!OSSData) return false;

    const expire = Number(OSSData.expire) * 1000;

    if (expire < Date.now()) {
      await init();
    }

    const suffix = file.name.slice(file.name.lastIndexOf('.'));
    const filename = Date.now() + suffix;
    // @ts-ignore
    file.url = OSSData.dir + filename;

    return file;
  };

  const uploadProps: UploadProps = {
    name: 'file',
    fileList: value,
    action: OSSData?.host,
    onChange: handleChange,
    onRemove,
    data: getExtraData,
    beforeUpload,
  };

  return (
    <Upload {...uploadProps}>
      <Button icon={<IUpload />}>Click to Upload</Button>
    </Upload>
  );
};

const App: React.FC = () => (
  <Form labelCol={{ span: 4 }}>
    <Form.Item label="Photos" name="photos">
      <AliyunOSSUpload />
    </Form.Item>
  </Form>
);

export default App;

使用 itemRender ,我们可以集成 react-dnd 来实现对上传列表拖拽排序。

expand codeexpand code
import update from 'immutability-helper';
import { IUpload } from 'infra-design-icons';
import { Button, Tooltip, Upload } from 'infrad';
import type { UploadFile, UploadProps } from 'infrad/es/upload/interface';
import React, { useCallback, useRef, useState } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

const type = 'DragableUploadList';

interface DragableUploadListItemProps {
  originNode: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
  file: UploadFile;
  fileList: UploadFile[];
  moveRow: (dragIndex: any, hoverIndex: any) => void;
}

const DragableUploadListItem = ({
  originNode,
  moveRow,
  file,
  fileList,
}: DragableUploadListItemProps) => {
  const ref = useRef<HTMLDivElement>(null);
  const index = fileList.indexOf(file);
  const [{ isOver, dropClassName }, drop] = useDrop({
    accept: type,
    collect: monitor => {
      const { index: dragIndex } = monitor.getItem() || {};
      if (dragIndex === index) {
        return {};
      }
      return {
        isOver: monitor.isOver(),
        dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
      };
    },
    drop: (item: any) => {
      moveRow(item.index, index);
    },
  });
  const [, drag] = useDrag({
    type,
    item: { index },
    collect: monitor => ({
      isDragging: monitor.isDragging(),
    }),
  });
  drop(drag(ref));
  const errorNode = <Tooltip title="Upload Error">{originNode.props.children}</Tooltip>;
  return (
    <div
      ref={ref}
      className={`ant-upload-draggable-list-item ${isOver ? dropClassName : ''}`}
      style={{ cursor: 'move' }}
    >
      {file.status === 'error' ? errorNode : originNode}
    </div>
  );
};

const App: React.FC = () => {
  const [fileList, setFileList] = useState<UploadFile[]>([
    {
      uid: '-1',
      name: 'image1.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-2',
      name: 'image2.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-3',
      name: 'image3.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-4',
      name: 'image4.png',
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
    {
      uid: '-5',
      name: 'image.png',
      status: 'error',
    },
  ]);

  const moveRow = useCallback(
    (dragIndex: number, hoverIndex: number) => {
      const dragRow = fileList[dragIndex];
      setFileList(
        update(fileList, {
          $splice: [
            [dragIndex, 1],
            [hoverIndex, 0, dragRow],
          ],
        }),
      );
    },
    [fileList],
  );

  const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    setFileList(newFileList);
  };

  return (
    <DndProvider backend={HTML5Backend}>
      <Upload
        action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
        fileList={fileList}
        onChange={onChange}
        itemRender={(originNode, file, currFileList) => (
          <DragableUploadListItem
            originNode={originNode}
            file={file}
            fileList={currFileList}
            moveRow={moveRow}
          />
        )}
      >
        <Button icon={<IUpload />}>Click to Upload</Button>
      </Upload>
    </DndProvider>
  );
};

export default App;
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item {
  border-top: 2px dashed rgba(0, 0, 0, 0);
  border-bottom: 2px dashed rgba(0, 0, 0, 0);
}
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item.drop-over-downward {
  border-bottom-color: #2673dd;
}
#components-upload-demo-drag-sorting .ant-upload-draggable-list-item.drop-over-upward {
  border-top-color: #2673dd;
}

使用 progress 属性自定义进度条样式。

expand codeexpand code
import { IUpload } from 'infra-design-icons';
import type { UploadProps } from 'infrad';
import { Button, message, Upload } from 'infrad';
import React from 'react';

const props: UploadProps = {
  name: 'file',
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  headers: {
    authorization: 'authorization-text',
  },
  onChange(info) {
    if (info.file.status !== 'uploading') {
      console.log(info.file, info.fileList);
    }
    if (info.file.status === 'done') {
      message.success(`${info.file.name} file uploaded successfully`);
    } else if (info.file.status === 'error') {
      message.error(`${info.file.name} file upload failed.`);
    }
  },
  progress: {
    strokeColor: {
      '0%': '#108ee9',
      '100%': '#87d068',
    },
    strokeWidth: 3,
    format: percent => percent && `${parseFloat(percent.toFixed(2))}%`,
  },
};

const App: React.FC = () => (
  <Upload {...props}>
    <Button icon={<IUpload />}>Click to Upload</Button>
  </Upload>
);

export default App;

API#

参数说明类型默认值版本
accept接受上传的文件类型, 详见 input accept Attributestring-
action上传的地址string | (file) => Promise<string>-
beforeUpload上传文件之前的钩子,参数为上传的文件,若返回 false 则停止上传。支持返回一个 Promise 对象,Promise 对象 reject 时则停止上传,resolve 时开始上传( resolve 传入 FileBlob 对象则上传 resolve 传入对象);也可以返回 Upload.LIST_IGNORE,此时列表中将不展示此文件。 注意:IE9 不支持该方法(file, fileList) => boolean | Promise<File> | Upload.LIST_IGNORE-
customRequest通过覆盖默认的上传行为,可以自定义自己的上传实现function-
data上传所需额外参数或返回上传额外参数的方法object|(file) => object | Promise<object>-
defaultFileList默认已经上传的文件列表object[]-
directory支持上传文件夹(caniusebooleanfalse
disabled是否禁用booleanfalse
fileList已经上传的文件列表(受控),使用此参数时,如果遇到 onChange 只调用一次的问题,请参考 #2423UploadFile[]-
headers设置上传的请求头部,IE10 以上有效object-
iconRender自定义显示 icon(file: UploadFile, listType?: UploadListType) => ReactNode-
isImageUrl自定义缩略图是否使用 <img /> 标签进行显示(file: UploadFile) => boolean(内部实现)
itemRender自定义上传列表项(originNode: ReactElement, file: UploadFile, fileList: object[], actions: { download: function, preview: function, remove: function }) => React.ReactNode-4.16.0
listType上传列表的内建样式,支持三种基本样式 text, picturepicture-cardstringtext
maxCount限制上传数量。当为 1 时,始终用最新上传的文件代替当前文件number-4.10.0
method上传请求的 http methodstringpost
multiple是否支持多选文件,ie10+ 支持。开启后按住 ctrl 可选择多个文件booleanfalse
name发到后台的文件参数名stringfile
openFileDialogOnClick点击打开文件对话框booleantrue
previewFile自定义文件预览逻辑(file: File | Blob) => Promise<dataURL: string>-
progress自定义进度条样式ProgressProps(仅支持 type="line"{ strokeWidth: 2, showInfo: false }4.3.0
showUploadList是否展示文件列表, 可设为一个对象,用于单独设定 showPreviewIcon, showRemoveIcon, showDownloadIcon, removeIcondownloadIconboolean | { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, previewIcon?: ReactNode | (file: UploadFile) => ReactNode, removeIcon?: ReactNode | (file: UploadFile) => ReactNode, downloadIcon?: ReactNode | (file: UploadFile) => ReactNode }truefunction: 4.7.0
withCredentials上传请求时是否携带 cookiebooleanfalse
onChange上传文件改变时的状态,详见 onChangefunction-
onDrop当文件被拖入上传区域时执行的回调功能(event: React.DragEvent) => void-4.16.0
onDownload点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页function(file): void(跳转新标签页)
onPreview点击文件链接或预览图标时的回调function(file)-
onRemove  点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除              function(file): boolean | Promise-  

UploadFile#

继承自 File,附带额外属性用于渲染。

参数说明类型默认值版本
crossOriginCORS 属性设置'anonymous' | 'use-credentials' | ''-4.20.0
name文件名string--
percent上传进度number--
status上传状态,不同状态展示颜色也会有所不同error | success | done | uploading--
thumbUrl缩略图地址string--
uid唯一标识符,不设置时会自动生成string--
url下载地址string--

onChange#

上传中、完成、失败都会调用这个函数。

文件状态改变的回调,返回为:

{
  file: { /* ... */ },
  fileList: [ /* ... */ ],
  event: { /* ... */ },
}
  1. file 当前操作的文件对象。

    {
       uid: 'uid',      // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突
       name: 'xx.png'   // 文件名
       status: 'done', // 状态有:uploading done error,beforeUpload 拦截的文件没有 status 属性
       response: '{"status": "success"}', // 服务端响应内容
       linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性
    }
  2. fileList 当前的文件列表。

  3. event 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。

FAQ#

服务端如何实现?#

如何显示下载链接?#

请使用 fileList 属性设置数组项的 url 属性进行展示控制。

customRequest 怎么使用?#

请参考 https://github.com/react-component/upload#customrequest

为何 fileList 受控时,上传不在列表中的文件不会触发 onChange 后续的 status 更新事件?#

onChange 事件仅会作用于在列表中的文件,因而 fileList 不存在对应文件时后续事件会被忽略。请注意,在 4.13.0 版本之前受控状态存在 bug 导致不在列表中的文件也会触发。

onChange 为什么有时候返回 File 有时候返回 { originFileObj: File }?#

历史原因,在 beforeUpload 返回 false 时,会返回 File 对象。在下个大版本我们会统一返回 { originFileObj: File } 对象。当前版本已经兼容所有场景下 info.file.originFileObj 获取原 File 写法。你可以提前切换。

为何有时 Chrome 点击 Upload 无法弹出文件选择框?#

与 antd 无关,原生上传也会失败。请重启 Chrome 浏览器,让其完成升级工作。相关 issue: