diff --git a/migrate-from-v2.md b/migrate-from-v2.md index ffcd44f21a..1b7b0234e5 100644 --- a/migrate-from-v2.md +++ b/migrate-from-v2.md @@ -525,23 +525,15 @@ plugins: [ #### Uploader -- `maximize` 重命名为 `maxFileSize` -- `maximum` 重命名为 `maxCount` -- `listType ` 重命名为 `previewType` -- `isDeletable ` 重命名为 `deletable` -- `isPreview` 重命名为 ` preview` -- `defaultImg` 重命名为 ` previewUrl` -- `defaultFileList` 重命名为 ` defaultValue` -- `uploadIconTip` 重命名为 `uploadLabel`,类型变更为 `ReactNode` -- `onBeforeUpload` 重命名为 `beforeUpload` -- `onBeforeXhrUpload` 重命名为 `beforeXhrUpload` -- `onBeforeDelete` 重命名为 `beforeDelete` -- `onRemove` 重命名为 `onDelete` -- 增加 `fit`,用于图片填充模式 -- 增加 `value`,用于受控传值 -- 移除 `uploadIconSize`,可通过 icon 属性传入自定义 icon 或借助 CSS Variables 修改 icon 大小 -- `uploadIcon` 类型从 `string` 调整为 `ReactNode` -- `onChange` 参数类型从 `{fileList: FileItem[], event: any}` 调整为 `FileItem[]` +- 移除了组件内部关于ajax相关网络逻辑的处理 +- 移除了`url`、`headers`、`data`、`xhrState`、`withCredentials`、`timeout` 网络配置相关props +- 移除了`onStart`、`onProgress`、`onFailure`、`beforeXhrUpload` 触发时机函数相关props +- 新增`onOverCount`属性,文件数量超过限制时触发 +- 新增`onUploadQueueChange`属性,图片上传队列变化时触发 +- 简化`FileItem`类型的使用,除url外其他属性变为可选 +- 调整多选状态下`maxCount`属性的默认值为`Number.MAX_VALUE` +- 新增了的 `upload` 方法 +- `defaultValue` 和 `value` 的类型从 `FileType` 变更为 `FileItem` ### 操作反馈 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 313bf3d1d2..2bcfad466b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26934,4 +26934,4 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 - zwitch@2.0.4: {} + zwitch@2.0.4: {} \ No newline at end of file diff --git a/src/config.json b/src/config.json index e1ff16c552..0cd47f797b 100644 --- a/src/config.json +++ b/src/config.json @@ -702,7 +702,7 @@ "author": "VickyYe" }, { - "version": "2.0.0", + "version": "3.0.0", "name": "Uploader", "type": "component", "tarodoc": true, diff --git a/src/packages/uploader/__tests__/uploader.spec.tsx b/src/packages/uploader/__tests__/uploader.spec.tsx index cd839c2bdd..1c810aa397 100644 --- a/src/packages/uploader/__tests__/uploader.spec.tsx +++ b/src/packages/uploader/__tests__/uploader.spec.tsx @@ -3,8 +3,9 @@ import { render, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import { Uploader } from '../uploader' -import { FileItem } from '../file-item' +import { FileItem } from '../types' import { Preview } from '../preview' +import Button from '@/packages/button' test('should render base uploader and type', () => { const { container, getByTestId } = render( @@ -73,7 +74,6 @@ test('should render base uploader other props', () => { { test('before-delete prop return false', () => { const onDelete = vi.fn() const App = () => { - const defaultFileList: FileItem[] = [ + const fileList: FileItem[] = [ { name: '文件1.png', url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', @@ -157,7 +157,7 @@ test('before-delete prop return false', () => { return ( { return false @@ -199,8 +199,125 @@ test('before-delete prop return true', () => { expect(onDelete).toBeCalled() }) +test('should render progress', () => { + const App = () => { + const list: FileItem[] = [ + { + name: '文件444.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + percentage: 30, + }, + ] + return + } + const { container } = render() + const progressElement = container.querySelector('.nut-progress') + expect(progressElement).toBeInTheDocument() + const progressInnerElement = container.querySelector('.nut-progress-inner') + expect(progressInnerElement).toBeInTheDocument() + expect(progressInnerElement).toHaveStyle('width: 30%') + const textElement = container.querySelector('span') + expect(textElement).toHaveTextContent('文件444.png') +}) +test('simulates single file upload', () => { + const handleUpload: any = vi.fn() + const { container } = render( + handleUpload(file)} /> + ) + const file = new File(['hello'], 'hello.png', { type: 'image/png' }) + const input: any = container.querySelector('input') + + fireEvent.change(input, { target: { files: [file] } }) + + expect(handleUpload).toHaveBeenCalledTimes(1) + expect(handleUpload).toHaveBeenCalledWith(file) +}) +test('simulates single file upload fail', async () => { + const handleUpload: any = vi.fn(() => + Promise.reject(new Error('Upload failed')) + ) + const { container } = render( + handleUpload(file)} /> + ) + const file = new File(['hello'], 'hello.png', { type: 'image/png' }) + const input: any = container.querySelector('input') + + fireEvent.change(input, { target: { files: [file] } }) + + expect(handleUpload).toHaveBeenCalledTimes(1) + expect(handleUpload).toHaveBeenCalledWith(file) +}) +test('simulates multiple file upload', () => { + const handleUpload: any = vi.fn() + const handleOverCount: any = vi.fn() + const { container } = render( + handleUpload(file)} + multiple + maxCount={2} + onOverCount={handleOverCount} + /> + ) + const file1 = new File(['file1'], 'file1.txt', { type: 'text/plain' }) + const file2 = new File(['file2'], 'file2.txt', { type: 'text/plain' }) + const file3 = new File(['file3'], 'file3.txt', { type: 'text/plain' }) + const files = [file1, file2, file3] + const input: any = container.querySelector('input') + + fireEvent.change(input, { target: { files } }) + + expect(handleUpload).toHaveBeenCalledTimes(2) + expect(handleOverCount).toHaveBeenCalledTimes(1) + expect(handleOverCount).toHaveBeenCalledWith(3) +}) +test('simulates file upload when autoupload is false', () => { + const handleUpload: any = vi.fn() + const handleOverCount: any = vi.fn() + const { container } = render( + handleUpload(file)} + multiple + autoUpload={false} + maxCount={2} + onOverCount={handleOverCount} + /> + ) + const file1 = new File(['file1'], 'file1.txt', { type: 'text/plain' }) + const file2 = new File(['file2'], 'file2.txt', { type: 'text/plain' }) + const file3 = new File(['file3'], 'file3.txt', { type: 'text/plain' }) + const files = [file1, file2, file3] + const input: any = container.querySelector('input') + fireEvent.change(input, { target: { files } }) + expect(handleUpload).toHaveBeenCalledTimes(0) +}) +test('should render button', () => { + const clearUpload = vi.fn() + const submitUpload = vi.fn() + const App = () => { + return ( + <> + + + + + ) + } + const { container } = render() + const buttonElement = container.querySelector('.nut-button') + expect(buttonElement).toBeInTheDocument() + fireEvent.click(container.querySelectorAll('.nut-button-success')[0]) + expect(submitUpload).toBeCalled() + fireEvent.click(container.querySelectorAll('.nut-button-primary')[0]) + expect(clearUpload).toBeCalled() +}) test('ready file list', () => { - const list = new FileItem() + const list: any = {} list.name = '文件1.png' list.url = 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' @@ -222,7 +339,46 @@ test('ready file list', () => { const { container, getByText } = render() expect(getByText('准备上传')).toHaveTextContent('准备上传') }) - +test('type is not image and doesnot set previewurl', () => { + const list: any = {} + list.name = '文件1.png' + list.url = + 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' + list.status = 'ready' + list.message = '准备上传' + list.type = 'video' + list.uid = '12' + const App = () => { + return + } + const { container, getByText } = render() + expect( + container.querySelector('.nut-uploader-preview-img-file') + ).toBeInTheDocument() +}) +test('type is not image and set previewurl', () => { + const list: any = {} + list.name = '文件1.png' + list.url = + 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' + list.status = 'ready' + list.message = '准备上传' + list.type = 'video' + list.uid = '12' + const App = () => { + return ( + + ) + } + const { container, getByText } = render() + expect( + container.querySelector('.nut-uploader-preview-img-c') + ).toBeInTheDocument() +}) test('preview component', () => { const delFunc = vi.fn() const clickFunc = vi.fn() @@ -232,6 +388,7 @@ test('preview component', () => { status: 'success', message: '上传成功', uid: '12', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', }, ] @@ -242,7 +399,6 @@ test('preview component', () => { deletable onDeleteItem={delFunc} handleItemClick={clickFunc} - previewUrl="https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif" /> ) fireEvent.click(container.querySelectorAll('.close')[0]) @@ -260,9 +416,7 @@ test('preview component', () => { handleItemClick={clickFunc} /> ) - fireEvent.click( - container1.querySelectorAll('.nut-uploader-preview-img-file-name')[0] - ) + fireEvent.click(container1.querySelectorAll('.nut-uploader-preview-img-c')[0]) expect(clickFunc).toBeCalled() const { container: container2 } = render( diff --git a/src/packages/uploader/demo.taro.tsx b/src/packages/uploader/demo.taro.tsx index e0b442d0c9..0a0eb1a583 100644 --- a/src/packages/uploader/demo.taro.tsx +++ b/src/packages/uploader/demo.taro.tsx @@ -1,9 +1,7 @@ import React from 'react' import Taro from '@tarojs/taro' -import { ScrollView, View } from '@tarojs/components' +import { View, ScrollView } from '@tarojs/components' import { useTranslate } from '@/sites/assets/locale/taro' -import Header from '@/sites/components/header' - import Demo1 from './demos/taro/demo1' import Demo2 from './demos/taro/demo2' import Demo3 from './demos/taro/demo3' @@ -14,59 +12,46 @@ import Demo7 from './demos/taro/demo7' import Demo8 from './demos/taro/demo8' import Demo9 from './demos/taro/demo9' import Demo10 from './demos/taro/demo10' -import Demo11 from './demos/taro/demo11' -import Demo12 from './demos/taro/demo12' -import Demo13 from './demos/taro/demo13' -import Demo14 from './demos/taro/demo14' +import Header from '@/sites/components/header' const UploaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', uploadListDefault: '基础用法-上传列表展示', - uploadDefaultProgress: '自定义上传使用默认进度条', uploadStatus: '上传状态', - camera: '直接调起摄像头(移动端生效)', - limitedQuantity: '限制上传数量5个', + limitedQuantity: '限制上传数量', limitSize: '限制上传大小(每个文件最大不超过50kb)', - videoUploader: '使用前摄像头拍摄3s视频并上传(仅支持微信小程序)', - custom: '自定义数据 FormData、headers', - uploadXhrCustom: '自定义 Taro.uploadFile 上传方式(before-xhr-upload)', + beforeUpload: '自定义上传前的处理', manualExecution: '选中文件后,通过按钮手动执行上传', disabled: '禁用状态', customDeleteIcon: '自定义删除icon', + camera: '直接调起摄像头(移动端生效)', }, 'zh-TW': { basic: '基础用法', uploadListDefault: '基础用法-上傳列表展示', - uploadDefaultProgress: '自定義上傳使用默認進度條', uploadStatus: '上傳狀態', - camera: '直接調起攝像頭(移動端生效)', - limitedQuantity: '限制上傳數量5個', - limitSize: '限制上傳大小(每個檔案最大不超過50kb)', - videoUploader: '使用前鏡頭拍攝3s影片並上傳(僅支援微信小程式)', - custom: '自定義數據 FormData、headers', - uploadXhrCustom: '自定義 Taro.uploadFile 上傳方式(before-xhr-upload)', + limitedQuantity: '限制上傳數量', + beforeUpload: '自定義上傳前的處理', + limitSize: '限制上傳大小', manualExecution: '選取檔後,通過按鈕手動執行上傳', disabled: '禁用狀態', customDeleteIcon: '自定義刪除icon', + camera: '直接調起攝像頭(移動端生效)', }, 'en-US': { basic: 'Basic usage', uploadListDefault: 'Basic usage - upload list dispaly', - uploadDefaultProgress: 'Custom upload uses default progress bar', uploadStatus: 'Upload status', - camera: 'Direct camera up (mobile)', - limitedQuantity: 'Limit the number of uploads to 5', + beforeUpload: 'Beforeupload Usage', + limitedQuantity: 'Limit the number of uploads', limitSize: 'Limit upload size (maximum 50kb per file)', - videoUploader: - 'Use the front camera to shoot 3s video and upload it (only support wechat mini program)', - custom: 'Custom data FormData, headers', - uploadXhrCustom: 'Custom xhr Taro.uploadFile method (before-xhr-upload)', manualExecution: 'After selecting Chinese, manually perform the upload via the button', disabled: 'Disabled state', customDeleteIcon: 'Custom DeleteIcon', + camera: 'Direct camera up (mobile)', }, }) @@ -74,36 +59,29 @@ const UploaderDemo = () => { <>
+ {' '} {translated.basic} - {translated.basic} - {translated.uploadStatus} + + {translated.limitedQuantity} - {translated.uploadListDefault} + {translated.limitSize} - {translated.uploadDefaultProgress} + {translated.beforeUpload} - {translated.camera} + {translated.disabled} - {translated.videoUploader} + {translated.customDeleteIcon} - {translated.limitedQuantity} + {translated.camera} - {translated.limitSize} + {translated.manualExecution} - {translated.custom} + {translated.uploadListDefault} - {translated.uploadXhrCustom} - - {translated.manualExecution} - - {translated.disabled} - -

{translated.customDeleteIcon}

-
) diff --git a/src/packages/uploader/demo.tsx b/src/packages/uploader/demo.tsx index ff55c976bf..11dd11a432 100644 --- a/src/packages/uploader/demo.tsx +++ b/src/packages/uploader/demo.tsx @@ -11,24 +11,17 @@ import Demo7 from './demos/h5/demo7' import Demo8 from './demos/h5/demo8' import Demo9 from './demos/h5/demo9' import Demo10 from './demos/h5/demo10' -import Demo11 from './demos/h5/demo11' -import Demo12 from './demos/h5/demo12' -import Demo13 from './demos/h5/demo13' -import Demo14 from './demos/h5/demo14' const UploaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', uploadListDefault: '基础用法-上传列表展示', - uploadDefaultProgress: '自定义上传使用默认进度条', uploadStatus: '上传状态', camera: '直接调起摄像头(移动端生效)', - limitedQuantity: '限制上传数量5个', + limitedQuantity: '限制上传数量', limitSize: '限制上传大小(每个文件最大不超过50kb)', - compress: '图片压缩(在beforeupload钩子中处理)', - custom: '自定义数据 FormData、headers', - uploadXhrCustom: '自定义 xhr 上传方式(before-xhr-upload)', + beforeUpload: '自定义上传前的处理', manualExecution: '选中文件后,通过按钮手动执行上传', disabled: '禁用状态', customDeleteIcon: '自定义删除icon', @@ -36,14 +29,11 @@ const UploaderDemo = () => { 'zh-TW': { basic: '基础用法', uploadListDefault: '基础用法-上傳列表展示', - uploadDefaultProgress: '自定義上傳使用默認進度條', uploadStatus: '上傳狀態', camera: '直接調起攝像頭(移動端生效)', - limitedQuantity: '限制上傳數量5個', - limitSize: '限制上傳大小(每個檔案最大不超過50kb)', - compress: '圖片壓縮(在beforeupload鉤子中處理)', - custom: '自定義數據 FormData、headers', - uploadXhrCustom: '自定義 xhr 上傳方式(before-xhr-upload)', + limitedQuantity: '限制上傳數量', + beforeUpload: '自定義上傳前的處理', + limitSize: '限制上傳大小', manualExecution: '選取檔後,通過按鈕手動執行上傳', disabled: '禁用狀態', customDeleteIcon: '自定義刪除icon', @@ -51,14 +41,11 @@ const UploaderDemo = () => { 'en-US': { basic: 'Basic usage', uploadListDefault: 'Basic usage - upload list dispaly', - uploadDefaultProgress: 'Custom upload uses default progress bar', uploadStatus: 'Upload status', + beforeUpload: 'Beforeupload Usage', camera: 'Direct camera up (mobile)', - limitedQuantity: 'Limit the number of uploads to 5', + limitedQuantity: 'Limit the number of uploads', limitSize: 'Limit upload size (maximum 50kb per file)', - compress: 'Image compression (handled in a foreupload hook)', - custom: 'Custom data FormData, headers', - uploadXhrCustom: 'Custom xhr upload method (before-xhr-upload)', manualExecution: 'After selecting Chinese, manually perform the upload via the button', disabled: 'Disabled state', @@ -71,32 +58,24 @@ const UploaderDemo = () => {

{translated.basic}

-

{translated.basic}

-

{translated.uploadStatus}

+ +

{translated.limitedQuantity}

-

{translated.uploadListDefault}

+

{translated.limitSize}

-

{translated.uploadDefaultProgress}

+

{translated.beforeUpload}

-

{translated.camera}

+

{translated.disabled}

-

{translated.limitedQuantity}

+

{translated.customDeleteIcon}

-

{translated.limitSize}

+

{translated.camera}

-

{translated.compress}

+

{translated.manualExecution}

-

{translated.custom}

+

{translated.uploadListDefault}

-

{translated.uploadXhrCustom}

- -

{translated.manualExecution}

- -

{translated.disabled}

- -

{translated.customDeleteIcon}

-
) diff --git a/src/packages/uploader/demos/h5/demo1.tsx b/src/packages/uploader/demos/h5/demo1.tsx index ab55af17f5..96b994a1ba 100644 --- a/src/packages/uploader/demos/h5/demo1.tsx +++ b/src/packages/uploader/demos/h5/demo1.tsx @@ -1,46 +1,49 @@ -import React from 'react' -import { Uploader, Cell } from '@nutui/nutui-react' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem, Space } from '@nutui/nutui-react' import { Dongdong } from '@nutui/icons-react' const Demo1 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onStart = () => { - console.log('start触发') + const [list, setList] = useState([ + { + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + uid: 133, + status: 'uploading', + }, + ]) + + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } } - const beforeUpload = async (files: File[]) => { - const allowedTypes = ['image/png'] - const filteredFiles = Array.from(files).filter((file) => - allowedTypes.includes(file.type) - ) - return filteredFiles + async function uploadFail(file: File): Promise { + await sleep(2000) + throw new Error('Fail to upload') } return ( <> - - { - console.log('outer onChange', v) - }} - /> - - } - onStart={onStart} - style={{ marginBottom: '10px' }} - /> + + + upload(file)} + /> + upload(file)} /> + } + upload={(file: File) => uploadFail(file)} + /> + ) diff --git a/src/packages/uploader/demos/h5/demo10.tsx b/src/packages/uploader/demos/h5/demo10.tsx index 721f594f2e..56bb98d17f 100644 --- a/src/packages/uploader/demos/h5/demo10.tsx +++ b/src/packages/uploader/demos/h5/demo10.tsx @@ -1,18 +1,74 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' +import React, { useState } from 'react' +import { Loading, Star } from '@nutui/icons-react' +import { Uploader, Button, FileItem } from '@nutui/nutui-react' const Demo10 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const formData = { - custom: 'test', + const [list, setList] = useState([ + { + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + name: '文件2.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + name: '文件3.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + failIcon: , + }, + { + name: '文件444.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + percentage: 30, + }, + { + name: '文件555.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + loadingIcon: , + percentage: 80, + }, + ]) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + if (Math.random() < 0.5) { + return { + url: URL.createObjectURL(file), + } + } + throw new Error('Fail to upload') } return ( + upload={(file: File) => upload(file)} + value={list} + onChange={setList} + maxCount="10" + multiple + previewType="list" + style={{ marginBottom: 20 }} + > + + ) } export default Demo10 diff --git a/src/packages/uploader/demos/h5/demo11.tsx b/src/packages/uploader/demos/h5/demo11.tsx deleted file mode 100644 index 23d425cefc..0000000000 --- a/src/packages/uploader/demos/h5/demo11.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' - -const Demo11 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const beforeXhrUpload = (xhr: XMLHttpRequest, options: any) => { - if (options.method.toLowerCase() === 'put') { - xhr.send(options.sourceFile) - } else { - xhr.send(options.formData) - } - } - - return ( - - ) -} -export default Demo11 diff --git a/src/packages/uploader/demos/h5/demo12.tsx b/src/packages/uploader/demos/h5/demo12.tsx deleted file mode 100644 index 713bdb949a..0000000000 --- a/src/packages/uploader/demos/h5/demo12.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useRef } from 'react' -import { Uploader, Button } from '@nutui/nutui-react' - -interface uploadRefState { - submit: () => void - clear: () => void -} - -const Demo12 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const uploadRef = useRef(null) - - const submitUpload = () => { - ;(uploadRef.current as uploadRefState).submit() - } - const clearUpload = () => { - ;(uploadRef.current as uploadRefState).clear() - } - return ( - <> - -
-
- - -
- - ) -} -export default Demo12 diff --git a/src/packages/uploader/demos/h5/demo13.tsx b/src/packages/uploader/demos/h5/demo13.tsx deleted file mode 100644 index 7428860a66..0000000000 --- a/src/packages/uploader/demos/h5/demo13.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' - -const Demo13 = () => { - return -} -export default Demo13 diff --git a/src/packages/uploader/demos/h5/demo14.tsx b/src/packages/uploader/demos/h5/demo14.tsx deleted file mode 100644 index 08c04fb007..0000000000 --- a/src/packages/uploader/demos/h5/demo14.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -import { Dongdong, Star } from '@nutui/icons-react' - -const Demo14 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - ] - const onDelete = (file: any, fileList: any) => { - console.log('delete事件触发', file, fileList) - } - return ( - } - deleteIcon={} - /> - ) -} -export default Demo14 diff --git a/src/packages/uploader/demos/h5/demo2.tsx b/src/packages/uploader/demos/h5/demo2.tsx index 238035ddf3..f8ac1f50e9 100644 --- a/src/packages/uploader/demos/h5/demo2.tsx +++ b/src/packages/uploader/demos/h5/demo2.tsx @@ -1,15 +1,59 @@ import React from 'react' -import { Uploader, Cell } from '@nutui/nutui-react' +import { Uploader, FileItem } from '@nutui/nutui-react' +import { Dongdong, Loading, Star } from '@nutui/icons-react' -const Demo1 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onStart = () => { - console.log('start触发') +const Demo2 = () => { + const defaultList: FileItem[] = [ + { + uid: 111, + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + uid: 222, + name: '文件2.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + uid: 333, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + }, + { + uid: 444, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + failIcon: , + }, + { + uid: 555, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + }, + { + uid: 666, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + loadingIcon: , + }, + ] + const onDelete = (file: FileItem, fileList: FileItem[]) => { + console.log('delete事件触发', file, fileList) } return ( - - - + } + maxCount={6} + /> ) } -export default Demo1 +export default Demo2 diff --git a/src/packages/uploader/demos/h5/demo3.tsx b/src/packages/uploader/demos/h5/demo3.tsx index f266988695..1e4274cbc2 100644 --- a/src/packages/uploader/demos/h5/demo3.tsx +++ b/src/packages/uploader/demos/h5/demo3.tsx @@ -1,72 +1,35 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -import { Dongdong, Loading, Star } from '@nutui/icons-react' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem, Space } from '@nutui/nutui-react' const Demo3 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '123', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'error', - message: '上传失败', - type: 'image', - uid: '124', - failIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '125', - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '126', - loadingIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '127', - loadingIcon: null, - }, - ] - const onDelete = (file: any, fileList: any) => { - console.log('delete事件触发', file, fileList) + const [list, setList] = useState([]) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } } return ( - } - /> + <> + + + upload(file)} + maxCount={5} + multiple + /> + + + ) } export default Demo3 diff --git a/src/packages/uploader/demos/h5/demo4.tsx b/src/packages/uploader/demos/h5/demo4.tsx index 6392d622c6..a89befe47d 100644 --- a/src/packages/uploader/demos/h5/demo4.tsx +++ b/src/packages/uploader/demos/h5/demo4.tsx @@ -1,75 +1,35 @@ -import React from 'react' -import { Loading, Star } from '@nutui/icons-react' -import { Uploader, Button } from '@nutui/nutui-react' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react' const Demo4 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '123', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'error', - message: '上传失败', - type: 'image', - uid: '124', - failIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '125', - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '126', - loadingIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '127', - loadingIcon: null, - }, - ] - + const [list, setList] = useState([]) + const onOversize = (files: File[]) => { + console.log('oversize 触发 文件大小不能超过 50kb', files) + } + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } + } return ( - - - + + upload(file)} + multiple + maxFileSize={1024 * 50} + onOversize={onOversize} + /> + ) } export default Demo4 diff --git a/src/packages/uploader/demos/h5/demo5.tsx b/src/packages/uploader/demos/h5/demo5.tsx index 7f788cc80f..1e0722ac27 100644 --- a/src/packages/uploader/demos/h5/demo5.tsx +++ b/src/packages/uploader/demos/h5/demo5.tsx @@ -1,27 +1,37 @@ import React, { useState } from 'react' -import { Uploader, Button, Progress } from '@nutui/nutui-react' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react' const Demo5 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const [progressPercent, setProgressPercent] = useState(0) - const onProgress = ({ event, options, percentage }: any) => { - setProgressPercent(percentage) + const [list, setList] = useState([]) + const beforeUpload = async (files: File[]) => { + const allowedTypes = ['image/png'] + const filteredFiles = Array.from(files).filter((file) => + allowedTypes.includes(file.type) + ) + return filteredFiles + } + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } } - return ( - <> - - - -
- + upload(file)} /> - +
) } export default Demo5 diff --git a/src/packages/uploader/demos/h5/demo6.tsx b/src/packages/uploader/demos/h5/demo6.tsx index 1b8b896c29..2980b6a74e 100644 --- a/src/packages/uploader/demos/h5/demo6.tsx +++ b/src/packages/uploader/demos/h5/demo6.tsx @@ -1,8 +1,11 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react' +import { Uploader, Cell } from '@nutui/nutui-react' const Demo6 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - return + return ( + + + + ) } export default Demo6 diff --git a/src/packages/uploader/demos/h5/demo7.tsx b/src/packages/uploader/demos/h5/demo7.tsx index 72c432371f..73c1b89d85 100644 --- a/src/packages/uploader/demos/h5/demo7.tsx +++ b/src/packages/uploader/demos/h5/demo7.tsx @@ -1,8 +1,31 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react' +import { Dongdong, Star } from '@nutui/icons-react' +const defaultFileList: FileItem[] = [ + { + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + message: '上传成功', + }, +] const Demo7 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - return + return ( + + } + deleteIcon={} + style={{ + marginInlineEnd: '10px', + }} + /> + } + deleteIcon={} + /> + + ) } export default Demo7 diff --git a/src/packages/uploader/demos/h5/demo8.tsx b/src/packages/uploader/demos/h5/demo8.tsx index f6cb73261c..91b9c69412 100644 --- a/src/packages/uploader/demos/h5/demo8.tsx +++ b/src/packages/uploader/demos/h5/demo8.tsx @@ -1,18 +1,24 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react' +import { Uploader, Cell } from '@nutui/nutui-react' const Demo8 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onOversize = (files: File[]) => { - console.log('oversize 触发 文件大小不能超过 50kb', files) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } } return ( - + + upload(file)} /> + ) } export default Demo8 diff --git a/src/packages/uploader/demos/h5/demo9.tsx b/src/packages/uploader/demos/h5/demo9.tsx index c993a753ea..69dcd3069c 100644 --- a/src/packages/uploader/demos/h5/demo9.tsx +++ b/src/packages/uploader/demos/h5/demo9.tsx @@ -1,45 +1,51 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react' +import React, { useRef } from 'react' +import { Uploader, Button, Cell, Space } from '@nutui/nutui-react' + +interface uploadRefState { + submit: () => void + clear: () => void +} const Demo9 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const canvastoFile = ( - canvas: HTMLCanvasElement, - type: string, - quality: number - ): Promise => { - return new Promise((resolve) => { - canvas.toBlob((blob) => resolve(blob), type, quality) + const uploadRef = useRef(null) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) }) } - const fileToDataURL = (file: Blob): Promise => { - return new Promise((resolve) => { - const reader = new FileReader() - reader.onloadend = (e) => resolve((e.target as FileReader).result) - reader.readAsDataURL(file) - }) + async function upload(file: File) { + await sleep(2000) + return { + url: URL.createObjectURL(file), + } } - const dataURLToImage = (dataURL: string): Promise => { - return new Promise((resolve) => { - const img = new Image() - img.onload = () => resolve(img) - img.src = dataURL - }) + const submitUpload = () => { + ;(uploadRef.current as uploadRefState).submit() } - const beforeUpload = async (files: File[]) => { - const canvas = document.createElement('canvas') - const context = canvas.getContext('2d') as CanvasRenderingContext2D - const base64 = await fileToDataURL(files[0]) - const img = await dataURLToImage(base64) - canvas.width = img.width - canvas.height = img.height - context.clearRect(0, 0, img.width, img.height) - context.drawImage(img, 0, 0, img.width, img.height) - const blob = (await canvastoFile(canvas, 'image/jpeg', 0.5)) as Blob - const f = await new File([blob], files[0].name, { type: files[0].type }) - return [f] + const clearUpload = () => { + ;(uploadRef.current as uploadRefState).clear() } - - return + return ( + + upload(file)} + style={{ marginBottom: 10 }} + /> + + + + + + ) } export default Demo9 diff --git a/src/packages/uploader/demos/taro/demo1.tsx b/src/packages/uploader/demos/taro/demo1.tsx index 1a0ecb831e..b094b77778 100644 --- a/src/packages/uploader/demos/taro/demo1.tsx +++ b/src/packages/uploader/demos/taro/demo1.tsx @@ -1,39 +1,51 @@ -import React from 'react' -import { Uploader, Cell } from '@nutui/nutui-react-taro' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem, Space } from '@nutui/nutui-react-taro' import { Dongdong } from '@nutui/icons-react-taro' const Demo1 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onStart = () => { - console.log('start触发') + const demoUrl = + 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' + + const [list, setList] = useState([ + { + url: demoUrl, + uid: 133, + }, + ]) + + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { url: demoUrl } } - const beforeUpload = async (files: File[]) => { - console.log('beforeUpload') - const allowedTypes = ['image/png'] - const filteredFiles = Array.from(files).filter((file) => - allowedTypes.includes(file.type) - ) - return filteredFiles + async function uploadFail(file: File): Promise { + await sleep(2000) + throw new Error('Fail to upload') } return ( - - - - } onStart={onStart} /> - + <> + + + upload(file)} + /> + upload(file)} /> + } + upload={(file: File) => uploadFail(file)} + /> + + + ) } export default Demo1 diff --git a/src/packages/uploader/demos/taro/demo10.tsx b/src/packages/uploader/demos/taro/demo10.tsx index 4048d6d886..0c5653c4b7 100644 --- a/src/packages/uploader/demos/taro/demo10.tsx +++ b/src/packages/uploader/demos/taro/demo10.tsx @@ -1,11 +1,74 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' +import React, { useState } from 'react' +import { Loading, Star } from '@nutui/icons-react' +import { Uploader, Button, FileItem } from '@nutui/nutui-react-taro' const Demo10 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const formData = { - custom: 'test', + const [list, setList] = useState([ + { + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + name: '文件2.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + name: '文件3.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + failIcon: , + }, + { + name: '文件444.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + percentage: 30, + }, + { + name: '文件555.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + loadingIcon: , + percentage: 80, + }, + ]) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) } - return + async function upload(file: File) { + await sleep(2000) + if (Math.random() < 0.5) { + return { + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + } + } + throw new Error('Fail to upload') + } + return ( + upload(file)} + value={list} + onChange={setList} + maxCount="10" + multiple + previewType="list" + style={{ marginBottom: 20 }} + > + + + ) } export default Demo10 diff --git a/src/packages/uploader/demos/taro/demo11.tsx b/src/packages/uploader/demos/taro/demo11.tsx deleted file mode 100644 index 0ceef47cd2..0000000000 --- a/src/packages/uploader/demos/taro/demo11.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' - -const Demo11 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const beforeXhrUpload = (taroUploadFile: any, options: any) => { - const uploadTask = taroUploadFile({ - url: options.url, - filePath: options.taroFilePath, - fileType: options.fileType, - header: { - 'Content-Type': 'multipart/form-data', - ...options.headers, - }, - formData: options.formData, - name: options.name, - success(response: { errMsg: any; statusCode: number; data: string }) { - if (options.xhrState === response.statusCode) { - options.onSuccess?.(response, options) - } else { - options.onFailure?.(response, options) - } - }, - fail(e: any) { - options.onFailure?.(e, options) - }, - }) - options.onStart?.(options) - uploadTask.progress( - (res: { - progress: any - totalBytesSent: any - totalBytesExpectedToSend: any - }) => { - options.onProgress?.(res, options) - } - ) - } - return ( - - ) -} -export default Demo11 diff --git a/src/packages/uploader/demos/taro/demo12.tsx b/src/packages/uploader/demos/taro/demo12.tsx deleted file mode 100644 index 7f389f2cf0..0000000000 --- a/src/packages/uploader/demos/taro/demo12.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useRef } from 'react' -import { View } from '@tarojs/components' -import { Uploader, Button } from '@nutui/nutui-react-taro' - -interface uploadRefState { - submit: () => void - clear: () => void -} - -const Demo12 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const uploadRef = useRef(null) - - const submitUpload = () => { - ;(uploadRef.current as uploadRefState).submit() - } - const clearUpload = () => { - ;(uploadRef.current as uploadRefState).clear() - } - return ( - <> - - - - - - - ) -} -export default Demo12 diff --git a/src/packages/uploader/demos/taro/demo13.tsx b/src/packages/uploader/demos/taro/demo13.tsx deleted file mode 100644 index cf6a11e423..0000000000 --- a/src/packages/uploader/demos/taro/demo13.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' - -const Demo13 = () => { - return -} -export default Demo13 diff --git a/src/packages/uploader/demos/taro/demo14.tsx b/src/packages/uploader/demos/taro/demo14.tsx deleted file mode 100644 index e5b8fb09a7..0000000000 --- a/src/packages/uploader/demos/taro/demo14.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' -import { Dongdong, Star } from '@nutui/icons-react-taro' - -const Demo14 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - ] - const onDelete = (file: any, fileList: any) => { - console.log('delete事件触发', file, fileList) - } - return ( - } - deleteIcon={} - /> - ) -} -export default Demo14 diff --git a/src/packages/uploader/demos/taro/demo2.tsx b/src/packages/uploader/demos/taro/demo2.tsx index 626e885f01..764cc9d464 100644 --- a/src/packages/uploader/demos/taro/demo2.tsx +++ b/src/packages/uploader/demos/taro/demo2.tsx @@ -1,15 +1,59 @@ import React from 'react' -import { Uploader, Cell } from '@nutui/nutui-react-taro' +import { Uploader, FileItem } from '@nutui/nutui-react-taro' +import { Dongdong, Loading, Star } from '@nutui/icons-react-taro' const Demo2 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onStart = () => { - console.log('start触发') + const defaultList: FileItem[] = [ + { + uid: 111, + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + uid: 222, + name: '文件2.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'success', + message: '上传成功', + }, + { + uid: 333, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + }, + { + uid: 444, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + failIcon: , + }, + { + uid: 555, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'error', + message: '上传失败', + }, + { + uid: 666, + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + status: 'uploading', + message: '上传中...', + loadingIcon: , + }, + ] + const onDelete = (file: FileItem, fileList: FileItem[]) => { + console.log('delete事件触发', file, fileList) } return ( - - - + } + maxCount={6} + /> ) } export default Demo2 diff --git a/src/packages/uploader/demos/taro/demo3.tsx b/src/packages/uploader/demos/taro/demo3.tsx index d3f2d0d2aa..5e9e644b75 100644 --- a/src/packages/uploader/demos/taro/demo3.tsx +++ b/src/packages/uploader/demos/taro/demo3.tsx @@ -1,72 +1,38 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' -import { Dongdong, Loading, Star } from '@nutui/icons-react-taro' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem, Space } from '@nutui/nutui-react-taro' const Demo3 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '123', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'error', - message: '上传失败', - type: 'image', - uid: '124', - failIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '125', - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '126', - loadingIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '127', - loadingIcon: null, - }, - ] - const onDelete = (file: any, fileList: any) => { - console.log('delete事件触发', file, fileList) + const demoUrl = + 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' + + const [list, setList] = useState([]) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: demoUrl, + } } return ( - } - /> + <> + + + upload(file)} + maxCount={5} + multiple + /> + + + ) } export default Demo3 diff --git a/src/packages/uploader/demos/taro/demo4.tsx b/src/packages/uploader/demos/taro/demo4.tsx index 646fe4bbc1..0d3fcdf942 100644 --- a/src/packages/uploader/demos/taro/demo4.tsx +++ b/src/packages/uploader/demos/taro/demo4.tsx @@ -1,75 +1,40 @@ -import React from 'react' -import { Uploader, Button, Loading } from '@nutui/nutui-react-taro' -import { Star } from '@nutui/icons-react-taro' +import React, { useState } from 'react' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react-taro' const Demo4 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const defaultFileList: any = [ - { - name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '122', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'success', - message: '上传成功', - type: 'image', - uid: '123', - }, - { - name: '文件2.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'error', - message: '上传失败', - type: 'image', - uid: '124', - failIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '125', - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '126', - loadingIcon: , - }, - { - name: '文件3.png', - url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', - status: 'uploading', - message: '上传中...', - type: 'image', - uid: '127', - loadingIcon: null, - }, - ] + const demoUrl = + 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif' + const [list, setList] = useState([]) + + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: demoUrl, + } + } + const onOversize = (files: File[]) => { + console.log('oversize触发文件大小不能超过50kb', files) + } return ( - - - + <> + + upload(file)} + maxFileSize={1024 * 50} + onOversize={onOversize} + /> + + ) } export default Demo4 diff --git a/src/packages/uploader/demos/taro/demo5.tsx b/src/packages/uploader/demos/taro/demo5.tsx index d6835ae100..82099058fe 100644 --- a/src/packages/uploader/demos/taro/demo5.tsx +++ b/src/packages/uploader/demos/taro/demo5.tsx @@ -1,26 +1,37 @@ import React, { useState } from 'react' -import { Uploader, Button, Progress } from '@nutui/nutui-react-taro' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react-taro' const Demo5 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const [progressPercent, setProgressPercent] = useState(0) - const onProgress = ({ event, options, percentage }: any) => { - setProgressPercent(percentage) + const [list, setList] = useState([]) + const beforeUpload = async (files: File[]) => { + const allowedTypes = ['image/png'] + const filteredFiles = Array.from(files).filter((file) => + allowedTypes.includes(file.type) + ) + return filteredFiles + } + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + } } return ( - <> - - - -
- + upload(file)} /> - + ) } export default Demo5 diff --git a/src/packages/uploader/demos/taro/demo6.tsx b/src/packages/uploader/demos/taro/demo6.tsx index 573b42ccf1..c1cb00343e 100644 --- a/src/packages/uploader/demos/taro/demo6.tsx +++ b/src/packages/uploader/demos/taro/demo6.tsx @@ -1,8 +1,11 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' +import { Uploader, Cell } from '@nutui/nutui-react-taro' const Demo6 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - return + return ( + + + + ) } export default Demo6 diff --git a/src/packages/uploader/demos/taro/demo7.tsx b/src/packages/uploader/demos/taro/demo7.tsx index 59ed3cb608..7dd9210a6f 100644 --- a/src/packages/uploader/demos/taro/demo7.tsx +++ b/src/packages/uploader/demos/taro/demo7.tsx @@ -1,15 +1,33 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' +import { Uploader, Cell, FileItem } from '@nutui/nutui-react-taro' +import { Dongdong, Star } from '@nutui/icons-react-taro' +const defaultFileList: FileItem[] = [ + { + name: '文件文件文件文件1文件文件文件文件1文件文件文件文件1.png', + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + message: '上传成功', + }, +] const Demo7 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' return ( - + + } + deleteIcon={} + style={{ + marginInlineEnd: '10px', + }} + maxCount={1} + /> + } + deleteIcon={} + maxCount={1} + /> + ) } export default Demo7 diff --git a/src/packages/uploader/demos/taro/demo8.tsx b/src/packages/uploader/demos/taro/demo8.tsx index 80e0dfd90b..7e432462c1 100644 --- a/src/packages/uploader/demos/taro/demo8.tsx +++ b/src/packages/uploader/demos/taro/demo8.tsx @@ -1,8 +1,24 @@ import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' +import { Cell, Uploader } from '@nutui/nutui-react-taro' const Demo8 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - return + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + } + } + return ( + + upload(file)} sourceType={['camera']} /> + + ) } export default Demo8 diff --git a/src/packages/uploader/demos/taro/demo9.tsx b/src/packages/uploader/demos/taro/demo9.tsx index 4e32e8e70f..96dd98102c 100644 --- a/src/packages/uploader/demos/taro/demo9.tsx +++ b/src/packages/uploader/demos/taro/demo9.tsx @@ -1,19 +1,57 @@ -import React from 'react' -import { Uploader } from '@nutui/nutui-react-taro' -import Taro from '@tarojs/taro' +import React, { useRef } from 'react' +import { Uploader, Button, Cell } from '@nutui/nutui-react-taro' +import { View } from '@tarojs/components' + +interface uploadRefState { + submit: () => void + clear: () => void +} const Demo9 = () => { - const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts' - const onOversize = (files: Taro.chooseImage.ImageFile[]) => { - console.log('oversize触发文件大小不能超过50kb', files) + const uploadRef = useRef(null) + function sleep(time: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) + } + async function upload(file: File) { + await sleep(2000) + return { + url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif', + } + } + const submitUpload = () => { + ;(uploadRef.current as uploadRefState).submit() + } + const clearUpload = () => { + ;(uploadRef.current as uploadRefState).clear() } return ( - + + upload(file)} + style={{ marginBottom: 10 }} + /> + + + + + ) } export default Demo9 diff --git a/src/packages/uploader/doc.en-US.md b/src/packages/uploader/doc.en-US.md index ad5c7a2ff3..ddd803979b 100644 --- a/src/packages/uploader/doc.en-US.md +++ b/src/packages/uploader/doc.en-US.md @@ -1,14 +1,14 @@ -# Uploader +# Uploader Upload -Used to upload local pictures or files to the server. +Used to upload local images or files to the server. -## Import +## Introduction ```tsx import { Uploader } from '@nutui/nutui-react' ``` -## Demo +## Sample code ### Basic usage @@ -18,29 +18,7 @@ import { Uploader } from '@nutui/nutui-react' ::: -> When using the Uploader component to upload files, you may encounter the problem of garbled Chinese characters in the response file information. This usually happens when the client and server are inconsistent in how they handle the encoding of the file. To avoid this problem, it is recommended to ensure that the encoding format of the file read by the server is consistent with that of the client. - -```javascript -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -// Server Demo -app.post('/upload', upload.single('file'), (req, res) => { - const fileEncoding = req.headers['x-file-encoding'] || 'UTF-8' - const fileContent = iconv.decode( - Buffer.from(JSON.stringify(req.file), 'binary'), - fileEncoding - ) - res.json({ - success: true, - message: 'File uploaded successfully', - data: JSON.parse(fileContent), - }) -}) -// Client Demo -; -``` - -### Basic usage +### Upload status :::demo @@ -48,7 +26,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### upload status +### Limit the number of uploads :::demo @@ -56,7 +34,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Basic usage - upload list dispaly +### Limit upload size :::demo @@ -64,7 +42,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Custom upload uses default progress bar +### Customize pre-upload processing :::demo @@ -72,7 +50,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Direct camera up (mobile) +### Disabled state :::demo @@ -80,7 +58,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Limit the number of uploads to 5 +### Custom delete icon :::demo @@ -88,7 +66,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Limit upload size (maximum 50kb per file) +### Directly activate the camera (valid on mobile version) :::demo @@ -96,133 +74,98 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### Image compression (handled in a foreupload hook) +### Manually perform an upload with a button when a file is selected :::demo - - ::: -### Custom data FormData, headers +### Basic usage - upload list display :::demo - - -::: - -### Custom xhr upload method (before-xhr-upload) - -:::demo - - - -::: - -### After selecting Chinese, manually perform the upload via the button - -:::demo - - - -::: - -### Disabled state - -:::demo - - - -::: +:::. ## Uploader ### Props | Property | Description | Type | Default | -| --- | --- | --- | --- | -| autoUpload | Whether to upload the file immediately after selecting it, if false, you need to manually execute the ref submit method to upload | `boolean` | `true` | -| name | The name of the `input` tag `name`, the file parameter name sent to the background | `string` | `file` | -| url | The interface address of the upload server | `string` | `-` | -| defaultValue | List of uploaded files by default | `FileType[]` | `[]` | -| value | List of uploaded files | `FileType[]` | `[]` | -| preview | Whether to display the preview image after the upload is successful | `boolean` | `true` | -| previewUrl | When uploading a default image URL in a non-image ('image') format | `string` | `-` | -| deletable | Whether to display the delete button | `boolean` | `true` | -| method | The http method of upload request | `string` | `post` | -| previewType | The built-in style of the upload list, supports two basic styles picture, list | `string` | `picture` | -| capture | Capture, can be set to [camera](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),turn on the camera directly | `string` | `false` | -| maxFileSize | You can set the maximum upload file size (bytes) | `number` \| `string` | `Number.MAX_VALUE` | -| maxCount | File upload limit | `number` \| `string` | `1` | -| fit | image fill mode | `contain` \| `cover` \| `fill` \| `none` \| `scale-down` | `cover` | -| clearInput | Whether to clear the `input` content, set to `true` to support repeated selection and upload of the same file | `boolean` | `true` | -| accept | File types that can be accepted. See [Des](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B) | `string` | `*` | -| headers | Set request headers | `object` | `{}` | -| data | Uploading extra params or function which can return uploading extra params formData | `object` | `{}` | -| uploadIcon | Upload areaicon name | `React.ReactNode` | `-` | -| deleteIcon | Delete area icon name | `React.ReactNode` | `-` | -| uploadLabel | Upload area tip | `React.ReactNode` | `-` | -| xhrState | The success status (status) value of the interface response | `number` | `200` | -| withCredentials | Support for sending cookie credential information | `boolean` | `false` | -| multiple | Whether to support multiple file selection | `boolean` | `false` | -| disabled | Whether to disable file upload | `boolean` | `false` | -| timeout | timeout, in milliseconds | `number` \| `string` | `1000 * 30` | -| beforeUpload | The pre-upload function needs to return a `Promise` object | `(file: File[]) => Promise` | `-` | -| beforeXhrUpload | When performing an XHR upload, the custom method | `(xhr: XMLHttpRequest, options: any) => void` | `-` | -| beforeDelete | Callback when file is removed. If the return value is false, it will not be removed. Supports returning a `Promise` object, which is not removed when the `Promise` object resolves(false) or rejects | `(file: FileItem, files: FileItem[]) => boolean` | `-` | -| onStart | File upload started | `(option: UploadOptions) => void` | `-` | -| onProgress | Progress of file upload | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onOversize | Triggered when the file size exceeds the limit | `(file: File[]) => void` | `-` | -| onSuccess | uploaded successfully | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onFailure | upload failed | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onChange | Status when uploaded files change | `(files: FileItem[]) => void` | `-` | -| onDelete | The state of the file before deletion | `(file: FileItem, files: FileItem[]) => void` | `-` | -| onFileItemClick | Click trigger after the file is uploaded successfully | `(file: FileItem, index: number) => void` | `-` | - -> Note: accept, capture, and multiple are the native attributes of the browser's input tag. The mobile terminal's different models support these attributes differently, so there may be some compatibility problems under different models and WebView. +| --- | --- | --- | --- | autoUpload | Whether to upload the file immediately after selecting it. +| autoUpload | If or not the upload will be done immediately after the file is selected, if false, you need to manually execute the ref submit method to upload | `boolean` | `true` | upload | The upload method, the input parameter is the file to be uploaded. +| upload | Upload method, input is the file object to be uploaded, after asynchronous processing, return the upload result | `(file: File) => Promise` | `-` | +| name | The name of the `input` tag `name`, the name of the file parameter sent to the backend | `string` | `file` | +| defaultValue | Default list of files already uploaded | `FileItem[]` | `[]` | +| value | List of files that have been uploaded | `FileItem[]` | `-` | +| preview | Whether or not to show the preview image after a successful upload | `boolean` | `true` | +| previewUrl | Default image address when uploading non-image ('image') formats | `string` | `-` | +| deletable | Whether or not to show the delete button | `boolean` | `true` | +| method | The http method for the upload request | `string` | `post` | | previewType +| previewType | The built-in style of the uploaded list, two basic styles are supported picture, list | `string` +| capture | Picture [selection mode] ("), directly bring up the camera | `string` | `false` | maxFileSize +| maxFileSize | You can set the maximum file size (in bytes) for uploading | `number` \| `string` | `Number.MAX_VALUE` | +| maxCount | File upload count limit | `number` \| `string` | `1` | +| fit | Picture fill mode | `contain` \| `cover` \| `fill` \| `none` \| `scale-down` | `cover` | +| clearInput | If or not you want to clear the `input` content, set it to `true` to support selecting the same file to upload over and over again | `boolean` | `true` | +| accept | Allowed file types to be uploaded, [Details] (" B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B") | `string` | `*` | +| uploadIcon | uploadRegion Icon Name | `React.ReactNode` | `-` | +| deleteIcon | Delete the icon name of the region | `React.ReactNode` | `-` | +| uploadLabel | Text below the image in the upload area | `React. +| multiple | Whether to support file multi-selection |`boolean`|`false`| +| disabled | Whether to disable file uploading |`boolean`|`false`| +| beforeUpload | The beforeUpload function needs to return a`Promise`object |`(file: File[]) => Promise`|`-`| +| beforeDelete | Callback when deleting a file, does not remove it if the return value is false. Supports returning a`Promise`object, which is not removed when resolve(false) or reject |`(file: FileItem, files: FileItem[]) => boolean`|`-`| +| onOversize | Triggered when file size exceeds limit |`(file: File[]) => void`|`-`| +| onOverCount | Triggered when the number of files exceeds the limit |`(count: number) => void`|`-`| +| onChange | Triggered when the list of uploaded files changes |`(files: FileItem[]) => void`|`-`| +| onDelete | Triggered when clicked to delete a file |`(file: FileItem, files: FileItem[]) => void`|`-`| +| onFileItemClick | Triggered when a file is uploaded successfully |`(file: FileItem, index: number) => void`|`-`| +| onUploadQueueChange | Triggered when the image upload queue changes |`(tasks: FileItem[]) => void`|`-` | + +> Note: accept, capture and multiple are the native attributes of the browser input tag, the support for these attributes varies among mobile models, so there may be some compatibility issues under different models and WebViews. ### FileItem -| Property | Description | Default | -| --- | --- | --- | -| status | File status value, optional ‘ready,uploading,success,error’' | `ready` | -| uid | Unique ID of the file | `new Date().getTime().toString()` | -| name | File name | `-` | -| url | File path | `-` | -| type | File type | `image/jpeg` | -| formData | Upload the required data | `new FormData()` | +| Name | Description | Default Value | +| --- | --- | --- | status | File status value. +| status | File status value, optionally 'ready,uploading,success,error' | `ready` | +| uid | Unique identifier of the file | `-` | name | File name. +| name | File name | `-` | url | Path to file +| url | file path | `-` | uid | unique identifier for the file | `-` | name | file name | `-` | url | file path | `-` | type | file type +| type | file type | `image` | +| loadingIcon | Loading Icon | `-` | +| failIcon | failureIcon | `-` | +| percentage | upload prgress percent | `-` | ### Methods -Use ref to get Uploader instance and call instance methods. - -| Name | Description | Arguments | Return value | -| --- | --- | --- | --- | -| submit | Manual upload mode, perform upload operation | `-` | `-` | -| clear | Empty the selected file queue (this method is generally used when uploading in manual mode) | `index` | `-` | +The Uploader instance can be retrieved by ref and the instance methods called. +| MethodName | Description | Parameters | ReturnValues | +| --- | --- | --- | --- | --- | submit | Manual upload mode +| submit | Manual upload mode, performs the upload operation | `-` | `-` | +| clear | Clear the queue of selected files (this method is usually used when uploading in manual mode) | `index` | `-` | -## Theming +## Theme customization -### CSS Variables +### Style variables -The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/component/configprovider). +The component provides the following CSS variables that can be used to customize styles, see [ConfigProvider component](#/zh-CN/component/configprovider). | Name | Description | Default Value | | --- | --- | --- | | \--nutui-uploader-image-width | The width of the uploaded image | `100px` | -| \--nutui-uploader-image-height | The height of the uploaded image | `100px` | -| \--nutui-uploader-image-border | The border value of the uploaded image | `0px` | `-` | -| \--nutui-uploader-image-border-radius | Border rounded corners of uploaded images | `4px` | +| \--nutui-uploader-image-height | Height of the uploaded image | `100px` | +| \--nutui-uploader-image-border | Border value of the uploaded image | `0px` | +| \--nutui-uploader-image-border-radius | Rounded border of uploaded image | `4px` | | \--nutui-uploader-background | The background color of the uploaded image | `$color-background` | -| \--nutui-uploader-background-disabled | The background color of the disabled state of uploading images | `$color-background` | -| \--nutui-uploader-image-icon-tip-font-size | The size of the text below the image in the upload area | `12px` | -| \--nutui-uploader-image-icon-tip-color | The color of the text below the image in the upload area | `#C2C4CC` | -| \--nutui-uploader-preview-progress-background | The background color of the upload area preview progress | `rgba(0, 0, 0, 0.65)` | -| \--nutui-uploader-preview-margin-right | Upload area preview margin-right value | `10px` | -| \--nutui-uploader-preview-margin-bottom | Upload area preview margin-bottom value | `10px` | -| \--nutui-uploader-preview-tips-height | Upload a picture to preview the height under tips | `24px` | -| \--nutui-uploader-preview-tips-background | Upload image preview background color under tips | `rgba(0, 0, 0, 0.45)` | -| \--nutui-uploader-preview-tips-padding | Upload an image to preview the padding value under tips | `0 5px` | -| \--nutui-uploader-preview-close-right | The right value of the upload image close button | `0px` | -| \--nutui-uploader-preview-close-top | The top value of the upload image close button | `0px` | +| \--nutui-uploader-background-disabled | Background color for uploaded images in disabled state | `$color-background` | +| \--nutui-uploader-image-icon-tip-font-size | Text size below image in upload area | `12px` | +| \--nutui-uploader-image-icon-tip-color | Text color below image in upload area | `#BFBFBF` | +| \--nutui-uploader-preview-progress-background | The background color of the preview progress in the upload area | `rgba(0, 0, 0, 0.65)` | +| \--nutui-uploader-preview-margin-right | The value of margin-right for the preview of the upload area | `10px` | +| \--nutui-uploader-preview-margin-bottom | Upload area preview the value of margin-bottom | `10px` | +| \--nutui-uploader-preview-tips-height | Height under uploaded image preview tips | `24px` | +| \--nutui-uploader-preview-tips-background | Background color under uploaded image preview tips | `rgba(0, 0, 0, 0.45)` | +| \--nutui-uploader-preview-tips-padding | Padding value under uploaded image preview tips | `0 5px` | +| \--nutui-uploader-preview-close-right | The right value under the upload image's close button | `0px` | +| \--nutui-uploader-preview-close-top | The top value of the uploader's close button | `0px` | diff --git a/src/packages/uploader/doc.md b/src/packages/uploader/doc.md index bf943fce91..3586fe115d 100644 --- a/src/packages/uploader/doc.md +++ b/src/packages/uploader/doc.md @@ -18,30 +18,7 @@ import { Uploader } from '@nutui/nutui-react' ::: -> 在使用Uploader组件上传文件时,可能会遇到响应文件信息中文乱码的问题。这通常发生在客户端与服务器端在处理文件编码时不一致的情况下。为了避免这种问题,建议确保服务器端读取文件的编码格式与客户端保持一致。 - -```javascript -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -// Server Demo -app.post('/upload', upload.single('file'), (req, res) => { - const fileEncoding = req.headers['x-file-encoding'] || 'UTF-8' - const fileContent = iconv.decode( - Buffer.from(JSON.stringify(req.file), 'binary'), - fileEncoding - ) - res.json({ - success: true, - message: 'File uploaded successfully', - data: JSON.parse(fileContent), - }) -}) - -// Client Demo -; -``` - -### 基础用法 +### 上传状态 :::demo @@ -49,7 +26,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 上传状态 +### 限制上传数量 :::demo @@ -57,7 +34,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 基础用法-上传列表展示 +### 限制上传大小 :::demo @@ -65,7 +42,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定义上传使用默认进度条 +### 自定义上传前的处理 :::demo @@ -73,7 +50,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 直接调起摄像头(移动端生效) +### 禁用状态 :::demo @@ -81,7 +58,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上传数量5个 +### 自定义删除icon :::demo @@ -89,7 +66,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上传大小(每个文件最大不超过50kb) +### 直接调起摄像头(移动端生效) :::demo @@ -97,7 +74,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 图片压缩(在beforeupload钩子中处理) +### 选中文件后,通过按钮手动执行上传 :::demo @@ -105,7 +82,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定义数据 FormData、headers +### 基础用法-上传列表展示 :::demo @@ -113,41 +90,17 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定义 xhr 上传方式(before-xhr-upload) - -:::demo - - - -::: - -### 选中文件后,通过按钮手动执行上传 - -:::demo - - - -::: - -### 禁用状态 - -:::demo - - - -::: - ## Uploader ### Props | 字段 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| autoUpload | 是否在选取文件后立即进行上传,false 时需要手动执行 ref submit 方法进行上传 | `Boolean` | `true` | +| autoUpload | 是否在选取文件后立即进行上传,false 时需要手动执行 ref submit 方法进行上传 | `boolean` | `true` | +| upload | 上传方法,入参是需要被上传的文件对象,经过异步处理之后,返回上传结果 | `(file: File) => Promise` | `-` | | name | `input` 标签 `name` 的名称,发到后台的文件参数名 | `string` | `file` | -| url | 上传服务器的接口地址 | `string` | `-` | -| defaultValue | 默认已经上传的文件列表 | `FileType[]` | `[]` | -| value | 已经上传的文件列表 | `FileType[]` | `[]` | +| defaultValue | 默认已经上传的文件列表 | `FileItem[]` | `[]` | +| value | 已经上传的文件列表 | `FileItem[]` | `-` | | preview | 是否上传成功后展示预览图 | `boolean` | `true` | | previewUrl | 当上传非图片('image')格式的默认图片地址 | `string` | `-` | | deletable | 是否展示删除按钮 | `boolean` | `true` | @@ -159,27 +112,19 @@ app.post('/upload', upload.single('file'), (req, res) => { | fit | 图片填充模式 | `contain` \| `cover` \| `fill` \| `none` \| `scale-down` | `cover` | | clearInput | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | `boolean` | `true` | | accept | 允许上传的文件类型,[详细说明]("https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B") | `string` | `*` | -| headers | 设置上传的请求头部 | `object` | `{}` | -| data | 附加上传的信息 formData | `object` | `{}` | | uploadIcon | 上传区域图标名称 | `React.ReactNode` | `-` | | deleteIcon | 删除区域的图标名称 | `React.ReactNode` | `-` | | uploadLabel | 上传区域图片下方文字 | `React.ReactNode` | `-` | -| xhrState | 接口响应的成功状态(status)值 | `number` | `200` | -| withCredentials | 支持发送 cookie 凭证信息 | `Boolean` | `false` | | multiple | 是否支持文件多选 | `boolean` | `false` | | disabled | 是否禁用文件上传 | `boolean` | `false` | -| timeout | 超时时间,单位为毫秒 | `number` \| `string` | `1000 * 30` | | beforeUpload | 上传前的函数需要返回一个`Promise`对象 | `(file: File[]) => Promise` | `-` | -| beforeXhrUpload | 执行 XHR 上传时,自定义方式 | `(xhr: XMLHttpRequest, options: any) => void` | `-` | | beforeDelete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 `Promise` 对象,`Promise` 对象 resolve(false) 或 reject 时不移除 | `(file: FileItem, files: FileItem[]) => boolean` | `-` | -| onStart | 文件上传开始 | `(option: UploadOptions) => void` | `-` | -| onProgress | 文件上传的进度 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | | onOversize | 文件大小超过限制时触发 | `(file: File[]) => void` | `-` | -| onSuccess | 上传成功 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onFailure | 上传失败 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onChange | 上传文件改变时的状态 | `(files: FileItem[]) => void` | `-` | -| onDelete | 文件删除之前的状态 | `(file: FileItem, files: FileItem[]) => void` | `-` | +| onOverCount | 文件数量超过限制时触发 | `(count: number) => void` | `-` | +| onChange | 已上传的文件列表变化时触发 | `(files: FileItem[]) => void` | `-` | +| onDelete | 点击删除文件时触发 | `(file: FileItem, files: FileItem[]) => void` | `-` | | onFileItemClick | 文件上传成功后点击触发 | `(file: FileItem, index: number) => void` | `-` | +| onUploadQueueChange | 图片上传队列变化时触发 | `(tasks: FileItem[]) => void` | `-` | > 注意:accept、capture 和 multiple 为浏览器 input 标签的原生属性,移动端各种机型对这些属性的支持程度有所差异,因此在不同机型和 WebView 下可能出现一些兼容性问题。 @@ -187,12 +132,14 @@ app.post('/upload', upload.single('file'), (req, res) => { | 名称 | 说明 | 默认值 | | --- | --- | --- | -| status | 文件状态值,可选'ready,uploading,success,error,removed' | `ready` | -| uid | 文件的唯一标识 | `new Date().getTime().toString()` | +| status | 文件状态值,可选'ready,uploading,success,error' | `ready` | +| uid | 文件的唯一标识 | `-` | | name | 文件名称 | `-` | | url | 文件路径 | `-` | -| type | 文件类型 | `image/jpeg` | -| formData | 上传所需的data | `new FormData()` | +| type | 文件类型 | `image` | +| loadingIcon | 加载图标 | `-` | +| failIcon | 加载失败图标 | `-` | +| percentage | 上传进度条百分比 | `-` | ### Methods @@ -218,7 +165,7 @@ app.post('/upload', upload.single('file'), (req, res) => { | \--nutui-uploader-background | 上传图片的背景颜色 | `$color-background` | | \--nutui-uploader-background-disabled | 上传图片禁用状态的背景颜色 | `$color-background` | | \--nutui-uploader-image-icon-tip-font-size | 上传区域图片下方文字大小 | `12px` | -| \--nutui-uploader-image-icon-tip-color | 上传区域图片下方文字颜色 | `#C2C4CC` | +| \--nutui-uploader-image-icon-tip-color | 上传区域图片下方文字颜色 | `#BFBFBF` | | \--nutui-uploader-preview-progress-background | 上传区域预览进度的背景颜色 | `rgba(0, 0, 0, 0.65)` | | \--nutui-uploader-preview-margin-right | 上传区域预览margin-right的值 | `10px` | | \--nutui-uploader-preview-margin-bottom | 上传区域预览margin-bottom的值 | `10px` | diff --git a/src/packages/uploader/doc.taro.md b/src/packages/uploader/doc.taro.md index 9a2232ffa2..8350d415ce 100644 --- a/src/packages/uploader/doc.taro.md +++ b/src/packages/uploader/doc.taro.md @@ -18,29 +18,7 @@ import { Uploader } from '@nutui/nutui-react-taro' ::: -> 在使用Uploader组件上传文件时,可能会遇到响应文件信息中文乱码的问题。这通常发生在客户端与服务器端在处理文件编码时不一致的情况下。为了避免这种问题,建议确保服务器端读取文件的编码格式与客户端保持一致。 - -```javascript -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -// Server Demo -app.post('/upload', upload.single('file'), (req, res) => { - const fileEncoding = req.headers['x-file-encoding'] || 'UTF-8' - const fileContent = iconv.decode( - Buffer.from(JSON.stringify(req.file), 'binary'), - fileEncoding - ) - res.json({ - success: true, - message: 'File uploaded successfully', - data: JSON.parse(fileContent), - }) -}) -// Client Demo -; -``` - -### 基础用法 +### 上传状态 :::demo @@ -48,7 +26,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 上传状态 +### 限制上传数量 :::demo @@ -56,7 +34,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 基础用法-上传列表展示 +### 限制上传大小 :::demo @@ -64,7 +42,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定义上传使用默认进度条 +### 自定义上传前的处理 :::demo @@ -72,7 +50,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 直接调起摄像头(移动端生效) +### 禁用状态 :::demo @@ -80,7 +58,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 使用前摄像头拍摄3s视频并上传(仅支持微信小程序) +### 自定义删除icon :::demo @@ -88,7 +66,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上传数量5个 +### 选中文件后,通过按钮手动执行上传 :::demo @@ -96,7 +74,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上传大小(每个文件最大不超过50kb) +### 基础用法-上传列表展示 :::demo @@ -104,38 +82,6 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定义数据 FormData、headers - -:::demo - - - -::: - -### 自定义 Taro.uploadFile 上传方式(before-xhr-upload) - -:::demo - - - -::: - -### 选中文件后,通过按钮手动执行上传 - -:::demo - - - -::: - -### 禁用状态 - -:::demo - - - -::: - ## Uploader ### Props @@ -143,10 +89,10 @@ app.post('/upload', upload.single('file'), (req, res) => { | 字段 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | autoUpload | 是否在选取文件后立即进行上传,false 时需要手动执行 ref submit 方法进行上传 | `boolean` | `true` | +| upload | 上传方法,入参是需要被上传的文件对象,经过异步处理之后,返回上传结果 | `(file: File) => Promise` | `-` | | name | `input` 标签 `name` 的名称,发到后台的文件参数名 | `string` | `file` | -| url | 上传服务器的接口地址 | `string` | `-` | -| defaultValue | 默认已经上传的文件列表 | `FileType[]` | `[]` | -| value | 已经上传的文件列表 | `FileType[]` | `[]` | +| defaultValue | 默认已经上传的文件列表 | `FileItem[]` | `[]` | +| value | 已经上传的文件列表 | `FileItem[]` | `-` | | preview | 是否上传成功后展示预览图 | `boolean` | `true` | | previewUrl | 当上传非图片('image')格式的默认图片地址 | `string` | `-` | | deletable | 是否展示删除按钮 | `boolean` | `true` | @@ -155,46 +101,42 @@ app.post('/upload', upload.single('file'), (req, res) => { | maxFileSize | 可以设定最大上传文件的大小(字节) | `number` \| `string` | `Number.MAX_VALUE` | | maxCount | 文件上传数量限制 | `number` \| `string` | `1` | | fit | 图片填充模式 | `contain` \| `cover` \| `fill` \| `none` \| `scale-down` | `cover` | -| sourceType | [选择文件的来源]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['album','camera']` | -| camera`仅支持WEAPP` | 仅在 `source-type` 为 `camera` 时生效,使用前置或后置摄像头 | `String` | `back` | -| sizeType | [是否压缩所选文件]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['original','compressed']` | -| mediaType`仅支持WEAPP` | [选择文件类型]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['image', 'video', 'mix']` | -| maxDuration`仅支持WEAPP` | 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 60s 之间。不限制相册。 | `number` | `10` | -| headers | 设置上传的请求头部 | `object` | `{}` | -| data | 附加上传的信息 formData | `object` | `{}` | -| uploadIcon | 上传区域图标名称 | `ReactNode` | `-` | +| clearInput | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | `boolean` | `true` | +| uploadIcon | 上传区域图标名称 | `React.ReactNode` | `-` | | deleteIcon | 删除区域的图标名称 | `React.ReactNode` | `-` | -| uploadLabel | 上传区域图片下方文字 | `string` | `""` | -| xhrState | 接口响应的成功状态(status)值 | `number` | `200` | -| disabled | 是否禁用文件上传 | `boolean` | `false` | +| uploadLabel | 上传区域图片下方文字 | `React.ReactNode` | `-` | | multiple | 是否支持文件多选 | `boolean` | `false` | -| timeout | 超时时间,单位为毫秒 | `number` \| `string` | `1000 * 30` | +| disabled | 是否禁用文件上传 | `boolean` | `false` | | beforeUpload | 上传前的函数需要返回一个`Promise`对象 | `(file: File[]) => Promise` | `-` | -| beforeXhrUpload | 执行 XHR 上传时,自定义方式 | `(xhr: XMLHttpRequest, options: any) => void` | `-` | | beforeDelete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 `Promise` 对象,`Promise` 对象 resolve(false) 或 reject 时不移除 | `(file: FileItem, files: FileItem[]) => boolean` | `-` | -| onStart | 文件上传开始 | `(option: UploadOptions) => void` | `-` | -| onProgress | 文件上传的进度 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onOversize | 文件大小超过限制时触发 | `(param: {responseText: XMLHttpRequest['responseText'];option: UploadOptions;files: FileItem[]}) => void` | `-` | -| onSuccess | 上传成功 | `(param: {responseText: XMLHttpRequest['responseText'];option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onFailure | 上传失败 | `(param: {responseText: XMLHttpRequest['responseText'];option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onChange | 上传文件改变时的状态 | `(param: FileItem[]) => void` | `-` | -| onDelete | 文件删除之前的状态 | `(file: FileItem, files: FileItem[]) => void` | `-` | +| onOversize | 文件大小超过限制时触发 | `(file: File[]) => void` | `-` | +| onOverCount | 文件数量超过限制时触发 | `(count: number) => void` | `-` | +| onChange | 已上传的文件列表变化时触发 | `(files: FileItem[]) => void` | `-` | +| onDelete | 点击删除文件时触发 | `(file: FileItem, files: FileItem[]) => void` | `-` | | onFileItemClick | 文件上传成功后点击触发 | `(file: FileItem, index: number) => void` | `-` | +| onUploadQueueChange | 图片上传队列变化时触发 | `(tasks: FileItem[]) => void` | `-` | +| sourceType | [选择文件的来源]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['album','camera']` | +| camera`仅支持WEAPP` | 仅在 `source-type` 为 `camera` 时生效,使用前置或后置摄像头 | `String` | `back` | +| sizeType | [是否压缩所选文件]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['original','compressed']` | +| mediaType`仅支持WEAPP` | [选择文件类型]("https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.chooseMedia.html") | `Array` | `['image', 'video', 'mix']` | +| maxDuration`仅支持WEAPP` | 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 60s 之间。不限制相册。 | `number` | `10` | ### FileItem | 名称 | 说明 | 默认值 | | --- | --- | --- | -| status | 文件状态值,可选'ready,uploading,success,error,removed' | `ready` | -| uid | 文件的唯一标识 | `new Date().getTime().toString()` | +| status | 文件状态值,可选'ready,uploading,success,error' | `ready` | +| uid | 文件的唯一标识 | `-` | | name | 文件名称 | `-` | | url | 文件路径 | `-` | -| type | 文件类型 | `image/jpeg` | -| formData | 上传所需的data | `new FormData()` | +| type | 文件类型 | `image` | +| loadingIcon | 加载图标 | `-` | +| failIcon | 加载失败图标 | `-` | +| percentage | 上传进度条百分比 | `-` | ### Methods -通过 ref 可以获取到 Uploader 实例并调用实例方法 +通过ref可以获取到 Uploader 实例并调用实例方法 | 方法名 | 说明 | 参数 | 返回值 | | --- | --- | --- | --- | @@ -216,7 +158,7 @@ app.post('/upload', upload.single('file'), (req, res) => { | \--nutui-uploader-background | 上传图片的背景颜色 | `$color-background` | | \--nutui-uploader-background-disabled | 上传图片禁用状态的背景颜色 | `$color-background` | | \--nutui-uploader-image-icon-tip-font-size | 上传区域图片下方文字大小 | `12px` | -| \--nutui-uploader-image-icon-tip-color | 上传区域图片下方文字颜色 | `#C2C4CC` | +| \--nutui-uploader-image-icon-tip-color | 上传区域图片下方文字颜色 | `#BFBFBF` | | \--nutui-uploader-preview-progress-background | 上传区域预览进度的背景颜色 | `rgba(0, 0, 0, 0.65)` | | \--nutui-uploader-preview-margin-right | 上传区域预览margin-right的值 | `10px` | | \--nutui-uploader-preview-margin-bottom | 上传区域预览margin-bottom的值 | `10px` | diff --git a/src/packages/uploader/doc.zh-TW.md b/src/packages/uploader/doc.zh-TW.md index 07d2113d1f..bb3f29c96e 100644 --- a/src/packages/uploader/doc.zh-TW.md +++ b/src/packages/uploader/doc.zh-TW.md @@ -18,29 +18,7 @@ import { Uploader } from '@nutui/nutui-react' ::: -> 在使用Uploader組件上傳文件時,可能會遇到響應文件信息中文亂碼的問題。這通常發生在客戶端與服務器端在處理文件編碼時不一致的情況下。為了避免這種問題,建議確保服務器端讀取文件的編碼格式與客戶端保持一致。 - -```javascript -import React from 'react' -import { Uploader } from '@nutui/nutui-react' -// Server Demo -app.post('/upload', upload.single('file'), (req, res) => { - const fileEncoding = req.headers['x-file-encoding'] || 'UTF-8' - const fileContent = iconv.decode( - Buffer.from(JSON.stringify(req.file), 'binary'), - fileEncoding - ) - res.json({ - success: true, - message: 'File uploaded successfully', - data: JSON.parse(fileContent), - }) -}) -// Client Demo -; -``` - -### 基礎用法 +### 上傳狀態 :::demo @@ -48,7 +26,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 上傳狀態 +### 限製上傳數量 :::demo @@ -56,7 +34,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 基礎用法-上傳列表展示 +### 限製上傳大小 :::demo @@ -64,7 +42,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定義上傳使用默認進度條 +### 自定義上傳前的處理 :::demo @@ -72,7 +50,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 直接調起攝像頭(移動端生效) +### 禁用狀態 :::demo @@ -80,7 +58,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上傳數量5個 +### 自定義刪除icon :::demo @@ -88,7 +66,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 限制上傳大小(每個文件最大不超過50kb) +### 直接調起攝像頭(移動端生效) :::demo @@ -96,7 +74,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 圖片壓縮(在beforeupload鈎子中處理) +### 選中文件後,通過按鈕手動執行上傳 :::demo @@ -104,7 +82,7 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定義數據 FormData、headers +### 基礎用法-上傳列表展示 :::demo @@ -112,73 +90,41 @@ app.post('/upload', upload.single('file'), (req, res) => { ::: -### 自定義 xhr 上傳方式(before-xhr-upload) - -:::demo - - - -::: - -### 選中文件後,通過按鈕手動執行上傳 - -:::demo - - - -::: - -### 禁用狀態 - -:::demo - - - -::: - ## Uploader ### Props | 字段 | 說明 | 類型 | 默認值 | | --- | --- | --- | --- | -| autoUpload | 是否在選取文件後立即進行上傳,false 時需要手動執行 ref submit 方法進行上傳 | `Boolean` | `true` | +| autoUpload | 是否在選取文件後立即進行上傳,false 時需要手動執行 ref submit 方法進行上傳 | `boolean` | `true` | +| upload | 上傳方法,入參是需要被上傳的文件對象,經過異步處理之後,返回上傳結果 | `(file: File) => Promise` | `-` | | name | `input` 標簽 `name` 的名稱,發到後臺的文件參數名 | `string` | `file` | -| url | 上傳服務器的接口地址 | `string` | `-` | -| defaultValue | 默認已經上傳的文件列錶 | `FileType[]` | `[]` | -| value | 已經上傳的文件列錶 | `FileType[]` | `[]` | +| defaultValue | 默認已經上傳的文件列表 | `FileItem[]` | `[]` | +| value | 已經上傳的文件列表 | `FileItem[]` | `-` | | preview | 是否上傳成功後展示預覽圖 | `boolean` | `true` | | previewUrl | 當上傳非圖片('image')格式的默認圖片地址 | `string` | `-` | | deletable | 是否展示刪除按鈕 | `boolean` | `true` | | method | 上傳請求的 http method | `string` | `post` | -| previewType | 上傳列錶的內建樣式,支持兩種基本樣式 picture、list | `string` | `picture` | +| previewType | 上傳列表的內建樣式,支持兩種基本樣式 picture、list | `string` | `picture` | | capture | 圖片[選取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture"),直接調起攝像頭 | `string` | `false` | | maxFileSize | 可以設定最大上傳文件的大小(字節) | `number` \| `string` | `Number.MAX_VALUE` | -| maxCount | 文件上傳數量限制 | `number` \| `string` | `1` | +| maxCount | 文件上傳數量限製 | `number` \| `string` | `1` | | fit | 圖片填充模式 | `contain` \| `cover` \| `fill` \| `none` \| `scale-down` | `cover` | | clearInput | 是否需要清空`input`內容,設為`true`支持重復選擇上傳同一個文件 | `boolean` | `true` | | accept | 允許上傳的文件類型,[詳細說明]("https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B") | `string` | `*` | -| headers | 設置上傳的請求頭部 | `object` | `{}` | -| data | 附加上傳的信息 formData | `object` | `{}` | | uploadIcon | 上傳區域圖標名稱 | `React.ReactNode` | `-` | | deleteIcon | 刪除區域的圖標名稱 | `React.ReactNode` | `-` | | uploadLabel | 上傳區域圖片下方文字 | `React.ReactNode` | `-` | -| xhrState | 接口響應的成功狀態(status)值 | `number` | `200` | -| withCredentials | 支持發送 cookie 憑證信息 | `Boolean` | `false` | | multiple | 是否支持文件多選 | `boolean` | `false` | | disabled | 是否禁用文件上傳 | `boolean` | `false` | -| timeout | 超時時間,單位為毫秒 | `number` \| `string` | `1000 * 30` | | beforeUpload | 上傳前的函數需要返回一個`Promise`對象 | `(file: File[]) => Promise` | `-` | -| beforeXhrUpload | 執行 XHR 上傳時,自定義方式 | `(xhr: XMLHttpRequest, options: any) => void` | `-` | | beforeDelete | 除文件時的回調,返回值為 false 時不移除。支持返回一個 `Promise` 對象,`Promise` 對象 resolve(false) 或 reject 時不移除 | `(file: FileItem, files: FileItem[]) => boolean` | `-` | -| onStart | 文件上傳開始 | `(option: UploadOptions) => void` | `-` | -| onProgress | 文件上傳的進度 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onOversize | 文件大小超過限制時觸發 | `(file: File[]) => void` | `-` | -| onSuccess | 上傳成功 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onFailure | 上傳失敗 | `(param: {e: ProgressEvent;option: UploadOptions;percentage: string \| number}) => void` | `-` | -| onChange | 上傳文件改變時的狀態 | `(files: FileItem[]) => void` | `-` | -| onDelete | 文件刪除之前的狀態 | `(file: FileItem, files: FileItem[]) => void` | `-` | +| onOversize | 文件大小超過限製時觸發 | `(file: File[]) => void` | `-` | +| onOverCount | 文件數量超過限製時觸發 | `(count: number) => void` | `-` | +| onChange | 已上傳的文件列表變化時觸發 | `(files: FileItem[]) => void` | `-` | +| onDelete | 點擊刪除文件時觸發 | `(file: FileItem, files: FileItem[]) => void` | `-` | | onFileItemClick | 文件上傳成功後點擊觸發 | `(file: FileItem, index: number) => void` | `-` | +| onUploadQueueChange | 圖片上傳隊列變化時觸發 | `(tasks: FileItem[]) => void` | `-` | > 註意:accept、capture 和 multiple 為瀏覽器 input 標簽的原生屬性,移動端各種機型對這些屬性的支持程度有所差異,因此在不同機型和 WebView 下可能出現一些兼容性問題。 @@ -186,23 +132,25 @@ app.post('/upload', upload.single('file'), (req, res) => { | 名稱 | 說明 | 默認值 | | --- | --- | --- | -| status | 文件狀態值,可選'ready,uploading,success,error,removed' | `ready` | -| uid | 文件的唯一標識 | `new Date().getTime().toString()` | +| status | 文件狀態值,可選'ready,uploading,success,error' | `ready` | +| uid | 文件的唯一標識 | `-` | | name | 文件名稱 | `-` | | url | 文件路徑 | `-` | -| type | 文件類型 | `image/jpeg` | -| formData | 上傳所需的data | `new FormData()` | +| type | 文件類型 | `image` | +| loadingIcon | 加載圖標 | `-` | +| failIcon | 加載失敗圖標 | `-` | +| percentage | 上傳進度條百分比 | `-` | ### Methods -通過ref可以獲取到 Uploader 實例併調用實例方法 +通過ref可以獲取到 Uploader 實例並調用實例方法 | 方法名 | 說明 | 參數 | 返回值 | | --- | --- | --- | --- | | submit | 手動上傳模式,執行上傳操作 | `-` | `-` | | clear | 清空已選擇的文件隊列(該方法一般配合在手動模式上傳時使用) | `index` | `-` | -## 主題定制 +## 主題定製 ### 樣式變量 @@ -217,7 +165,7 @@ app.post('/upload', upload.single('file'), (req, res) => { | \--nutui-uploader-background | 上傳圖片的背景顏色 | `$color-background` | | \--nutui-uploader-background-disabled | 上傳圖片禁用狀態的背景顏色 | `$color-background` | | \--nutui-uploader-image-icon-tip-font-size | 上傳區域圖片下方文字大小 | `12px` | -| \--nutui-uploader-image-icon-tip-color | 上傳區域圖片下方文字顏色 | `#C2C4CC` | +| \--nutui-uploader-image-icon-tip-color | 上傳區域圖片下方文字顏色 | `#BFBFBF` | | \--nutui-uploader-preview-progress-background | 上傳區域預覽進度的背景顏色 | `rgba(0, 0, 0, 0.65)` | | \--nutui-uploader-preview-margin-right | 上傳區域預覽margin-right的值 | `10px` | | \--nutui-uploader-preview-margin-bottom | 上傳區域預覽margin-bottom的值 | `10px` | diff --git a/src/packages/uploader/file-item.taro.ts b/src/packages/uploader/file-item.taro.ts deleted file mode 100644 index 9ea3afdebb..0000000000 --- a/src/packages/uploader/file-item.taro.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNode } from 'react' - -export class FileItem { - status: FileItemStatus = 'ready' - - message = '准备中..' - - uid: string = new Date().getTime().toString() - - name?: string - - url?: string - - type?: string - - path?: string - - percentage?: string | number = 0 - - formData?: FormData = {} as FormData - - responseText?: string - - loadingIcon?: ReactNode - - failIcon?: ReactNode -} - -export type FileItemStatus = - | 'ready' - | 'uploading' - | 'success' - | 'error' - | 'removed' - -export type FileType = { [key: string]: T } diff --git a/src/packages/uploader/file-item.ts b/src/packages/uploader/file-item.ts deleted file mode 100644 index 258a17b20b..0000000000 --- a/src/packages/uploader/file-item.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNode } from 'react' - -export class FileItem { - status: FileItemStatus = 'ready' - - message = '' - - uid: string = new Date().getTime().toString() - - name?: string - - url?: string - - type?: string - - path?: string - - percentage?: string | number = 0 - - formData?: FormData = {} as FormData - - responseText?: string - - loadingIcon?: ReactNode - - failIcon?: ReactNode -} - -export type FileItemStatus = - | 'ready' - | 'uploading' - | 'success' - | 'error' - | 'removed' - -export type FileType = { [key: string]: T } diff --git a/src/packages/uploader/index.taro.ts b/src/packages/uploader/index.taro.ts index 00476178b8..045b14d207 100644 --- a/src/packages/uploader/index.taro.ts +++ b/src/packages/uploader/index.taro.ts @@ -2,5 +2,5 @@ import { Uploader } from './uploader.taro' export type { UploaderProps } from './uploader.taro' -export type { FileType, FileItem, FileItemStatus } from './file-item.taro' +export type { FileType, FileItem, FileItemStatus } from './types' export default Uploader diff --git a/src/packages/uploader/index.ts b/src/packages/uploader/index.ts index d2fa73e97f..62048c5107 100644 --- a/src/packages/uploader/index.ts +++ b/src/packages/uploader/index.ts @@ -2,5 +2,5 @@ import { Uploader } from './uploader' export type { UploaderProps } from './uploader' -export type { FileType, FileItem, FileItemStatus } from './file-item' +export type { FileType, FileItem, FileItemStatus } from './types' export default Uploader diff --git a/src/packages/uploader/preview.taro.tsx b/src/packages/uploader/preview.taro.tsx index cbc7c2e476..8fe8c8f46e 100644 --- a/src/packages/uploader/preview.taro.tsx +++ b/src/packages/uploader/preview.taro.tsx @@ -5,10 +5,10 @@ import { Link as LinkIcon, Loading, } from '@nutui/icons-react-taro' -import { Image, View } from '@tarojs/components' -import Progress from '@/packages/progress/index.taro' -import { FileItem } from '@/packages/uploader/file-item.taro' -import { ERROR } from '@/packages/uploader/utils' +import { View } from '@tarojs/components' +import { FileItem } from '../uploader' +import { Image } from '@/packages/image/image.taro' +import { Progress } from '../progress/progress.taro' export const Preview: React.FunctionComponent = ({ fileList, @@ -21,7 +21,7 @@ export const Preview: React.FunctionComponent = ({ children, }) => { const renderIcon = (item: FileItem) => { - if (item.status === ERROR) { + if (item.status === 'error') { return item.failIcon || } return ( @@ -31,46 +31,55 @@ export const Preview: React.FunctionComponent = ({ return ( <> {fileList.length !== 0 && - fileList.map((item: any, index: number) => { + fileList.map((item: FileItem, index: number) => { + const { + status = 'success', + uid = index, + url, + message = '', + name = '', + type = 'image', + } = item + return ( - + {previewType === 'picture' && !children && deletable && ( onDeleteItem(item, index)} + className="close" > {deleteIcon} )} {previewType === 'picture' && !children && ( - {item.status === 'ready' ? ( + {status === 'ready' ? ( - {item.message} + {message} ) : ( - item.status !== 'success' && ( - + status !== 'success' && ( + {renderIcon(item)} - {item.message} + {message} ) )} - {item.type?.includes('image') ? ( + + {type.includes('image') ? ( <> - {item.url && ( + {url && ( handleItemClick(item, index)} /> )} @@ -80,7 +89,6 @@ export const Preview: React.FunctionComponent = ({ {previewUrl ? ( handleItemClick(item, index)} /> @@ -91,14 +99,14 @@ export const Preview: React.FunctionComponent = ({ className="nut-uploader-preview-img-file-name" > -  {item.name} +  {name} )} )} - {item.status === 'success' ? ( - {item.name} + {status === 'success' && name ? ( + {name} ) : null} )} @@ -106,11 +114,15 @@ export const Preview: React.FunctionComponent = ({ {previewType === 'list' && ( handleItemClick(item, index)} > -  {item.name} + +  {name} + {deletable && ( = ({ onClick={() => onDeleteItem(item, index)} /> )} - {item.status === 'uploading' && ( + {item.status === 'uploading' && item.percentage && ( = ({ fileList, @@ -17,7 +15,7 @@ export const Preview: React.FunctionComponent = ({ children, }) => { const renderIcon = (item: FileItem) => { - if (item.status === ERROR) { + if (item.status === 'error') { return item.failIcon || } return ( @@ -27,12 +25,18 @@ export const Preview: React.FunctionComponent = ({ return ( <> {fileList.length !== 0 && - fileList.map((item: any, index: number) => { + fileList.map((item: FileItem, index: number) => { + const { + status = 'success', + uid = index, + url, + message = '', + name = '', + type = 'image', + } = item + return ( -
+
{previewType === 'picture' && !children && deletable && (
onDeleteItem(item, index)} @@ -43,30 +47,30 @@ export const Preview: React.FunctionComponent = ({ )} {previewType === 'picture' && !children && (
- {item.status === 'ready' ? ( + {status === 'ready' ? (
- {item.message} + {message}
) : ( - item.status !== 'success' && ( + status !== 'success' && (
{renderIcon(item)}
- {item.message} + {message}
) )} - {item.type?.includes('image') ? ( + {type.includes('image') ? ( <> - {item.url && ( + {url && ( handleItemClick(item, index)} /> @@ -88,14 +92,14 @@ export const Preview: React.FunctionComponent = ({ className="nut-uploader-preview-img-file-name" > -  {item.name} +  {name}
)} )} - {item.status === 'success' ? ( -
{item.name}
+ {status === 'success' && name ? ( +
{name}
) : null}
)} @@ -103,11 +107,11 @@ export const Preview: React.FunctionComponent = ({ {previewType === 'list' && (
handleItemClick(item, index)} > -  {item.name} +  {name}
{deletable && ( = ({ /> )} - {item.status === 'uploading' && ( + {item.status === 'uploading' && item.percentage && ( = { [key: string]: T } diff --git a/src/packages/uploader/uploader.taro.tsx b/src/packages/uploader/uploader.taro.tsx index cf2521b4ab..51d82712dc 100644 --- a/src/packages/uploader/uploader.taro.tsx +++ b/src/packages/uploader/uploader.taro.tsx @@ -7,20 +7,14 @@ import React, { useEffect, } from 'react' import classNames from 'classnames' -import Taro, { - chooseImage, - uploadFile, - getEnv, - chooseMedia, -} from '@tarojs/taro' -import { View, Text } from '@tarojs/components' +import Taro, { chooseImage, getEnv, chooseMedia } from '@tarojs/taro' import { Failure, Photograph } from '@nutui/icons-react-taro' +import { View } from '@tarojs/components' import Button from '@/packages/button/index.taro' -import { ERROR, SUCCESS, UploaderTaro, UPLOADING, UploadOptions } from './utils' import { useConfig } from '@/packages/configprovider/configprovider.taro' import { funcInterceptor } from '@/utils/interceptor' import { BasicComponent, ComponentDefaults } from '@/utils/typings' -import { FileItem } from './file-item.taro' +import { FileItem } from './types' import { usePropsValue } from '@/utils/use-props-value' import { Preview } from '@/packages/uploader/preview.taro' @@ -56,7 +50,6 @@ interface TFileType { } export interface UploaderProps extends BasicComponent { - url: string maxCount: string | number sizeType: (keyof sizeType)[] sourceType: (keyof sourceType)[] @@ -75,51 +68,32 @@ export interface UploaderProps extends BasicComponent { disabled: boolean autoUpload: boolean multiple: boolean - timeout: number - data: any - method: string - xhrState: number | string - headers: any preview: boolean deletable: boolean className: string previewUrl?: string maxDuration: number style: React.CSSProperties - onStart?: (option: UploadOptions) => void onDelete?: (file: FileItem, files: FileItem[]) => void - onSuccess?: (param: { - responseText: XMLHttpRequest['responseText'] - option: UploadOptions - files: FileItem[] - }) => void - onProgress?: (param: { - e: ProgressEvent - option: UploadOptions - percentage: number | string - }) => void - onFailure?: (param: { - responseText: XMLHttpRequest['responseText'] - option: UploadOptions - files: FileItem[] - }) => void - onUpdate?: (files: FileItem[]) => void onOversize?: ( files: Taro.chooseImage.ImageFile[] | Taro.chooseMedia.ChooseMedia[] | any ) => void + onOverCount?: (count: number) => void onChange?: (files: FileItem[]) => void + upload: ( + files: Taro.chooseImage.ImageFile | Taro.chooseMedia.ChooseMedia | any + ) => Promise beforeUpload?: ( files: Taro.chooseImage.ImageFile[] | Taro.chooseMedia.ChooseMedia[] | any - ) => Promise - beforeXhrUpload?: (xhr: XMLHttpRequest, options: any) => void + ) => Promise beforeDelete?: (file: FileItem, files: FileItem[]) => boolean onFileItemClick?: (file: FileItem, index: number) => void + onUploadQueueChange?: (tasks: FileItem[]) => void } const defaultProps = { ...ComponentDefaults, - url: '', - maxCount: 1, + maxCount: Number.MAX_VALUE, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], mediaType: ['image', 'video'], @@ -135,12 +109,7 @@ const defaultProps = { autoUpload: true, multiple: false, maxFileSize: Number.MAX_VALUE, - data: {}, - headers: {}, - method: 'post', previewUrl: '', - xhrState: 200, - timeout: 1000 * 30, preview: true, deletable: true, maxDuration: 10, @@ -168,13 +137,7 @@ const InternalUploader: ForwardRefRenderFunction< fit, disabled, multiple, - url, previewUrl, - headers, - timeout, - method, - xhrState, - data, preview, deletable, maxCount, @@ -185,18 +148,15 @@ const InternalUploader: ForwardRefRenderFunction< sizeType, sourceType, maxDuration, - onStart, onDelete, onChange, onFileItemClick, - onProgress, - onSuccess, - onUpdate, - onFailure, onOversize, + onOverCount, beforeUpload, - beforeXhrUpload, + upload, beforeDelete, + onUploadQueueChange, ...restProps } = { ...defaultProps, ...props } const [fileList, setFileList] = usePropsValue({ @@ -207,24 +167,23 @@ const InternalUploader: ForwardRefRenderFunction< onChange?.(v) }, }) - const [uploadQueue, setUploadQueue] = useState[]>([]) - + const [uploadQueue, setUploadQueue] = useState([]) + const fileListRef = useRef([]) const classes = classNames(className, 'nut-uploader') - useImperativeHandle(ref, () => ({ - submit: () => { - Promise.all(uploadQueue).then((res) => { - res.forEach((i) => i.uploadTaro(uploadFile, getEnv())) - }) + submit: async () => { + await uploadAction(uploadQueue) }, clear: () => { clearUploadQueue() }, })) - const fileListRef = useRef([]) useEffect(() => { fileListRef.current = fileList }, [fileList]) + useEffect(() => { + onUploadQueueChange?.(uploadQueue) + }, [uploadQueue]) const clearUploadQueue = (index = -1) => { if (index > -1) { uploadQueue.splice(index, 1) @@ -256,7 +215,6 @@ const InternalUploader: ForwardRefRenderFunction< } } if ((getEnv() === 'WEAPP' || getEnv() === 'JD') && chooseMedia) { - // 其余端全部使用 chooseImage API chooseMedia({ count: multiple ? (maxCount as number) * 1 - fileList.length : 1, /** 文件类型 */ @@ -269,9 +227,6 @@ const InternalUploader: ForwardRefRenderFunction< sizeType, /** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 */ camera, - fail: (res: any) => { - onFailure && onFailure(res) - }, success: onChangeMedia, }) } else { @@ -281,111 +236,55 @@ const InternalUploader: ForwardRefRenderFunction< sizeType, sourceType, success: onChangeImage, - fail: (res: any) => { - onFailure && onFailure(res) - }, }) } } - - const executeUpload = (fileItem: FileItem, index: number) => { - const uploadOption = new UploadOptions() - uploadOption.name = name - uploadOption.url = url - uploadOption.fileType = fileItem.type - uploadOption.formData = fileItem.formData - uploadOption.timeout = timeout * 1 - uploadOption.method = method - uploadOption.xhrState = xhrState - uploadOption.headers = headers - uploadOption.taroFilePath = fileItem.path - uploadOption.beforeXhrUpload = beforeXhrUpload - uploadOption.onStart = (option: UploadOptions) => { - clearUploadQueue(index) - setFileList( - fileListRef.current.map((item) => { - if (item.uid === fileItem.uid) { - item.status = 'ready' - item.message = locale.uploader.readyUpload - } - return item - }) - ) - onStart?.(option) - } - - uploadOption.onProgress = (e: any, option: UploadOptions) => { - setFileList( - fileListRef.current.map((item) => { - if (item.uid === fileItem.uid) { - item.status = UPLOADING - item.message = locale.uploader.uploading - item.percentage = e.progress - onProgress?.({ e, option, percentage: item.percentage as number }) - } - return item - }) - ) - } - - uploadOption.onSuccess = ( - responseText: XMLHttpRequest['responseText'], - option: UploadOptions - ) => { - const list = fileListRef.current.map((item) => { - if (item.uid === fileItem.uid) { - item.status = SUCCESS - item.message = locale.uploader.success - item.responseText = responseText + const uploadAction = async (tasks: FileItem[]) => { + const taskIds = tasks.map((task) => task.uid) + const list: FileItem[] = fileListRef.current.map((file: FileItem) => { + if (taskIds.includes(file.uid)) { + return { + ...file, + status: 'uploading', + message: locale.uploader.uploading, } - return item - }) - setFileList(list) - onSuccess?.({ - responseText, - option, - files: list, - }) - } - - uploadOption.onFailure = ( - responseText: XMLHttpRequest['responseText'], - option: UploadOptions - ) => { - const list = fileListRef.current.map((item) => { - if (item.uid === fileItem.uid) { - item.status = ERROR - item.message = locale.uploader.error - item.responseText = responseText + } + return file + }) + setFileList(list) + await Promise.all( + tasks.map(async (currentTask, index) => { + try { + const result = await upload(currentTask.file as File) + const list = fileListRef.current.map((item) => { + if (item.uid === currentTask.uid) { + item.status = 'success' + item.message = locale.uploader.success + item.url = result.url + } + return item + }) + setFileList(list) + } catch (e) { + const list = fileListRef.current.map((item) => { + if (item.uid === currentTask.uid) { + item.status = 'error' + item.message = locale.uploader.error + } + return item + }) + setFileList(list) + throw e } - return item }) - setFileList(list) - onFailure?.({ - responseText, - option, - files: list, - }) - } - - const task = new UploaderTaro(uploadOption) - if (autoUpload) { - task.uploadTaro(uploadFile, getEnv()) - } else { - uploadQueue.push( - new Promise((resolve, reject) => { - resolve(task) - }) - ) - setUploadQueue(uploadQueue) - } + ).catch((errs) => console.error(errs)) } - const readFile = (files: T[]) => { - files.forEach((file: T, index: number) => { + const idCountRef = useRef(0) + const readFile = async (files: T[]) => { + const tasks = files.map((file) => { let fileType = file.type const filepath = (file.tempFilePath || file.path) as string - const fileItem = new FileItem() if (file.fileType) { fileType = file.fileType } else { @@ -397,34 +296,29 @@ const InternalUploader: ForwardRefRenderFunction< fileType = 'image' } } - - fileItem.path = filepath - fileItem.name = filepath - fileItem.status = 'ready' - fileItem.type = fileType - fileItem.uid = `${fileItem.uid}_${index}` - fileItem.message = autoUpload - ? locale.uploader.readyUpload - : locale.uploader.waitingUpload - - if (getEnv() === 'WEB') { - const formData = new FormData() - for (const [key, value] of Object.entries(data)) { - formData.append(key, value as any) - } - formData.append(name, file.originalFileObj as Blob) - fileItem.name = file.originalFileObj?.name - fileItem.type = file.originalFileObj?.type - fileItem.formData = formData - } else { - fileItem.formData = data as any + const info: any = { + uid: idCountRef.current++, + status: autoUpload ? 'uploading' : 'ready', + file, + message: autoUpload + ? locale.uploader.uploading + : locale.uploader.waitingUpload, + name: getEnv() === 'WEB' ? file.originalFileObj?.name : filepath, + path: filepath, + type: getEnv() === 'WEB' ? file.originalFileObj?.type : fileType, } if (preview) { - fileItem.url = fileType === 'video' ? file.thumbTempFilePath : filepath + info.url = fileType === 'video' ? file.thumbTempFilePath : filepath } - executeUpload(fileItem, index) - setFileList([...fileList, fileItem]) + fileListRef.current = [...fileListRef.current, info] + setFileList(fileListRef.current) + return info }) + if (!autoUpload) { + setUploadQueue(tasks) + } else { + await uploadAction(tasks) + } } const filterFiles = (files: T[]) => { @@ -439,9 +333,9 @@ const InternalUploader: ForwardRefRenderFunction< return true }) oversizes.length && onOversize?.(files as any) - const currentFileLength = filterFile.length + fileList.length if (currentFileLength > maximum) { + onOverCount?.(filterFile.length) filterFile.splice(filterFile.length - (currentFileLength - maximum)) } return filterFile @@ -461,7 +355,7 @@ const InternalUploader: ForwardRefRenderFunction< }) } - const onChangeMedia = (res: Taro.chooseMedia.SuccessCallbackResult) => { + const onChangeMedia = async (res: Taro.chooseMedia.SuccessCallbackResult) => { // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片 const { tempFiles } = res const _files: Taro.chooseMedia.ChooseMedia[] = filterFiles(tempFiles) @@ -478,44 +372,60 @@ const InternalUploader: ForwardRefRenderFunction< } } - const onChangeImage = (res: Taro.chooseImage.SuccessCallbackResult) => { + const onChangeImage = async (res: Taro.chooseImage.SuccessCallbackResult) => { // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片 const { tempFiles } = res const _files: Taro.chooseImage.ImageFile[] = filterFiles(tempFiles) + let files: File[] = [] + const filesArr = new Array().slice.call(files) if (beforeUpload) { - beforeUpload(new Array().slice.call(_files)).then( - (f: Array | boolean) => { - const _files: File[] = filterFiles(new Array().slice.call(f)) - if (!_files.length) res.tempFiles = [] - readFile(_files) - } - ) - } else { - readFile(_files) + files = await beforeUpload(filesArr) } + files = filterFiles(filesArr) + readFile(_files) } const handleItemClick = (file: FileItem, index: number) => { onFileItemClick?.(file, index) } - - return ( - - {(children || previewType === 'list') && ( + const renderListUploader = () => { + return ( + previewType === 'list' && ( <> - {children || ( - - )} + {Number(maxCount) > fileList.length && ( - )} + {Number(maxCount) > fileList.length && ( )}
- )} - + ) + ) + } + return ( +
+ {renderListUploader()} - - {Number(maxCount) > fileList.length && - previewType === 'picture' && - !children && ( -
-
- {uploadIcon} - {uploadLabel} -
- - -
- )} + {renderImageUploader()}
) } export const Uploader = React.forwardRef(InternalUploader) - Uploader.displayName = 'NutUploader' diff --git a/src/packages/uploader/utils.ts b/src/packages/uploader/utils.ts deleted file mode 100644 index 8a19fe54f6..0000000000 --- a/src/packages/uploader/utils.ts +++ /dev/null @@ -1,131 +0,0 @@ -export class UploadOptions { - url = '' - - name = 'file' - - fileType? = 'image' - - formData?: FormData - - sourceFile: any - - method = 'post' - - xhrState: string | number = 200 - - timeout: number = 30 * 1000 - - headers = {} - - withCredentials = false - - onStart?: any - - taroFilePath?: string - - onProgress?: any - - onSuccess?: any - - onFailure?: any - - beforeXhrUpload?: any -} -export const UPLOADING = 'uploading' -export const SUCCESS = 'success' -export const ERROR = 'error' - -export class Utils { - options: UploadOptions - - constructor(options: UploadOptions) { - this.options = options - } - - upload() { - const { options } = this - const xhr = new XMLHttpRequest() - xhr.timeout = options.timeout - if (xhr.upload) { - xhr.upload.addEventListener( - 'progress', - (e: ProgressEvent) => { - options.onProgress?.(e, options) - }, - false - ) - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === options.xhrState) { - options.onSuccess?.(xhr.responseText, options) - } else { - options.onFailure?.(xhr.responseText, options) - } - } - } - xhr.withCredentials = options.withCredentials - xhr.open(options.method, options.url, true) - // headers - for (const [key, value] of Object.entries(options.headers)) { - xhr.setRequestHeader(key, value as string) - } - options.onStart?.(options) - if (options.beforeXhrUpload) { - options.beforeXhrUpload(xhr, options) - } else { - xhr.send(options.formData) - } - } else { - console.warn('浏览器不支持 XMLHttpRequest') - } - } -} - -export class UploaderTaro extends Utils { - constructor(options: UploadOptions) { - super(options) - } - - uploadTaro(uploadFile: any, env: string) { - const options = this.options - if (options.beforeXhrUpload) { - options.beforeXhrUpload(uploadFile, options) - return - } - if (env === 'WEB') { - this.upload() - } else { - const uploadTask = uploadFile({ - url: options.url, - filePath: options.taroFilePath, - fileType: options.fileType, - header: { - 'Content-Type': 'multipart/form-data', - ...options.headers, - }, // - formData: options.formData, - name: options.name, - success(response: { errMsg: any; statusCode: number; data: string }) { - if (options.xhrState === response.statusCode) { - options.onSuccess?.(response, options) - } else { - options.onFailure?.(response, options) - } - }, - fail(e: any) { - options.onFailure?.(e, options) - }, - }) - options.onStart?.(options) - uploadTask.progress( - (res: { - progress: any - totalBytesSent: any - totalBytesExpectedToSend: any - }) => { - options.onProgress?.(res, options) - } - ) - } - } -}