diff --git a/src/config-provider/ConfigContext.tsx b/src/config-provider/ConfigContext.tsx index 8f091d40..66e822b2 100644 --- a/src/config-provider/ConfigContext.tsx +++ b/src/config-provider/ConfigContext.tsx @@ -1,4 +1,5 @@ import { createContext } from 'react'; +import defaultZhLocale from '../_common/js/global-config/mobile/locale/zh_CN'; export const defaultClassPrefix = 't'; @@ -9,10 +10,12 @@ export interface Config { * @default 't' */ classPrefix?: string; + globalConfig?: typeof defaultZhLocale; } export const defaultContext = { classPrefix: defaultClassPrefix, + globalConfig: defaultZhLocale, }; const ConfigContext = createContext(defaultContext); diff --git a/src/upload/Upload.tsx b/src/upload/Upload.tsx index f3043672..d453c693 100644 --- a/src/upload/Upload.tsx +++ b/src/upload/Upload.tsx @@ -1,374 +1,116 @@ -import React, { ChangeEvent, useCallback, useEffect, useRef } from 'react'; -import { AddIcon, CloseIcon, RefreshIcon } from 'tdesign-icons-react'; +import type { MouseEvent } from 'react'; +import React from 'react'; +import { AddIcon, CloseCircleIcon, CloseIcon, LoadingIcon } from 'tdesign-icons-react'; import classNames from 'classnames'; -import { - ProgressContext, - RequestMethodResponse, - SizeLimitObj, - SizeUnit, - SizeUnitArray, - SuccessContext, - TdUploadProps, - UploadFile, -} from './type'; -// eslint-disable-next-line import/no-relative-packages -import request from '../_common/js/upload/xhr'; -import useDefault from '../_util/useDefault'; -import { StyledProps } from '../common'; -import useConfig from '../_util/useConfig'; -import { TdUploadFile } from '.'; -import { urlCreator, removeProperty, removePropertyAtArray, getNewestUidFactory } from './util'; - -// upload 工具函数 -const removeUidProperty = (file: TdUploadFile) => removeProperty(file, 'uid'); -const removeUidPropertyAtArray = (fileList: TdUploadFile[]) => removePropertyAtArray(fileList, 'uid'); -const getNewestUid = getNewestUidFactory(); -export interface UploadProps extends TdUploadProps, StyledProps { - name?: string; -} +import type { TdUploadProps, UploadFile } from './type'; +import type { StyledProps } from '../common'; +import Image from '../image'; +import useUpload from './hooks/useUpload'; +import useDefaultProps from '../hooks/useDefaultProps'; +import { usePrefixClass } from '../hooks/useClass'; +import useConfig from '../hooks/useConfig'; +import { uploadDefaultProps } from './defaultProps'; +import parseTNode from '../_util/parseTNode'; + +export interface UploadProps extends TdUploadProps, StyledProps {} const Upload: React.FC = (props) => { - const { - accept, - action, - method, - name, - files, - defaultFiles, - headers, - max, - multiple, - disabled, - sizeLimit, - withCredentials, - className, - data, - beforeUpload, - format, - requestMethod, - onProgress, - onSuccess, - onFail, - onRemove, - } = props; - const { classPrefix } = useConfig(); - const [fileList, onChange] = useDefault, any[]>(files, defaultFiles || [], props.onChange); - - const uploadRef = useRef(); - - const formatFiles = (files: File[] = []): TdUploadFile[] => - files.map((fileRaw) => { - const file = typeof format === 'function' ? format(fileRaw) : fileRaw; - const uploadFile: TdUploadFile = { - raw: fileRaw, - lastModified: fileRaw.lastModified, - name: fileRaw.name, - size: fileRaw.size, - type: fileRaw.type, - percent: 0, - status: 'waiting', - url: urlCreator()?.createObjectURL(fileRaw), - ...file, - }; - return uploadFile; + const rootClassName = usePrefixClass('upload'); + const { globalConfig } = useConfig(); + const { onPreview, onClickUpload, addContent, accept, children, className, files, max, multiple, imageProps } = + useDefaultProps(props, uploadDefaultProps); + const { displayFiles, inputRef, disabled, onNormalFileChange, onInnerRemove } = useUpload(props); + const uploadGlobalConfig = globalConfig.upload; + const containerClassName = classNames(rootClassName, className); + + const handlePreview = (e: MouseEvent, file: UploadFile, index: number) => { + onPreview?.({ + e: e as MouseEvent, + file, + index, }); - - const getLimitedFiles = (files: Array = []) => { - const isSingleMode = !multiple; - const mergeLen = files.length + fileList.length; - if (isSingleMode) { - return files.splice(0, 1); - } - - // 限制了最大的张数 - if (max > 0) { - const limitedFiles = mergeLen > max ? files.slice(0, max - mergeLen) : files; - return fileList.concat(limitedFiles); - } - - return fileList.concat(files); - }; - - const generateUploadFiles = (files: FileList): TdUploadFile[] => { - const uploadList = formatFiles(Array.from(files)); - return getLimitedFiles(uploadList); }; - const handleSizeLimit = ( - fileSize: number, - sizeLimit: UploadProps['sizeLimit'], - ): { - isExceed: boolean; - message: string; - } => { - const unitArr: SizeUnitArray = ['B', 'KB', 'MB', 'GB']; - // 各个单位和 B 的关系 - const SIZE_MAP = { - B: 1, - KB: 1024, - MB: 1048576, // 1024 * 1024 - GB: 1073741824, // 1024 * 1024 * 1024 - }; - const sizeLimitObj: SizeLimitObj = typeof sizeLimit === 'number' ? { size: sizeLimit, unit: 'KB' } : sizeLimit; - // fileSize 的单位为 B - sizeLimitObj.unit = sizeLimitObj.unit.toUpperCase() as SizeUnit; - if (!unitArr.includes(sizeLimitObj.unit)) { - console.warn(`TDesign Upload Warn: \`sizeLimit.unit\` can only be one of ${unitArr.join()}`); + const triggerUpload = (e: MouseEvent) => { + const input = inputRef.current as HTMLInputElement; + if (disabled) { + return; } - return { - isExceed: fileSize <= sizeLimitObj.size * SIZE_MAP[sizeLimitObj.unit], - message: sizeLimitObj.message, - }; - }; - - const isShowAdd = (): boolean => { - if (!multiple && fileList.length) return false; - if (multiple && max && fileList.length === max) return false; - return true; - }; - - const handleBeforeUpload = useCallback( - (file: TdUploadFile): Promise => { - if (typeof beforeUpload === 'function') { - const res = beforeUpload(file); - if (res instanceof Promise) return res; - return Promise.resolve(res); - } - if (sizeLimit) { - const { isExceed } = handleSizeLimit(file.size, sizeLimit); - return Promise.resolve(isExceed); - } - return Promise.resolve(true); - }, - [beforeUpload, sizeLimit], - ); - - const handleProgress = useCallback( - ({ e, file, percent, type = 'mock' }: ProgressContext) => { - const newFileList = [...fileList]; - const currentFile = newFileList.find((item) => item.uid === (file as TdUploadFile).uid); - currentFile.percent = percent; - onProgress?.({ e, file: removeUidProperty(currentFile), percent, type }); - onChange?.(newFileList, { trigger: 'progress' }); - }, - [fileList, onProgress, onChange], - ); - - const handleSuccess = useCallback( - ({ e, file, response }: SuccessContext) => { - const newFileList = [...fileList]; - const currentFile = newFileList.find((item) => item.uid === (file as TdUploadFile).uid); - currentFile.status = 'success'; - if (response.url) currentFile.url = response.url; - onSuccess?.({ - e, - file: removeUidProperty(currentFile), - fileList: removeUidPropertyAtArray(newFileList), - response, - }); - onChange?.(newFileList, { trigger: 'success' }); - }, - [fileList, onSuccess, onChange], - ); - - const handleFail = useCallback( - (options: { e?: ProgressEvent; file: UploadFile }) => { - const { e, file } = options; - const newFileList = [...fileList]; - const currentFile = newFileList.find((item) => item.uid === (file as TdUploadFile).uid); - currentFile.status = 'fail'; - onFail?.({ e, file: removeUidProperty(currentFile) }); - onChange(newFileList, { trigger: 'fail' }); - }, - [fileList, onFail, onChange], - ); - - const handleRequestMethod = useCallback( - async (file: TdUploadFile) => { - if (typeof requestMethod !== 'function') { - console.error('TDesign Upload Error: `requestMethod` must be a function.'); - return; - } - const res: RequestMethodResponse = await requestMethod(removeUidProperty(file)); - // 验证请求结果 - if (!res) { - console.error('TDesign Upload Error: `requestMethodResponse` is required.'); - return; - } - if (!res.status) { - console.error( - 'TDesign Upload Error: `requestMethodResponse.status` is missing, which value is `success` or `fail`', - ); - return; - } - if (!['success', 'fail'].includes(res.status)) { - console.error('TDesign Upload Error: `requestMethodResponse.status` must be `success` or `fail`'); - return; - } - if (res.status === 'success' && (!res.response || !res.response.url)) { - console.warn( - 'TDesign Upload Warn: `requestMethodResponse.response.url` is required, when `status` is `success`', - ); - } - if (res.status === 'success') { - handleSuccess({ file, response: res.response }); - } - if (res.status === 'fail') { - handleFail({ file }); - } - }, - [requestMethod, handleSuccess, handleFail], - ); - - const handleUpload = useCallback( - (uploadFile: TdUploadFile): Promise => { - const file = { ...uploadFile }; - if (file.status !== 'waiting') return; - if (!action && !requestMethod) { - console.error('TDesign Upload Error: action or requestMethod is required.'); - return; - } - file.status = 'progress'; - if (requestMethod) { - return handleRequestMethod(file); - } - request({ - action, - method, - data, - file, - name, - headers, - withCredentials, - onSuccess: handleSuccess, - onProgress: handleProgress, - onError: handleFail, - }); - }, - [ - requestMethod, - action, - method, - data, - name, - headers, - withCredentials, - handleRequestMethod, - handleSuccess, - handleProgress, - handleFail, - ], - ); - - const handleUpLoadFiles = useCallback( - (files: TdUploadFile[]) => { - files.forEach((file) => { - handleBeforeUpload(file).then((canUpload) => { - if (canUpload) { - // 上传 - handleUpload(file); - } - }); - }); - }, - [handleBeforeUpload, handleUpload], - ); - - const handleUploadChange = (event: ChangeEvent) => { - if (disabled) return; - const { files } = event.target; - const uploadFiles = generateUploadFiles(files); - onChange?.(uploadFiles, { trigger: 'upload' }); + input.click(); + onClickUpload?.({ e: e as MouseEvent }); }; - const handleTriggerUpload = () => { - if (disabled) return; - uploadRef?.current.click(); - }; - - const handleDeleteFile = (file: TdUploadFile, e) => { - const newFileList = [...fileList]; - const index = newFileList.findIndex((item) => item.uid === file.uid); - if (index !== -1) { - newFileList.splice(index, 1); + const renderStatus = (file: UploadFile) => { + if (file.status !== 'fail' && file.status !== 'progress') { + return null; } - onRemove?.({ index, file: removeUidProperty(file), e }); - onChange?.(newFileList, { trigger: 'delete' }); - }; - - const handleReloadFile = (file: TdUploadFile) => { - const newFileList = [...fileList]; - const currentFile = newFileList.find((item) => item.uid === file.uid); - currentFile.percent = 0; - currentFile.status = 'waiting'; - onChange?.(newFileList, { trigger: 'reload' }); + const renderIcon = () => + file.status === 'progress' ? ( + <> + +
+ {file.percent ? `${file.percent}%` : uploadGlobalConfig.progress.uploadingText} +
+ + ) : ( + + ); + const renderFailText = () => + file.status === 'fail' ? ( +
{uploadGlobalConfig.progress.failText}
+ ) : null; + + return ( +
+ {renderIcon()} + {renderFailText()} +
+ ); }; - useEffect(() => { - if (fileList && fileList.length) { - // 若 fileList 中存在 waiting 状态的文件则触发上传 - const hasWaitingStatus = fileList.some((file) => file.status === 'waiting'); - if (hasWaitingStatus) { - handleUpLoadFiles(fileList); - } - } - }, [fileList, handleUpLoadFiles]); - - useEffect(() => { - // 添加 uid - if (fileList && fileList.length) { - for (let i = 0; i < fileList.length; i++) { - if (!fileList[i].uid) { - fileList[i].uid = getNewestUid(); - } + const renderContent = () => { + const childNode = parseTNode(children); + const addContentNode = parseTNode(addContent, {}, ); + if (max === 0 || (max > 0 && displayFiles.length < max)) { + if (childNode) { + return
{childNode}
; } + return ( +
+
{addContentNode}
+
+ ); } - }, [fileList]); - - const uploadBaseClassNames = `${classPrefix}-upload`; + }; - const uploadClassNames = classNames(uploadBaseClassNames, className); + const renderDisplayFiles = () => + displayFiles.map((file, index) => ( +
+ {file.url ? ( +
handlePreview(e, file, index)}> + +
+ ) : null} + {renderStatus(file)} + onInnerRemove({ e: e as MouseEvent, file, index })} + /> +
+ )); return ( -
-
    - {fileList.map((file, index) => ( -
  • -
    - handleDeleteFile(file, e)} - > - - - {file.name} - {file.status === 'fail' && ( -
    - handleReloadFile(file)} /> -
    - )} -
    -
  • - ))} - {isShowAdd() ? ( -
  • handleTriggerUpload()}> -
    - -
    -
  • - ) : null} -
+
+ {renderDisplayFiles()} + {renderContent()} handleUploadChange(event)} + accept={accept} + onChange={onNormalFileChange} />
); diff --git a/src/upload/_example/base.jsx b/src/upload/_example/base.jsx deleted file mode 100644 index 8b30912d..00000000 --- a/src/upload/_example/base.jsx +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-ignore -import React from 'react'; -import { Upload } from 'tdesign-mobile-react'; - -import './style/index.less'; - -export default () => { - const files = [ - { - url: 'https://tdesign.gtimg.com/site/upload1.png', - name: 'uploaded1.png', - type: 'image', - }, - ]; - - return ( -
-

上传图片

- -
- ); -}; diff --git a/src/upload/_example/base.tsx b/src/upload/_example/base.tsx new file mode 100644 index 00000000..d75d1c89 --- /dev/null +++ b/src/upload/_example/base.tsx @@ -0,0 +1,25 @@ +// @ts-ignore +import React from 'react'; +import { Message, Upload } from 'tdesign-mobile-react'; +import './style/index.less'; + +export default function BaseDemo() { + const onValidate = (context: any) => { + if (context.type === 'FILE_OVER_SIZE_LIMIT') { + Message.warning('文件大小超出限制'); + } + }; + return ( +
+

上传图片

+ +
+ ); +} diff --git a/src/upload/_example/custom.tsx b/src/upload/_example/custom.tsx new file mode 100644 index 00000000..a5d0fce0 --- /dev/null +++ b/src/upload/_example/custom.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Image, Message, Upload } from 'tdesign-mobile-react'; +import './style/index.less'; + +export default function CustomDemo() { + const onValidate = (context: any) => { + if (context.type === 'FILE_OVER_SIZE_LIMIT') { + Message.warning('文件大小超出上限'); + } + }; + return ( +
+
请上传身份证人像面
+
+ + +
+ } + defaultFiles={[]} + multiple={false} + max={1} + sizeLimit={{ size: 3000000, unit: 'B' }} + onValidate={onValidate} + /> +
+
请上传身份证国徽面
+
+ + +
+ } + accept="image/png" + action="//service-bv448zsw-1257786608.gz.apigw.tencentcs.com/api/upload-demo" + defaultFiles={[]} + multiple={false} + max={1} + sizeLimit={{ size: 3000000, unit: 'B' }} + onValidate={onValidate} + /> +
+ + ); +} diff --git a/src/upload/_example/index.jsx b/src/upload/_example/index.jsx index 36b0a5d4..5867d93b 100644 --- a/src/upload/_example/index.jsx +++ b/src/upload/_example/index.jsx @@ -1,18 +1,26 @@ import React from 'react'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; -import Base from './base'; -import Multiple from './multiple'; +import BaseDemo from './base'; +import MultipleDemo from './multiple'; +import StatusDemo from './status'; +import CustomDemo from './custom'; export default function () { return (
- + - + - - + + + + + + + +
); diff --git a/src/upload/_example/multiple.jsx b/src/upload/_example/multiple.jsx deleted file mode 100644 index cd694b30..00000000 --- a/src/upload/_example/multiple.jsx +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-ignore -import React, { useState } from 'react'; -import { Upload } from 'tdesign-mobile-react'; - -export default () => { - const [files, setFiles] = useState([ - { - url: 'https://tdesign.gtimg.com/site/upload1.png', - name: 'uploaded1.png', - type: 'image', - }, - { - url: 'https://tdesign.gtimg.com/site/upload2.png', - name: 'uploaded2.png', - type: 'image', - }, - { - url: 'https://tdesign.gtimg.com/site/upload1.png', - name: 'uploaded3.png', - type: 'image', - status: 'fail', - }, - ]); - const handleRemove = ({ index }) => { - setFiles(files.filter((item, idx) => index !== idx)); - }; - - return ( -
-

上传图片

- -
- ); -}; diff --git a/src/upload/_example/multiple.tsx b/src/upload/_example/multiple.tsx new file mode 100644 index 00000000..d6e56894 --- /dev/null +++ b/src/upload/_example/multiple.tsx @@ -0,0 +1,78 @@ +// @ts-ignore +import type { MouseEvent } from 'react'; +import React, { useState } from 'react'; +import { + ProgressContext, + SuccessContext, + Upload, + UploadChangeContext, + UploadFailContext, + UploadFile, + UploadRemoveContext, +} from 'tdesign-mobile-react'; +import './style/index.less'; + +export default () => { + const [files, setFiles] = useState([ + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload4.png', + name: 'uploaded1.png', + type: 'image', + }, + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload6.png', + name: 'uploaded2.png', + type: 'image', + }, + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload4.png', + name: 'uploaded3.png', + type: 'image', + }, + ]); + const onFail = ({ file, e }: UploadFailContext): any => { + console.log('---onFail', file, e); + return null; + }; + const onProgress = ({ file, percent, type, e }: ProgressContext) => { + console.log('---onProgress:', file, percent, type, e); + }; + const onChange = (files: Array, { e, response, trigger, index, file }: UploadChangeContext) => { + console.log('====onChange', files, e, response, trigger, index, file); + setFiles(files); + }; + const onPreview = ({ file, e }: { file: UploadFile; e: MouseEvent }) => { + console.log('====onPreview', file, e); + }; + const onSuccess = ({ file, fileList, response, e }: SuccessContext) => { + console.log('====onSuccess', file, fileList, e, response); + }; + const onRemove = ({ index, file, e }: UploadRemoveContext) => { + console.log('====onRemove', index, file, e); + }; + const onSelectChange = (files: Array) => { + console.log('====onSelectChange', files); + }; + const onClickUpload = ({ e }: { e: MouseEvent }) => { + console.log('====onClickUpload', e); + }; + return ( +
+
多选上传
+ +
+ ); +}; diff --git a/src/upload/_example/status.tsx b/src/upload/_example/status.tsx new file mode 100644 index 00000000..2e931fb8 --- /dev/null +++ b/src/upload/_example/status.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type { UploadFile } from 'tdesign-mobile-react'; +import { Upload } from 'tdesign-mobile-react'; +import './style/index.less'; + +export default function StatusDemo() { + const files: UploadFile[] = [ + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload6.png', + name: 'uploaded1.png', + type: 'image', + status: 'progress', + }, + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload4.png', + name: 'uploaded1.png', + type: 'image', + status: 'progress', + percent: 68, + }, + ]; + const failFiles: UploadFile[] = [ + { + url: 'https://tdesign.gtimg.com/mobile/demos/upload4.png', + name: 'uploaded1.png', + type: 'image', + status: 'fail', + }, + ]; + return ( + <> +
+
上传图片
+ +
+
上传失败
+
+
上传图片
+ +
+ + ); +} diff --git a/src/upload/_example/style/index.less b/src/upload/_example/style/index.less index e2253e2d..bcec7734 100644 --- a/src/upload/_example/style/index.less +++ b/src/upload/_example/style/index.less @@ -1,12 +1,44 @@ - -.demo-section { - overflow: hidden; - background-color: #fff; - padding-bottom: 4px; +.upload-demo { + background: var(--bg-color-demo, #fff); &__title { - margin: 12px 16px; - color: rgba(0,0,0,0.9); + font-size: 14px; + margin: 24px 16px 16px; + color: var(--td-text-color-secondary, rgba(0, 0, 0, 0.6)); + } + + .upload-title { font-size: 16px; + color: var(--td-text-color-primary, rgba(0, 0, 0, 0.9)); + padding: 12px 16px 0; + } + + .upload-content { + --td-upload-width: 343px; + --td-upload-height: 200px; + --td-upload-grid-columns: 1; + } + + .front { + --td-upload-background: center / 100% 100% no-repeat url('https://tdesign.gtimg.com/mobile/demos/upload1.png'); + } + + .reverse { + --td-upload-background: center / 100% 100% no-repeat url('https://tdesign.gtimg.com/mobile/demos/upload2.png'); + } + + .add-content { + display: flex; + justify-content: center; + align-items: center; + width: 72px; + height: 72px; + background: #d9e1ff; + border-radius: 50%; + + .content-img { + width: 32px; + height: 32px; + } } -} \ No newline at end of file +} diff --git a/src/upload/defaultProps.ts b/src/upload/defaultProps.ts new file mode 100644 index 00000000..b25ecdf5 --- /dev/null +++ b/src/upload/defaultProps.ts @@ -0,0 +1,20 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdUploadProps } from './type'; + +export const uploadDefaultProps: TdUploadProps = { + allowUploadDuplicateFile: false, + autoUpload: true, + disabled: undefined, + defaultFiles: [], + isBatchUpload: false, + max: 0, + method: 'POST', + multiple: false, + name: 'file', + uploadAllFilesInOneRequest: false, + useMockProgress: true, + withCredentials: false, +}; diff --git a/src/upload/hooks/useUpload.ts b/src/upload/hooks/useUpload.ts new file mode 100644 index 00000000..fdef7633 --- /dev/null +++ b/src/upload/hooks/useUpload.ts @@ -0,0 +1,434 @@ +import type { ChangeEvent, MouseEvent } from 'react'; +import { useRef, useState } from 'react'; +import isString from 'lodash/isString'; +import isFunction from 'lodash/isFunction'; +import type { InnerProgressContext, OnResponseErrorContext } from '../../_common/js/upload/types'; +import type { + SizeLimitObj, + SuccessContext, + TdUploadProps, + UploadChangeContext, + UploadFile, + UploadRemoveContext, +} from '../type'; +import { getFileList, getFileUrlByFileRaw } from '../../_common/js/upload/utils'; +import { + formatToUploadFile, + getDisplayFiles, + getFilesAndErrors, + upload, + validateFile, +} from '../../_common/js/upload/main'; +import useDefaultProps from '../../hooks/useDefaultProps'; +import { uploadDefaultProps } from '../defaultProps'; +import useDefault from '../../_util/useDefault'; + +export type ValidateParams = Parameters[0]; + +export default function useUpload(props: TdUploadProps) { + const { + allowUploadDuplicateFile, + action, + disabled, + autoUpload, + multiple, + files, + defaultFiles, + max, + sizeLimit, + headers, + method, + isBatchUpload, + name, + uploadAllFilesInOneRequest, + mockProgressDuration, + withCredentials, + useMockProgress, + data, + beforeUpload, + beforeAllFilesUpload, + format, + formatRequest, + formatResponse, + requestMethod, + onChange, + onCancelUpload, + onFail, + onOneFileFail, + onOneFileSuccess, + onProgress, + onSuccess, + onSelectChange, + onWaitingUploadFilesChange, + onValidate, + } = useDefaultProps(props, uploadDefaultProps); + const [uploadValue, setUploadValue] = useDefault(files, defaultFiles, onChange); + const [isUploading, setUploading] = useState(false); + const [toUploadFiles, setToUploadFiles] = useState([]); + const [sizeOverLimitMessage, setSizeOverLimitMessage] = useState(''); + const inputRef = useRef(null); + const xhrReq = useRef<{ files: UploadFile[]; xhrReq: XMLHttpRequest }[]>([]); + + const displayFiles = getDisplayFiles({ + multiple, + toUploadFiles, + uploadValue, + autoUpload, + isBatchUpload, + }); + + const uploadFilePercent = ({ file, percent }: { file: UploadFile; percent: number }) => { + const index = toUploadFiles.findIndex((item) => item.raw === file.raw); + setToUploadFiles((toUploadFiles: UploadFile[]) => { + const newUploadFiles = [...toUploadFiles]; + newUploadFiles[index] = { ...toUploadFiles[index], percent }; + return newUploadFiles; + }); + }; + + const updateFilesProgress = () => { + if (!autoUpload) { + return; + } + setToUploadFiles([...toUploadFiles]); + }; + + const onResponseError = (e: OnResponseErrorContext) => { + if (!e || !e.files || !e.files[0]) { + return; + } + const { response, event, files } = e; + updateFilesProgress(); + onOneFileFail?.({ + e: event, + file: files?.[0], + currentFiles: files, + failedFiles: files, + response, + }); + // 单选或多文件替换,需要清空上一次上传成功的文件 + if (!multiple || isBatchUpload) { + setUploadValue([], { + trigger: 'progress-fail', + e: event, + file: files[0], + }); + } + }; + + // 多文件上传场景,单个文件进度 + const onResponseProgress = (p: InnerProgressContext) => { + const { event, file, files, percent, type, XMLHttpRequest } = p; + updateFilesProgress(); + onProgress?.({ + e: event, + currentFiles: files, + file, + percent, + type, + XMLHttpRequest, + }); + }; + + // 多文件上传场景,单文件上传成功后 + const onResponseSuccess = (p: SuccessContext) => { + const { e, fileList, response } = p; + // 只有多个上传请求同时触发时才需 onOneFileSuccess + if (multiple) { + updateFilesProgress(); + onOneFileSuccess?.({ + e, + file: fileList[0], + response, + }); + } + }; + + const t = function (pattern: T, ...args: any[]) { + const [data] = args; + if (isString(pattern)) { + if (!data) return pattern; + const regular = /\{\s*([\w-]+)\s*\}/g; + return pattern.replace(regular, (match, key) => { + if (data) { + return String(data[key]); + } + return ''; + }); + } + if (isFunction(pattern)) { + // 重要:组件的渲染必须存在参数 h,不能移除 + if (!args.length) { + return null; + } + return pattern(...args); + } + return ''; + }; + + const getSizeLimitError: (sizeLimitObj: SizeLimitObj) => any = (sizeLimitObj: SizeLimitObj) => { + const limit = sizeLimitObj; + return limit.message + ? t(limit.message, { sizeLimit: limit.size }) + : `${t('', { sizeLimit: limit.size })} ${limit.unit}`; + }; + + const handleNotAutoUpload = (toFiles: UploadFile[]) => { + const tmpFiles = multiple && isBatchUpload ? uploadValue.concat(toFiles) : toFiles; + if (!tmpFiles.length) return; + const list = tmpFiles.map( + (file) => + new Promise((resolve) => { + getFileUrlByFileRaw(file.raw as File).then((url) => { + resolve({ ...file, url: file.url || url }); + }); + }), + ); + Promise.all(list).then((files) => { + setUploadValue(files as UploadFile[], { + trigger: 'add', + index: uploadValue.length, + file: toFiles[0], + files: toFiles, + }); + }); + setToUploadFiles([]); + }; + + const onFileChange = (files: File[]) => { + if (disabled) { + return; + } + const params = { currentSelectedFiles: formatToUploadFile([...files], props.format) }; + onSelectChange?.([...files], params); + validateFile({ + uploadValue, + files: [...files], + allowUploadDuplicateFile, + max, + sizeLimit, + isBatchUpload, + autoUpload, + format, + beforeUpload, + beforeAllFilesUpload, + }).then((args) => { + // 自定义全文件校验不通过 + if (args.validateResult?.type === 'BEFORE_ALL_FILES_UPLOAD') { + const params: ValidateParams = { type: 'BEFORE_ALL_FILES_UPLOAD', files: args.files as UploadFile[] }; + onValidate?.(params); + return; + } + // 文件数量校验不通过 + if (args.lengthOverLimit) { + const params: ValidateParams = { type: 'FILES_OVER_LENGTH_LIMIT', files: args.files as UploadFile[] }; + onValidate?.(params); + if (!(args.files as UploadFile[]).length) return; + } + // 过滤相同的文件名 + if (args.hasSameNameFile) { + const params: ValidateParams = { type: 'FILTER_FILE_SAME_NAME', files: args.files as UploadFile[] }; + onValidate?.(params); + } + // 文件大小校验结果处理(已过滤超出限制的文件) + if (args.fileValidateList instanceof Array) { + const { sizeLimitErrors, beforeUploadErrorFiles, toFiles } = getFilesAndErrors( + args.fileValidateList, + getSizeLimitError, + ); + const tmpWaitingFiles = autoUpload ? toFiles : [...toUploadFiles].concat(toFiles); + setToUploadFiles(tmpWaitingFiles); + // 文件大小处理 + if (sizeLimitErrors[0]) { + setSizeOverLimitMessage(sizeLimitErrors[0]?.file?.response?.error); + onValidate?.({ + type: 'FILE_OVER_SIZE_LIMIT', + files: sizeLimitErrors.map((t) => t.file), + }); + } else { + setSizeOverLimitMessage(''); + // 自定义方法 beforeUpload 拦截的文件 + if (beforeUploadErrorFiles.length) { + const params: ValidateParams = { type: 'CUSTOM_BEFORE_UPLOAD', files: beforeUploadErrorFiles }; + onValidate?.(params); + } + } + // 如果是自动上传 + if (autoUpload) { + uploadFiles(tmpWaitingFiles); + } else { + handleNotAutoUpload(tmpWaitingFiles); + } + } + }); + + // 清空 元素的文件,避免出现重复文件无法选择的情况 + if (inputRef.current) { + inputRef.current.value = ''; + } + }; + + const onNormalFileChange = (e: ChangeEvent) => { + const fileList = getFileList(e.target.files as FileList); + onFileChange?.(fileList); + }; + + /** + * 上传文件。对外暴露方法,修改时需谨慎 + * @param toFiles 本地上传的文件列表 + */ + function uploadFiles(toFiles?: UploadFile[]) { + const notUploadedFiles = uploadValue.filter((t) => t.status !== 'success'); + const files = autoUpload ? toFiles || toUploadFiles : notUploadedFiles; + if (!files || !files.length) { + return; + } + setUploading(true); + xhrReq.current = []; + upload({ + action, + headers, + method, + name, + withCredentials, + uploadedFiles: uploadValue, + toUploadFiles: files, + multiple, + isBatchUpload, + autoUpload, + uploadAllFilesInOneRequest, + useMockProgress, + data, + mockProgressDuration, + requestMethod, + formatRequest, + formatResponse, + onResponseProgress, + onResponseSuccess, + onResponseError, + setXhrObject: (xhr) => { + if (xhr.files[0]?.raw && xhrReq.current.find((item) => item.files[0]?.raw === xhr.files[0].raw)) { + return; + } + xhrReq.current = xhrReq.current.concat(xhr); + }, + }).then( + // 多文件场景时,全量文件完成后 + ({ status, data, list, failedFiles }) => { + setUploading(false); + if (status === 'success') { + if (autoUpload) { + setUploadValue([...(data?.files as UploadFile[])], { + trigger: 'add', + file: (data?.files as UploadFile[])[0], + }); + } + xhrReq.current = []; + onSuccess?.({ + fileList: data?.files, + currentFiles: files, + file: files[0], + // 只有全部请求完成后,才会存在该字段 + // @ts-ignore + results: list?.map((t) => t.data), + // 单文件单请求有一个 response,多文件多请求有多个 response + response: data?.response || list?.map((t) => t.data?.response), + XMLHttpRequest: data?.XMLHttpRequest, + }); + } else if (failedFiles?.[0]) { + onFail?.({ + e: data?.event, + file: failedFiles[0], + failedFiles, + currentFiles: files, + response: data?.response, + XMLHttpRequest: data?.XMLHttpRequest, + }); + } + + // 非自动上传,文件都在 uploadValue,不涉及 toUploadFiles + if (autoUpload) { + setToUploadFiles(failedFiles); + onWaitingUploadFilesChange?.({ files: failedFiles, trigger: 'uploaded' }); + } + }, + ); + } + + function onInnerRemove(p: UploadRemoveContext) { + setSizeOverLimitMessage(''); + p.e.stopPropagation?.(); + const changePrams: UploadChangeContext = { + e: p.e, + trigger: 'remove', + index: p.index, + file: p.file, + }; + // remove all files for batchUpload + if (isBatchUpload || !multiple) { + setToUploadFiles([]); + onWaitingUploadFilesChange?.({ files: [], trigger: 'remove' }); + setUploadValue([], changePrams); + } else if (!autoUpload) { + const curUploadValue = [...uploadValue]; + curUploadValue.splice(p.index, 1); + setUploadValue([...curUploadValue], changePrams); + } else { + // autoUpload 场景下, p.index < uploadValue.length 表示移除已经上传成功的文件;反之表示移除待上传列表文件 + // eslint-disable-next-line + if (p.index < uploadValue.length) { + const curUploadValue = [...uploadValue]; + curUploadValue.splice(p.index, 1); + setUploadValue([...curUploadValue], changePrams); + } else { + const curToUploadFiles = [...toUploadFiles]; + curToUploadFiles.splice(p.index - uploadValue.length, 1); + setToUploadFiles(curToUploadFiles); + onWaitingUploadFilesChange?.({ files: [...toUploadFiles], trigger: 'remove' }); + } + } + props.onRemove?.(p); + } + + const cancelUpload = (context?: { file?: UploadFile; e?: MouseEvent }) => { + xhrReq.current?.forEach((item) => { + item.xhrReq?.abort(); + }); + setUploading(false); + + if (autoUpload) { + setToUploadFiles((toUploadFiles) => toUploadFiles.map((item) => ({ ...item, status: 'waiting' }))); + } else { + const newUploadValue = uploadValue.map((item) => { + if (item.status !== 'success') { + return { ...item, status: 'waiting' }; + } + return item; + }); + setUploadValue(newUploadValue as UploadFile[], { trigger: 'abort' }); + } + + if (context?.file && !autoUpload) { + onInnerRemove?.({ file: context.file, e: context.e as MouseEvent, index: 0 }); + } + + onCancelUpload?.(); + }; + + return { + toUploadFiles, + uploadValue, + displayFiles, + sizeOverLimitMessage, + isUploading, + inputRef, + disabled, + xhrReq, + uploadFilePercent, + uploadFiles, + onFileChange, + onNormalFileChange, + onInnerRemove, + cancelUpload, + }; +} diff --git a/src/upload/index.ts b/src/upload/index.ts index 525b5a98..dbc3c9c7 100644 --- a/src/upload/index.ts +++ b/src/upload/index.ts @@ -3,7 +3,6 @@ import './style'; export type { UploadProps } from './Upload'; export * from './type'; -export * from './types'; export const Upload = _Upload; export default Upload; diff --git a/src/upload/style/index.js b/src/upload/style/index.js index f91323fb..62eeb1db 100644 --- a/src/upload/style/index.js +++ b/src/upload/style/index.js @@ -1 +1 @@ -import '../../_common/style/mobile/components/upload/_index.less'; +import '../../_common/style/mobile/components/upload/v2/_index.less'; diff --git a/src/upload/type.ts b/src/upload/type.ts index 78bf040d..a00e3386 100644 --- a/src/upload/type.ts +++ b/src/upload/type.ts @@ -4,80 +4,92 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode } from '../common'; +import { ImageProps } from '../image'; +import { PlainObject, TNode } from '../common'; import { MouseEvent } from 'react'; -export interface TdUploadProps { +export interface TdUploadProps { /** * 接受上传的文件类型,[查看 W3C示例](https://www.w3schools.com/tags/att_input_accept.asp),[查看 MDN 示例](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file) * @default '' */ accept?: string; /** - * 上传接口 + * 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片或文件 * @default '' */ action?: string; /** - * 是否选取文件后自动上传 + * 添加按钮内容。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效 + */ + addContent?: TNode; + /** + * 是否允许重复上传相同文件名的文件 + * @default false + */ + allowUploadDuplicateFile?: boolean; + /** + * 是否在选择文件后自动发起请求上传文件 * @default true */ autoUpload?: boolean; /** - * 上传文件之前的钩子,参数为上传的文件,返回值决定是否上传 + * 如果是自动上传模式 `autoUpload=true`,表示全部文件上传之前的钩子函数,函数参数为上传的文件,函数返回值决定是否继续上传,若返回值为 `false` 则终止上传。
如果是非自动上传模式 `autoUpload=false`,则函数返回值为 `false` 时表示本次选中的文件不会加入到文件列表中,即不触发 `onChange` 事件 + */ + beforeAllFilesUpload?: (file: UploadFile[]) => boolean | Promise; + /** + * 如果是自动上传模式 `autoUpload=true`,表示单个文件上传之前的钩子函数,若函数返回值为 `false` 则表示不上传当前文件。
如果是非自动上传模式 `autoUpload=false`,函数返回值为 `false` 时表示从上传文件中剔除当前文件 */ - beforeUpload?: (file: File | UploadFile) => boolean | Promise; + beforeUpload?: (file: UploadFile) => boolean | Promise; /** - * 触发上传的内容,同 trigger + * 非拖拽场景,指触发上传的元素,如:“选择文件”。如果是拖拽场景,则是指拖拽区域 */ children?: TNode; /** - * 上传文件时所需的额外数据 + * 上传请求所需的额外字段,默认字段有 `file`,表示文件信息。可以添加额外的文件名字段,如:`{file_name: "custom-file-name.txt"}`。`autoUpload=true` 时有效。也可以使用 `formatRequest` 完全自定义上传请求的字段 */ - data?: Record | ((file: File) => Record); + data?: Record | ((files: UploadFile[]) => Record); /** - * 触发上传的内容,同 trigger + * 是否禁用组件 */ - default?: TNode; + disabled?: boolean; /** - * 删除图标。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效。 + * 已上传文件列表,同 `value`。TS 类型:`UploadFile` + * @default [] */ - deleteBtn?: TNode; + files?: Array; /** - * 是否禁用 - * @default false + * 已上传文件列表,同 `value`。TS 类型:`UploadFile`,非受控属性 + * @default [] */ - disabled?: boolean; + defaultFiles?: Array; /** - * 已上传文件列表 + * 转换文件 `UploadFile` 的数据结构,可新增或修改 `UploadFile` 的属性,注意不能删除 `UploadFile` 属性。`action` 存在时有效 */ - files?: Array; + format?: (file: File) => UploadFile; /** - * 已上传文件列表,非受控属性 + * 用于新增或修改文件上传请求 参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`。
一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
可以使用 `name` 定义 `file` 字段的别名。
也可以使用 `formatRequest` 自定义任意字段,如添加一个字段 `fileList` ,存储文件数组 */ - defaultFiles?: Array; + formatRequest?: (requestData: { [key: string]: any }) => { [key: string]: any }; /** - * 文件上传前转换文件数据 + * 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。`action` 存在时有效。
此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接 */ - format?: (file: File) => UploadFile; + formatResponse?: (response: any, context: FormatResponseContext) => ResponseType; /** - * 用于格式化文件上传后的响应数据。error 用于显示错误提示,如果 error 值为真,组件会判定为上传失败;url 用于上传文件/图片地址。 + * 设置上传的请求头部,`action` 存在时有效 */ - formatResponse?: (response: any, context: FormatResponseContext) => ResponseType ; + headers?: { [key: string]: string }; /** - * upload组件每行上传图片列数以及图片的宽度和高度 + * 透传 Image 组件全部属性 */ - gridConfig?: { - column?: number; - width?: number; - height?: number; -}; + imageProps?: ImageProps; /** - * 设置上传的请求头部 + * 多个文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件。`theme=file-flow` 时有效 + * @default false */ - headers?: {[key: string]: string}; + isBatchUpload?: boolean; /** - * 用于控制文件上传数量,值为 0 则不限制 + * 用于控制文件上传数量,值为 0 则不限制。注意,单文件上传场景,请勿设置 `max` 属性 * @default 0 */ max?: number; @@ -85,31 +97,36 @@ export interface TdUploadProps { * HTTP 请求类型 * @default POST */ - method?: 'POST' | 'GET' | 'PUT' | 'OPTION'; + method?: 'POST' | 'GET' | 'PUT' | 'OPTIONS' | 'PATCH' | 'post' | 'get' | 'put' | 'options' | 'patch'; + /** + * 模拟进度间隔时间,单位:毫秒,默认:300。由于原始的上传请求,小文件上传进度只有 0 和 100,故而新增模拟进度,每间隔 `mockProgressDuration` 毫秒刷新一次模拟进度。小文件设置小一点,大文件设置大一点。注意:当 `useMockProgress` 为真时,当前设置有效 + */ + mockProgressDuration?: number; /** - * 是否支持多选文件 + * 支持多文件上传 * @default false */ multiple?: boolean; /** - * 占位符 - * @default '' + * 文件上传时的名称 + * @default file */ - placeholder?: string; + name?: string; /** - * 自定义上传方法。返回值 status 表示上传成功或失败,error 表示上传失败的原因,response 表示请求上传成功后的返回数据,response.url 表示上传成功后的图片地址。示例一:`{ status: 'fail', error: '上传失败', response }`。示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }` + * 自定义上传方法。返回值 `status` 表示上传成功或失败;`error` 或 `response.error` 表示上传失败的原因;
`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片/文件地址,`response.files` 表示一个请求上传多个文件/图片后的返回值。
示例一:`{ status: 'fail', error: '上传失败', response }`。
示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。
示例三:`{ status: 'success', files: [{ url: 'https://xxx.png', name: 'xxx.png' }]}` */ - requestMethod?: (files: UploadFile) => Promise; + requestMethod?: (files: UploadFile | UploadFile[]) => Promise; /** - * 图片文件大小限制,单位 KB。可选单位有:`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }` + * 图片文件大小限制,默认单位 KB。可选单位有:`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }` */ sizeLimit?: number | SizeLimitObj; /** - * 触发上传的内容 + * 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 + * @default false */ - trigger?: string | TNode; + uploadAllFilesInOneRequest?: boolean; /** - * 是否显示为模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传 + * 是否在请求时间超过 300ms 后显示模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传 * @default true */ useMockProgress?: boolean; @@ -123,19 +140,31 @@ export interface TdUploadProps { */ onCancelUpload?: () => void; /** - * 已上传文件列表发生变化时触发 + * 已上传文件列表发生变化时触发,`trigger` 表示触发本次的来源 */ - onChange?: (value: Array, context: UploadChangeContext) => void; + onChange?: (value: Array, context: UploadChangeContext) => void; /** - * 上传失败后触发 + * 点击上传区域时触发 */ - onFail?: (options: { e: ProgressEvent; file: UploadFile }) => void; + onClickUpload?: (context: { e: MouseEvent }) => void; /** - * 点击预览时触发 + * 上传失败后触发。`response` 指接口响应结果,`response.error` 会作为错误文本提醒。如果希望判定为上传失败,但接口响应数据不包含 `error` 字段,可以使用 `formatResponse` 格式化 `response` 数据结构。如果是多文件多请求上传场景,请到事件 `onOneFileFail` 中查看 `response` */ - onPreview?: (options: { file: UploadFile; e: MouseEvent }) => void; + onFail?: (options: UploadFailContext) => void; /** - * 上传进度变化时触发,真实进度和模拟进度都会触发。type 值为 real 表示真实上传进度,type 值为 mock 表示模拟上传进度 + * 多文件/图片场景下,单个文件上传失败后触发,如果一个请求上传一个文件,则会触发多次。单文件/图片不会触发 + */ + onOneFileFail?: (options: UploadFailContext) => void; + /** + * 单个文件上传成功后触发,在多文件场景下会触发多次。`context.file` 表示当前上传成功的单个文件,`context.response` 表示上传请求的返回数据 + */ + onOneFileSuccess?: (context: Pick) => void; + /** + * 点击图片预览时触发,文件没有预览 + */ + onPreview?: (options: { file: UploadFile; index: number; e: MouseEvent }) => void; + /** + * 上传进度变化时触发,真实进度和模拟进度都会触发。
⚠️ 原始上传请求,小文件的上传进度只有 0 和 100,故而不会触发 `progress` 事件;只有大文件才有真实的中间进度。如果你希望很小的文件也显示上传进度,保证 `useMockProgress=true` 的情况下,设置 `mockProgressDuration` 为更小的值。
参数 `options.type=real` 表示真实上传进度,`options.type=mock` 表示模拟上传进度 */ onProgress?: (options: ProgressContext) => void; /** @@ -143,12 +172,27 @@ export interface TdUploadProps { */ onRemove?: (context: UploadRemoveContext) => void; /** - * 上传成功后触发 + * 选择文件或图片之后,上传之前,触发该事件 + */ + onSelectChange?: (files: File[], context: UploadSelectChangeContext) => void; + /** + * 上传成功后触发。
`context.currentFiles` 表示当次请求上传的文件(无论成功或失败),`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
`context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
*/ onSuccess?: (context: SuccessContext) => void; + /** + * 文件上传校验结束事件,文件数量超出、文件大小超出限制、文件同名、`beforeAllFilesUpload` 返回值为假、`beforeUpload` 返回值为假等场景会触发。
注意:如果设置允许上传同名文件,即 `allowUploadDuplicateFile=true`,则不会因为文件重名触发该事件。
结合 `status` 和 `tips` 可以在组件中呈现不同类型的错误(或告警)提示 + */ + onValidate?: (context: { type: UploadValidateType; files: UploadFile[] }) => void; + /** + * 待上传文件列表发生变化时触发。`context.files` 表示事件参数为待上传文件,`context.trigger` 引起此次变化的触发来源 + */ + onWaitingUploadFilesChange?: (context: { + files: Array; + trigger: 'validate' | 'remove' | 'uploaded'; + }) => void; } -export interface UploadFile { +export interface UploadFile extends PlainObject { /** * 上一次变更的时间 */ @@ -167,9 +211,9 @@ export interface UploadFile { */ raw?: File; /** - * 上传接口返回的数据 + * 上传接口返回的数据。`response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片 */ - response?: object; + response?: { [key: string]: any }; /** * 文件大小 */ @@ -178,12 +222,17 @@ export interface UploadFile { * 文件上传状态:上传成功,上传失败,上传中,等待上传 * @default '' */ - status?: 'success' | 'fail' | 'progress' | 'waiting'; + status?: 'success' | 'fail' | 'progress' | 'waiting'; /** * 文件类型 * @default '' */ type?: string; + /** + * 上传时间 + * @default '' + */ + uploadTime?: string; /** * 文件上传成功后的下载/访问地址 * @default '' @@ -193,24 +242,81 @@ export interface UploadFile { export type ResponseType = { error?: string; url?: string } & Record; -export interface FormatResponseContext { file: UploadFile }; +export interface FormatResponseContext { + file: UploadFile; + currentFiles?: UploadFile[]; +} -export interface RequestMethodResponse { status: 'success' | 'fail'; error?: string; response: { url?: string; [key: string]: any } }; +export interface RequestMethodResponse { + status: 'success' | 'fail'; + error?: string; + response: { url?: string; files?: UploadFile[]; [key: string]: any }; +} -export interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }; +export interface SizeLimitObj { + size: number; + unit: SizeUnit; + message?: string; +} export type SizeUnitArray = ['B', 'KB', 'MB', 'GB']; export type SizeUnit = SizeUnitArray[number]; -export interface TriggerContext { dragActive?: boolean; uploadingFile?: UploadFile | Array }; +export interface UploadChangeContext { + e?: MouseEvent | ProgressEvent; + response?: any; + trigger: UploadChangeTrigger; + index?: number; + file?: UploadFile; + files?: UploadFile[]; +} + +export type UploadChangeTrigger = 'add' | 'remove' | 'abort' | 'progress-success' | 'progress' | 'progress-fail'; -export interface UploadChangeContext { e?: MouseEvent | ProgressEvent; response?: any; trigger: string; index?: number; file?: UploadFile }; +export interface UploadFailContext { + e?: ProgressEvent; + failedFiles: UploadFile[]; + currentFiles: UploadFile[]; + response?: any; + file: UploadFile; + XMLHttpRequest?: XMLHttpRequest; +} -export interface ProgressContext { e?: ProgressEvent; file: UploadFile; percent: number; type: UploadProgressType }; +export interface ProgressContext { + e?: ProgressEvent; + file?: UploadFile; + currentFiles: UploadFile[]; + percent: number; + type: UploadProgressType; + XMLHttpRequest?: XMLHttpRequest; +} export type UploadProgressType = 'real' | 'mock'; -export interface UploadRemoveContext { index?: number; file?: UploadFile; e: MouseEvent }; +export interface UploadRemoveContext { + index?: number; + file?: UploadFile; + e: MouseEvent; +} + +export interface UploadSelectChangeContext { + currentSelectedFiles: UploadFile[]; +} + +export interface SuccessContext { + e?: ProgressEvent; + file?: UploadFile; + fileList?: UploadFile[]; + currentFiles?: UploadFile[]; + response?: any; + results?: SuccessContext[]; + XMLHttpRequest?: XMLHttpRequest; +} -export interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; response: any }; +export type UploadValidateType = + | 'FILE_OVER_SIZE_LIMIT' + | 'FILES_OVER_LENGTH_LIMIT' + | 'FILTER_FILE_SAME_NAME' + | 'BEFORE_ALL_FILES_UPLOAD' + | 'CUSTOM_BEFORE_UPLOAD'; diff --git a/src/upload/types.ts b/src/upload/types.ts deleted file mode 100644 index 10e47a1f..00000000 --- a/src/upload/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { UploadFile } from './type'; - -export interface TdUploadFile extends UploadFile { - // fileList中每个文件的唯一标识 - uid?: string; -} diff --git a/src/upload/upload.en-US.md b/src/upload/upload.en-US.md new file mode 100644 index 00000000..3ddf8708 --- /dev/null +++ b/src/upload/upload.en-US.md @@ -0,0 +1,68 @@ +:: BASE_DOC :: + +## API + + +### Upload Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | className of component | N +style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +accept | String | - | File types that can be accepted. [W3C](https://www.w3schools.com/tags/att_input_accept.asp),[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file) | N +action | String | - | Uploading URL | N +addContent | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +allowUploadDuplicateFile | Boolean | false | allow to upload duplicate name files | N +autoUpload | Boolean | true | post upload request automatically after files being selected | N +beforeAllFilesUpload | Function | - | before all files upload, return false can stop uploading file。Typescript:`(file: UploadFile[]) => boolean \| Promise` | N +beforeUpload | Function | - | stop one of files to upload。Typescript:`(file: UploadFile) => boolean \| Promise` | N +children | TNode | - | to define upload trigger elements if `draggable=false`, to define drag elements if `draggable=true`。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +data | Object | - | extra request data of uploading. `formatRequest` can redefine all request data。Typescript:`Record \| ((files: UploadFile[]) => Record)` | N +disabled | Boolean | undefined | make upload to be disabled | N +files | Array | [] | Typescript:`Array` | N +defaultFiles | Array | [] | uncontrolled property。Typescript:`Array` | N +format | Function | - | to redefine `UploadFile` data structure。Typescript:`(file: File) => UploadFile` | N +formatRequest | Function | - | redefine request data。Typescript:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N +formatResponse | Function | - | redefine response data structure。Typescript:`(response: any, context: FormatResponseContext) => ResponseType` `type ResponseType = { error?: string; url?: string } & Record` ` interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +headers | Object | - | HTTP Request Header。Typescript:`{[key: string]: string}` | N +imageProps | Object | - | Typescript:`ImageProps`,[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +isBatchUpload | Boolean | false | make all files to be a whole package, files can only be replaced or deleted together, can not add more files | N +max | Number | 0 | max count of files limit | N +method | String | POST | HTTP request method。options: POST/GET/PUT/OPTIONS/PATCH/post/get/put/options/patch | N +mockProgressDuration | Number | - | mock progress duration time. more large files more duration time | N +multiple | Boolean | false | multiple files uploading | N +name | String | file | field name of files in upload request data | N +requestMethod | Function | - | custom upload request method。Typescript:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; files?: UploadFile[]; [key: string]: any } }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +sizeLimit | Number / Object | - | files size limit。Typescript:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +uploadAllFilesInOneRequest | Boolean | false | uploading all files in one request | N +useMockProgress | Boolean | true | use mock progress, instead of real progress | N +withCredentials | Boolean | false | uploading request with cookie | N +onCancelUpload | Function | | Typescript:`() => void`
trigger on cancel button click | N +onChange | Function | | Typescript:`(value: Array, context: UploadChangeContext) => void`
trigger on uploaded files change。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile; files?: UploadFile[] }`

`type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
| N +onClickUpload | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N +onFail | Function | | Typescript:`(options: UploadFailContext) => void`
`response.error` used for error tips, `formatResponse` can format `response`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadFailContext { e?: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile; XMLHttpRequest?: XMLHttpRequest}`
| N +onOneFileFail | Function | | Typescript:`(options: UploadFailContext) => void`
trigger on one file upload failed | N +onOneFileSuccess | Function | | Typescript:`(context: Pick) => void`
trigger on file uploaded successfully | N +onPreview | Function | | Typescript:`(options: { file: UploadFile; index: number; e: MouseEvent }) => void`
trigger on preview elements click | N +onProgress | Function | | Typescript:`(options: ProgressContext) => void`
uploading request progress event。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface ProgressContext { e?: ProgressEvent; file?: UploadFile; currentFiles: UploadFile[]; percent: number; type: UploadProgressType; XMLHttpRequest?: XMLHttpRequest }`

`type UploadProgressType = 'real' \| 'mock'`
| N +onRemove | Function | | Typescript:`(context: UploadRemoveContext) => void`
trigger on file removed。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadRemoveContext { index?: number; file?: UploadFile; e: MouseEvent }`
| N +onSelectChange | Function | | Typescript:`(files: File[], context: UploadSelectChangeContext) => void`
trigger after file choose and before upload。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadSelectChangeContext { currentSelectedFiles: UploadFile[] }`
| N +onSuccess | Function | | Typescript:`(context: SuccessContext) => void`
trigger on all files uploaded successfully。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[]; XMLHttpRequest?: XMLHttpRequest }`
| N +onValidate | Function | | Typescript:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
trigger on length over limit, or trigger on file size over limit。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME' \| 'BEFORE_ALL_FILES_UPLOAD' \| 'CUSTOM_BEFORE_UPLOAD'`
| N +onWaitingUploadFilesChange | Function | | Typescript:`(context: { files: Array, trigger: 'validate' \| 'remove' \| 'uploaded' }) => void`
trigger on waiting upload files changed | N + +### UploadFile + +name | type | default | description | required +-- | -- | -- | -- | -- +lastModified | Number | - | \- | N +name | String | - | \- | N +percent | Number | - | \- | N +raw | Object | - | Typescript:`File` | N +response | Object | - | Typescript:`{ [key: string]: any }` | N +size | Number | - | \- | N +status | String | - | Typescript:` 'success' \| 'fail' \| 'progress' \| 'waiting'` | N +type | String | - | \- | N +uploadTime | String | - | upload time | N +url | String | - | \- | N +`PlainObject` | \- | - | `PlainObject` is not an attribute of UploadFile,it means you can add and attributes to UploadFile, `type PlainObject = {[key: string]: any}`' | N diff --git a/src/upload/upload.md b/src/upload/upload.md index eaee976e..0767f258 100644 --- a/src/upload/upload.md +++ b/src/upload/upload.md @@ -2,52 +2,67 @@ ## API + ### Upload Props -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N accept | String | - | 接受上传的文件类型,[查看 W3C示例](https://www.w3schools.com/tags/att_input_accept.asp),[查看 MDN 示例](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file) | N -action | String | - | 上传接口 | N -autoUpload | Boolean | true | 是否选取文件后自动上传 | N -beforeUpload | Function | - | 上传文件之前的钩子,参数为上传的文件,返回值决定是否上传。TS 类型:`(file: File | UploadFile) => boolean | Promise` | N -children | TNode | N | 触发上传的内容,同 trigger -data | Object | - | 上传文件时所需的额外数据。TS 类型:`Record | ((file: File) => Record)` | N -default | String / Slot / Function | - | 触发上传的内容,同 trigger。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N -deleteBtn | String / Slot / Function | - | 删除图标。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效。。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N -disabled | Boolean | false | 是否禁用 | N -files | Array | - | 已上传文件列表。支持语法糖。TS 类型:`Array` | N -defaultFiles | Array | - | 已上传文件列表。非受控属性。TS 类型:`Array` | N -format | Function | - | 文件上传前转换文件数据。TS 类型:`(file: File) => UploadFile` | N -formatResponse | Function | - | 用于格式化文件上传后的响应数据。error 用于显示错误提示,如果 error 值为真,组件会判定为上传失败;url 用于上传文件/图片地址。。TS 类型:`(response: any, context: FormatResponseContext) => ResponseType `。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts) | N -gridConfig | Object | - | upload组件每行上传图片列数以及图片的宽度和高度。TS 类型:`{column?: number;width?: number;height?: number;}` | N -headers | Object | - | 设置上传的请求头部。TS 类型:`{[key: string]: string}` | N -max | Number | 0 | 用于控制文件上传数量,值为 0 则不限制 | N -method | String | POST | HTTP 请求类型。可选项:POST/GET/PUT/OPTION | N -multiple | Boolean | false | 是否支持多选文件 | N -placeholder | String | - | 占位符 | N -requestMethod | Function | - | 自定义上传方法。返回值 status 表示上传成功或失败,error 表示上传失败的原因,response 表示请求上传成功后的返回数据,response.url 表示上传成功后的图片地址。示例一:`{ status: 'fail', error: '上传失败', response }`。示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。TS 类型:`(files: UploadFile) => Promise`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts) | N -sizeLimit | Number / Object | - | 图片文件大小限制,单位 KB。可选单位有:`'B' | 'KB' | 'MB' | 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number | SizeLimitObj`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts) | N -trigger | String / Slot / Function | - | 触发上传的内容。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts) | N -useMockProgress | Boolean | true | 是否显示为模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传 | N +action | String | - | 上传接口。设接口响应数据为字段 `response`,那么 `response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片或文件 | N +addContent | TNode | - | 添加按钮内容。值为空,使用默认图标渲染;值为 slot 则表示使用插槽渲染;其他值无效。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +allowUploadDuplicateFile | Boolean | false | 是否允许重复上传相同文件名的文件 | N +autoUpload | Boolean | true | 是否在选择文件后自动发起请求上传文件 | N +beforeAllFilesUpload | Function | - | 如果是自动上传模式 `autoUpload=true`,表示全部文件上传之前的钩子函数,函数参数为上传的文件,函数返回值决定是否继续上传,若返回值为 `false` 则终止上传。
如果是非自动上传模式 `autoUpload=false`,则函数返回值为 `false` 时表示本次选中的文件不会加入到文件列表中,即不触发 `onChange` 事件。TS 类型:`(file: UploadFile[]) => boolean \| Promise` | N +beforeUpload | Function | - | 如果是自动上传模式 `autoUpload=true`,表示单个文件上传之前的钩子函数,若函数返回值为 `false` 则表示不上传当前文件。
如果是非自动上传模式 `autoUpload=false`,函数返回值为 `false` 时表示从上传文件中剔除当前文件。TS 类型:`(file: UploadFile) => boolean \| Promise` | N +children | TNode | - | 非拖拽场景,指触发上传的元素,如:“选择文件”。如果是拖拽场景,则是指拖拽区域。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +data | Object | - | 上传请求所需的额外字段,默认字段有 `file`,表示文件信息。可以添加额外的文件名字段,如:`{file_name: "custom-file-name.txt"}`。`autoUpload=true` 时有效。也可以使用 `formatRequest` 完全自定义上传请求的字段。TS 类型:`Record \| ((files: UploadFile[]) => Record)` | N +disabled | Boolean | undefined | 是否禁用组件 | N +files | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。TS 类型:`Array` | N +defaultFiles | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。非受控属性。TS 类型:`Array` | N +format | Function | - | 转换文件 `UploadFile` 的数据结构,可新增或修改 `UploadFile` 的属性,注意不能删除 `UploadFile` 属性。`action` 存在时有效。TS 类型:`(file: File) => UploadFile` | N +formatRequest | Function | - | 用于新增或修改文件上传请求 参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`。
一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
可以使用 `name` 定义 `file` 字段的别名。
也可以使用 `formatRequest` 自定义任意字段,如添加一个字段 `fileList` ,存储文件数组。TS 类型:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N +formatResponse | Function | - | 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。`action` 存在时有效。
此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接。TS 类型:`(response: any, context: FormatResponseContext) => ResponseType` `type ResponseType = { error?: string; url?: string } & Record` ` interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +headers | Object | - | 设置上传的请求头部,`action` 存在时有效。TS 类型:`{[key: string]: string}` | N +imageProps | Object | - | 透传 Image 组件全部属性。TS 类型:`ImageProps`,[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +isBatchUpload | Boolean | false | 多个文件是否作为一个独立文件包,整体替换,整体删除。不允许追加文件,只允许替换文件。`theme=file-flow` 时有效 | N +max | Number | 0 | 用于控制文件上传数量,值为 0 则不限制。注意,单文件上传场景,请勿设置 `max` 属性 | N +method | String | POST | HTTP 请求类型。可选项:POST/GET/PUT/OPTIONS/PATCH/post/get/put/options/patch | N +mockProgressDuration | Number | - | 模拟进度间隔时间,单位:毫秒,默认:300。由于原始的上传请求,小文件上传进度只有 0 和 100,故而新增模拟进度,每间隔 `mockProgressDuration` 毫秒刷新一次模拟进度。小文件设置小一点,大文件设置大一点。注意:当 `useMockProgress` 为真时,当前设置有效 | N +multiple | Boolean | false | 支持多文件上传 | N +name | String | file | 文件上传时的名称 | N +requestMethod | Function | - | 自定义上传方法。返回值 `status` 表示上传成功或失败;`error` 或 `response.error` 表示上传失败的原因;
`response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片/文件地址,`response.files` 表示一个请求上传多个文件/图片后的返回值。
示例一:`{ status: 'fail', error: '上传失败', response }`。
示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。
示例三:`{ status: 'success', files: [{ url: 'https://xxx.png', name: 'xxx.png' }]}`。TS 类型:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; files?: UploadFile[]; [key: string]: any } }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +sizeLimit | Number / Object | - | 图片文件大小限制,默认单位 KB。可选单位有:`'B' \| 'KB' \| 'MB' \| 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts) | N +uploadAllFilesInOneRequest | Boolean | false | 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 | N +useMockProgress | Boolean | true | 是否在请求时间超过 300ms 后显示模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传 | N withCredentials | Boolean | false | 上传请求时是否携带 cookie | N -onCancelUpload | Function | | 点击「取消上传」时触发。`() => {}` | N -onChange | Function | | 已上传文件列表发生变化时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts)。`(value: Array, context: UploadChangeContext) => {}` | N -onFail | Function | | 上传失败后触发。`(options: { e: ProgressEvent; file: UploadFile }) => {}` | N -onPreview | Function | | 点击预览时触发。`(options: { file: UploadFile; e: MouseEvent }) => {}` | N -onProgress | Function | | 上传进度变化时触发,真实进度和模拟进度都会触发。type 值为 real 表示真实上传进度,type 值为 mock 表示模拟上传进度。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts)。`(options: ProgressContext) => {}` | N -onRemove | Function | | 移除文件时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts)。`(context: UploadRemoveContext) => {}` | N -onSuccess | Function | | 上传成功后触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/upload/type.ts)。`(context: SuccessContext) => {}` | N +onCancelUpload | Function | | TS 类型:`() => void`
点击「取消上传」时触发 | N +onChange | Function | | TS 类型:`(value: Array, context: UploadChangeContext) => void`
已上传文件列表发生变化时触发,`trigger` 表示触发本次的来源。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadChangeContext { e?: MouseEvent \| ProgressEvent; response?: any; trigger: UploadChangeTrigger; index?: number; file?: UploadFile; files?: UploadFile[] }`

`type UploadChangeTrigger = 'add' \| 'remove' \| 'abort' \| 'progress-success' \| 'progress' \| 'progress-fail'`
| N +onClickUpload | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
点击上传区域时触发 | N +onFail | Function | | TS 类型:`(options: UploadFailContext) => void`
上传失败后触发。`response` 指接口响应结果,`response.error` 会作为错误文本提醒。如果希望判定为上传失败,但接口响应数据不包含 `error` 字段,可以使用 `formatResponse` 格式化 `response` 数据结构。如果是多文件多请求上传场景,请到事件 `onOneFileFail` 中查看 `response`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadFailContext { e?: ProgressEvent; failedFiles: UploadFile[]; currentFiles: UploadFile[]; response?: any; file: UploadFile; XMLHttpRequest?: XMLHttpRequest}`
| N +onOneFileFail | Function | | TS 类型:`(options: UploadFailContext) => void`
多文件/图片场景下,单个文件上传失败后触发,如果一个请求上传一个文件,则会触发多次。单文件/图片不会触发 | N +onOneFileSuccess | Function | | TS 类型:`(context: Pick) => void`
单个文件上传成功后触发,在多文件场景下会触发多次。`context.file` 表示当前上传成功的单个文件,`context.response` 表示上传请求的返回数据 | N +onPreview | Function | | TS 类型:`(options: { file: UploadFile; index: number; e: MouseEvent }) => void`
点击图片预览时触发,文件没有预览 | N +onProgress | Function | | TS 类型:`(options: ProgressContext) => void`
上传进度变化时触发,真实进度和模拟进度都会触发。
⚠️ 原始上传请求,小文件的上传进度只有 0 和 100,故而不会触发 `progress` 事件;只有大文件才有真实的中间进度。如果你希望很小的文件也显示上传进度,保证 `useMockProgress=true` 的情况下,设置 `mockProgressDuration` 为更小的值。
参数 `options.type=real` 表示真实上传进度,`options.type=mock` 表示模拟上传进度。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface ProgressContext { e?: ProgressEvent; file?: UploadFile; currentFiles: UploadFile[]; percent: number; type: UploadProgressType; XMLHttpRequest?: XMLHttpRequest }`

`type UploadProgressType = 'real' \| 'mock'`
| N +onRemove | Function | | TS 类型:`(context: UploadRemoveContext) => void`
移除文件时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadRemoveContext { index?: number; file?: UploadFile; e: MouseEvent }`
| N +onSelectChange | Function | | TS 类型:`(files: File[], context: UploadSelectChangeContext) => void`
选择文件或图片之后,上传之前,触发该事件。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface UploadSelectChangeContext { currentSelectedFiles: UploadFile[] }`
| N +onSuccess | Function | | TS 类型:`(context: SuccessContext) => void`
上传成功后触发。
`context.currentFiles` 表示当次请求上传的文件(无论成功或失败),`context.fileList` 表示上传成功后的文件,`context.response` 表示上传请求的返回数据。
`context.results` 表示单次选择全部文件上传成功后的响应结果,可以在这个字段存在时提醒用户上传成功或失败。
。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`interface SuccessContext { e?: ProgressEvent; file?: UploadFile; fileList?: UploadFile[]; currentFiles?: UploadFile[]; response?: any; results?: SuccessContext[]; XMLHttpRequest?: XMLHttpRequest }`
| N +onValidate | Function | | TS 类型:`(context: { type: UploadValidateType, files: UploadFile[] }) => void`
文件上传校验结束事件,文件数量超出、文件大小超出限制、文件同名、`beforeAllFilesUpload` 返回值为假、`beforeUpload` 返回值为假等场景会触发。
注意:如果设置允许上传同名文件,即 `allowUploadDuplicateFile=true`,则不会因为文件重名触发该事件。
结合 `status` 和 `tips` 可以在组件中呈现不同类型的错误(或告警)提示。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/upload/type.ts)。
`type UploadValidateType = 'FILE_OVER_SIZE_LIMIT' \| 'FILES_OVER_LENGTH_LIMIT' \| 'FILTER_FILE_SAME_NAME' \| 'BEFORE_ALL_FILES_UPLOAD' \| 'CUSTOM_BEFORE_UPLOAD'`
| N +onWaitingUploadFilesChange | Function | | TS 类型:`(context: { files: Array, trigger: 'validate' \| 'remove' \| 'uploaded' }) => void`
待上传文件列表发生变化时触发。`context.files` 表示事件参数为待上传文件,`context.trigger` 引起此次变化的触发来源 | N ### UploadFile -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- lastModified | Number | - | 上一次变更的时间 | N name | String | - | 文件名称 | N percent | Number | - | 下载进度 | N raw | Object | - | 原始文件对象。TS 类型:`File` | N -response | Object | - | 上传接口返回的数据 | N +response | Object | - | 上传接口返回的数据。`response.error` 存在时会判断此次上传失败,并显示错误文本信息;`response.url` 会作为文件上传成功后的地址,并使用该地址显示图片。TS 类型:`{ [key: string]: any }` | N size | Number | - | 文件大小 | N -status | String | - | 文件上传状态:上传成功,上传失败,上传中,等待上传。TS 类型:` 'success' | 'fail' | 'progress' | 'waiting'` | N +status | String | - | 文件上传状态:上传成功,上传失败,上传中,等待上传。TS 类型:` 'success' \| 'fail' \| 'progress' \| 'waiting'` | N type | String | - | 文件类型 | N +uploadTime | String | - | 上传时间 | N url | String | - | 文件上传成功后的下载/访问地址 | N +`PlainObject` | \- | - | `PlainObject` 不是 UploadFile 中的属性,而表示 UploadFile 本身支持添加任意属性,`type PlainObject = {[key: string]: any}`' | N diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 727169d6..bd0a07be 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -62741,6 +62741,701 @@ exports[`csr snapshot test > csr test src/toast/_example/theme.tsx 1`] = ` `; +exports[`csr snapshot test > csr test src/upload/_example/base.tsx 1`] = ` +
+
+

+ 上传图片 +

+
+
+
+ + + +
+
+ +
+
+
+`; + +exports[`csr snapshot test > csr test src/upload/_example/custom.tsx 1`] = ` +
+
+
+ 请上传身份证人像面 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ 请上传身份证国徽面 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+`; + +exports[`csr snapshot test > csr test src/upload/_example/multiple.tsx 1`] = ` +
+
+
+ 多选上传 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+ + + +
+
+ +
+
+
+`; + +exports[`csr snapshot test > csr test src/upload/_example/status.tsx 1`] = ` +
+
+
+ 上传图片 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+ 上传中... +
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+ 68% +
+
+ + + +
+
+
+ + + +
+
+ +
+
+
+ 上传失败 +
+
+
+ 上传图片 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+ 上传失败 +
+
+ + + +
+
+
+ + + +
+
+ +
+
+
+`; + exports[`ssr snapshot test > ssr test src/avatar/_example/action.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/avatar/_example/badgeAvatar.tsx 1`] = `"
A
8
12
"`; @@ -63268,3 +63963,11 @@ exports[`ssr snapshot test > ssr test src/toast/_example/cover.tsx 1`] = `"
ssr test src/toast/_example/index.tsx 1`] = `"

Toast 轻提示

用于轻量级反馈或提示,不会打断用户操作

01 组件类型

基础提示

02 组件状态

内置主题

03 显示遮罩

弹窗可显示遮罩,禁止滑动和点击

04 手动关闭

手动关闭轻提示

"`; exports[`ssr snapshot test > ssr test src/toast/_example/theme.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/base.tsx 1`] = `"

上传图片

"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/custom.tsx 1`] = `"
请上传身份证人像面
请上传身份证国徽面
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/multiple.tsx 1`] = `"
多选上传
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/status.tsx 1`] = `"
上传图片
上传中...
68%
上传失败
上传图片
上传失败
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index ea2a563b..f63cb94d 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -527,3 +527,11 @@ exports[`ssr snapshot test > ssr test src/toast/_example/cover.tsx 1`] = `"
ssr test src/toast/_example/index.tsx 1`] = `"

Toast 轻提示

用于轻量级反馈或提示,不会打断用户操作

01 组件类型

基础提示

02 组件状态

内置主题

03 显示遮罩

弹窗可显示遮罩,禁止滑动和点击

04 手动关闭

手动关闭轻提示

"`; exports[`ssr snapshot test > ssr test src/toast/_example/theme.tsx 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/base.tsx 1`] = `"

上传图片

"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/custom.tsx 1`] = `"
请上传身份证人像面
请上传身份证国徽面
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/multiple.tsx 1`] = `"
多选上传
"`; + +exports[`ssr snapshot test > ssr test src/upload/_example/status.tsx 1`] = `"
上传图片
上传中...
68%
上传失败
上传图片
上传失败
"`;