From a1ddb54cb864ec588132f237ce1c94ac5372f951 Mon Sep 17 00:00:00 2001 From: sheepluo Date: Sun, 29 Jan 2023 14:15:50 +0800 Subject: [PATCH] Upload Style & XHR (#1126) * feat(web): upload xhr * feat(web): upload * feat(web): upload dragger-btns * feat(web): upload buttn style * feat(web): upload --------- Co-authored-by: sheepluo --- docs/web/api/upload.md | 1 + js/upload/main.ts | 41 +++++++++++++------------ js/upload/types.ts | 18 ++++++++--- js/upload/utils.ts | 28 +---------------- js/upload/xhr.ts | 24 ++++++++++++--- style/web/components/upload/_index.less | 7 ++++- 6 files changed, 63 insertions(+), 56 deletions(-) diff --git a/docs/web/api/upload.md b/docs/web/api/upload.md index 1ec1ffe12a..74087a8d9f 100644 --- a/docs/web/api/upload.md +++ b/docs/web/api/upload.md @@ -60,6 +60,7 @@ spline: form - 上传组件中的全部文本,均可通过 `locale` 进行修改,也支持全局配置,查看文档。 - 如果拖拽上传单个文件,设置 `theme="file"` 和 `draggable=true`。 - 如果拖拽上传单张图片,设置 `theme="image"` 和 `draggable=true`。 +- 可以使用 `fileListDisplay` 自定义文件信息呈现内容。 {{ draggable }} diff --git a/js/upload/main.ts b/js/upload/main.ts index fba65a9171..46490f4d18 100644 --- a/js/upload/main.ts +++ b/js/upload/main.ts @@ -12,6 +12,7 @@ import { SuccessContext, handleSuccessParams, UploadTriggerUploadText, + ErrorContext, } from './types'; export interface BeforeUploadExtra { @@ -58,15 +59,12 @@ export function handleBeforeUpload( }); } -export interface OnErrorParams { - event?: ProgressEvent; - files?: UploadFile[]; - response?: any; +export interface OnErrorParams extends ErrorContext { formatResponse?: HandleUploadParams['formatResponse']; } export function handleError(options: OnErrorParams) { - const { event, files, response, formatResponse } = options; + const { event, files, response, XMLHttpRequest, formatResponse } = options; files.forEach((file) => { file.status = 'fail'; }); @@ -74,11 +72,11 @@ export function handleError(options: OnErrorParams) { if (typeof formatResponse === 'function') { res = formatResponse(response, { file: files[0], currentFiles: files }); } - return { response: res, event, files }; + return { response: res, event, files, XMLHttpRequest }; } export function handleSuccess(params: handleSuccessParams) { - const { event, files, response } = params; + const { event, files, response, XMLHttpRequest } = params; if (files?.length <= 0) { log.error('Upload', 'Empty File in Success Callback'); } @@ -89,7 +87,7 @@ export function handleSuccess(params: handleSuccessParams) { }); const res = response; files[0].url = res.url || files[0].url; - return { response: res, event, files }; + return { response: res, event, files, XMLHttpRequest }; } export type UploadRequestReturn = { @@ -173,7 +171,7 @@ export function uploadOneRequest(params: HandleUploadParams): Promise { + onError: (p: ErrorContext) => { const r = handleError({ ...p, formatResponse: params.formatResponse }); params.onResponseError?.(r); resolve({ status: 'fail', data: r }); @@ -272,11 +270,12 @@ Promise { } export function formatToUploadFile( - tmpFiles: File[], + files: File[], format: FileChangeParams['format'], - autoUpload: boolean, + status, + percent = 0, ) { - return tmpFiles.map((fileRaw: File) => { + return files.map((fileRaw: File) => { let file: UploadFile = fileRaw; if (typeof format === 'function') { file = format(fileRaw); @@ -287,8 +286,8 @@ export function formatToUploadFile( name: fileRaw.name, size: fileRaw.size, type: fileRaw.type, - percent: 0, - status: autoUpload ? 'progress' : 'waiting', + percent, + status, ...file, }; return uploadFile; @@ -311,11 +310,10 @@ export function validateFile( hasSameNameFile = true; } if (!tmpFiles.length) { - const tFiles = formatToUploadFile(files, params.format, params.autoUpload); + const tFiles = formatToUploadFile(files, params.format, params.autoUpload ? 'progress' : 'waiting'); resolve({ hasSameNameFile, file: tFiles?.[0], files: tFiles, validateResult: { type: 'FILTER_FILE_SAME_NAME' } }); return; } - // 上传文件数量限制 let lengthOverLimit = false; if (max && tmpFiles.length && !params.isBatchUpload) { @@ -326,7 +324,7 @@ export function validateFile( } // 格式化文件对象 - const formattedFiles = formatToUploadFile(tmpFiles, params.format, params.autoUpload); + const formattedFiles = formatToUploadFile(tmpFiles, params.format, params.autoUpload ? 'progress' : 'waiting'); // 全量文件,一波校验,整体上传 或 终止上传 let allFileValidatePromise; @@ -351,6 +349,7 @@ export function validateFile( })); Promise.all([allFileValidatePromise].concat(promiseList)).then((results) => { const [allFilesResult, ...others] = results; + // 如果 beforeAllFilesUpload 校验未通过 if (allFilesResult === false) { resolve({ lengthOverLimit, @@ -372,9 +371,13 @@ export function validateFile( export function getFilesAndErrors(fileValidateList: FileChangeReturn[], getError: (p: {[key: string]: any }) => string) { const sizeLimitErrors: FileChangeReturn[] = []; + const beforeUploadErrorFiles: UploadFile[] = []; const toFiles: UploadFile[] = []; fileValidateList.forEach((oneFile) => { - if (oneFile.validateResult?.type === 'CUSTOM_BEFORE_UPLOAD') return; + if (oneFile.validateResult?.type === 'CUSTOM_BEFORE_UPLOAD') { + beforeUploadErrorFiles.push(oneFile.file); + return; + } if (oneFile.validateResult?.type === 'FILE_OVER_SIZE_LIMIT') { if (!oneFile.file.response) { oneFile.file.response = {}; @@ -387,7 +390,7 @@ export function getFilesAndErrors(fileValidateList: FileChangeReturn[], getError toFiles.push(oneFile.file); }); - return { sizeLimitErrors, toFiles }; + return { sizeLimitErrors, beforeUploadErrorFiles, toFiles }; } /** diff --git a/js/upload/types.ts b/js/upload/types.ts index 1857fc7d3a..e2b7256023 100644 --- a/js/upload/types.ts +++ b/js/upload/types.ts @@ -51,6 +51,9 @@ export interface UploadFile { export interface RequestMethodResponse { status: 'success' | 'fail'; error?: string; + /** + * response.XMLHttpRequest is going to be deprecated + */ response: { url?: string; [key: string]: any } } @@ -74,10 +77,19 @@ export interface InnerProgressContext { XMLHttpRequest?: XMLHttpRequest; } +export interface ErrorContext { + event?: ProgressEvent; + file?: UploadFile; + files?: UploadFile[]; + response?: any; + XMLHttpRequest?: XMLHttpRequest; +} + export interface SuccessContext { event?: ProgressEvent; file?: UploadFile; files?: UploadFile[]; + XMLHttpRequest?: XMLHttpRequest; response?: RequestMethodResponse['response']; } @@ -108,11 +120,7 @@ export interface XhrOptions { name: string; /** 可与 data 共存 */ formatRequest?: (requestData: { [key: string]: any }) => { [key: string]: any }; - onError: ({ - event, file, files, response - }: { - event?: ProgressEvent; file?: UploadFile; files?: UploadFile[]; response?: any - }) => void; + onError: (context: ErrorContext) => void; onSuccess: (context: SuccessContext) => void; onProgress: (context: InnerProgressContext) => void; } diff --git a/js/upload/utils.ts b/js/upload/utils.ts index 7e295250d9..f5c5eb270a 100644 --- a/js/upload/utils.ts +++ b/js/upload/utils.ts @@ -1,4 +1,4 @@ -import { SizeUnit, TdUploadFile } from './types'; +import { SizeUnit } from './types'; import log from '../log/log'; /** @@ -146,32 +146,6 @@ export function isOverSizeLimit1( export const urlCreator = () => window.webkitURL || window.URL; -/** - * - * @param files 待处理文件 - * @param format 格式化规则 - */ -export function formatFiles( - files: File[] = [], - format?: (file: File) => TdUploadFile -) { - return 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', - ...file, - }; - uploadFile.url = urlCreator()?.createObjectURL(fileRaw); - return uploadFile; - }); -} - export function getFileUrlByFileRaw(fileRaw: File): Promise { return new Promise((resolve) => { if (!fileRaw) { diff --git a/js/upload/xhr.ts b/js/upload/xhr.ts index 33520c9a35..a580c30c58 100644 --- a/js/upload/xhr.ts +++ b/js/upload/xhr.ts @@ -58,14 +58,17 @@ export default function xhr({ let requestData: { [key: string]: any } = {}; if (data) { - const extraData = typeof data === 'function' ? data(file) : data; + const extraData = typeof data === 'function' ? data(innerFiles) : data; Object.assign(requestData, extraData); } innerFiles.forEach((file, index) => { const fileField = innerFiles.length > 1 ? `${name}[${index}]` : name; requestData[fileField] = file.raw; - requestData[name] = file.raw; }); + if (innerFiles.length === 1) { + requestData[name] = innerFiles[0].raw; + } + requestData.length = innerFiles.length; if (formatRequest) { requestData = formatRequest(requestData); @@ -84,11 +87,15 @@ export default function xhr({ }); xhr.onerror = (event: ProgressEvent) => { - onError({ event, file, files: innerFiles }); + onError({ event, file, files: innerFiles, XMLHttpRequest: xhr, }); clearInterval(timer1); clearTimeout(timer2); }; + xhr.ontimeout = (event) => { + onError({ event, file, files: innerFiles, XMLHttpRequest: xhr, }); + }; + if (xhr.upload) { xhr.upload.onprogress = (event: ProgressEvent) => { let realPercent = 0; @@ -117,7 +124,11 @@ export default function xhr({ const isFail = xhr.status < 200 || xhr.status >= 300; if (isFail) { return onError({ - event, file, files: innerFiles, response + event, + file, + files: innerFiles, + response, + XMLHttpRequest: xhr, }); } const text = xhr.responseText || xhr.response; @@ -142,11 +153,16 @@ export default function xhr({ event, file: file || innerFiles[0], files: [...innerFiles], + XMLHttpRequest: xhr, response, }); }; xhr.send(formData); + // @ts-ignore + xhr.upload.requestParams = requestData; + // @ts-ignore + xhr.upload.requestHeaders = headers; return xhr; } diff --git a/style/web/components/upload/_index.less b/style/web/components/upload/_index.less index 35173a8970..dbe9e73604 100644 --- a/style/web/components/upload/_index.less +++ b/style/web/components/upload/_index.less @@ -502,7 +502,8 @@ } } -.@{prefix}-upload__dragger-progress-cancel { +.@{prefix}-upload__dragger-progress-cancel, +.@{prefix}-upload__dragger-progress-reupload { margin-right: @upload-dragger-progress-cancel-margin; &:hover { @@ -664,3 +665,7 @@ .@{prefix}-upload--theme-file-input { width: 100%; } + +.@{prefix}-upload__dragger-btns > .@{prefix}-button { + padding: 0; +}