diff --git a/devui/style/theme/_animation.scss b/devui/style/theme/_animation.scss index 7604080d66a3fa903dc644d4f15af3e5e3154523..33d65e180d566145edc0bce6e1ab5a6f36f21d35 100644 --- a/devui/style/theme/_animation.scss +++ b/devui/style/theme/_animation.scss @@ -2,8 +2,28 @@ $devui-animation-duration-slow: var(--devui-animation-duration-slow, 300ms); $devui-animation-duration-base: var(--devui-animation-duration-base, 200ms); $devui-animation-duration-fast: var(--devui-animation-duration-fast, 100ms); -$devui-animation-ease-in: var(--devui-animation-ease-in, cubic-bezier(0.5, 0, 0.84, 0.25)); -$devui-animation-ease-out: var(--devui-animation-ease-out, cubic-bezier(0.16, 0.75, 0.5, 1)); -$devui-animation-ease-in-out: var(--devui-animation-ease-in-out, cubic-bezier(0.5, 0.05, 0.5, 0.95)); -$devui-animation-ease-in-smooth: var(--devui-animation-ease-in-smooth, cubic-bezier(0.645, 0.045, 0.355, 1)); -$devui-animation-linear: var(--devui-animation-linear, cubic-bezier(0, 0, 1, 1)); +$devui-animation-ease-in: var( + --devui-animation-ease-in, + cubic-bezier(0.5, 0, 0.84, 0.25) +); +$devui-animation-ease-out: var( + --devui-animation-ease-out, + cubic-bezier(0.16, 0.75, 0.5, 1) +); +$devui-animation-ease-in-out: var( + --devui-animation-ease-in-out, + cubic-bezier(0.5, 0.05, 0.5, 0.95) +); +$devui-animation-ease-in-smooth: var( + --devui-animation-ease-in-smooth, + cubic-bezier(0.645, 0.045, 0.355, 1) +); +// ng库中中把 $devui-animation-ease-in-smooth 重命名为了 $devui-animation-ease-in-out-smooth +$devui-animation-ease-in-out-smooth: var( + --devui-animation-ease-in-out-smooth, + cubic-bezier(0.645, 0.045, 0.355, 1) +); +$devui-animation-linear: var( + --devui-animation-linear, + cubic-bezier(0, 0, 1, 1) +); diff --git a/devui/upload/index.ts b/devui/upload/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6283e55c4162fbe84a8ef04bad82670f8e8f17a7 --- /dev/null +++ b/devui/upload/index.ts @@ -0,0 +1,20 @@ +import type { App } from 'vue' +import Upload from './src/single-upload' +import MultiUpload from './src/multiple-upload' +import fileDropDirective from './src/file-drop-directive' + +Upload.install = function (app: App) { + app.directive('file-drop', fileDropDirective) + app.component(Upload.name, Upload) + app.component(MultiUpload.name, MultiUpload) +} + +export { Upload, MultiUpload } + +export default { + title: 'Upload 上传', + category: '数据录入', + install(app: App): void { + app.use(Upload as any) + }, +} diff --git a/devui/upload/src/file-drop-directive.ts b/devui/upload/src/file-drop-directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..eae3e67e741af3225e02ed66703d2d299bb87ca3 --- /dev/null +++ b/devui/upload/src/file-drop-directive.ts @@ -0,0 +1,86 @@ +interface BindingType { + value: { + enableDrop?: boolean + isSingle?: boolean + onFileDrop?: (files: File[]) => void + onFileOver?: (event: any) => void + } +} + +const getTransfer = (event: any) => { + return event.dataTransfer + ? event.dataTransfer + : event.originalEvent.dataTransfer +} + +const haveFiles = (types: any) => { + if (!types) { + return false + } + + if (types.indexOf) { + return types.indexOf('Files') !== -1 + } else if (types.contains) { + return types.contains('Files') + } else { + return false + } +} + +const preventAndStop = (event: any) => { + event.preventDefault() + event.stopPropagation() +} + +const onDragOver = (el: HTMLElement, binding: BindingType) => { + const { onFileOver } = binding.value + el.addEventListener('dragover', (event) => { + const transfer = getTransfer(event) + if (!haveFiles(transfer.types)) { + return + } + preventAndStop(event) + onFileOver && onFileOver(true) + }) +} + +const onDragLeave = (el: HTMLElement, binding: BindingType) => { + const { onFileOver } = binding.value + el.addEventListener('dragleave', (event) => { + if (event.currentTarget === el) { + return + } + preventAndStop(event) + onFileOver && onFileOver(true) + }) +} + +const onDrop = (el: HTMLElement, binding: BindingType) => { + const { onFileDrop, isSingle } = binding.value + el.addEventListener('drop', (event) => { + const transfer = getTransfer(event) + if (!transfer) { + return + } + preventAndStop(event) + if (isSingle) { + onFileDrop && onFileDrop([transfer.files[0]]) + } else { + onFileDrop && onFileDrop(transfer.files) + } + }) +} + +const fileDropDirective = { + mounted: (el: HTMLElement, binding: BindingType): void => { + const { enableDrop } = binding.value + if (!enableDrop) { + return + } + onDragOver(el, binding) + onDragLeave(el, binding) + onDrop(el, binding) + }, +} + +export default fileDropDirective diff --git a/devui/upload/src/file-uploader.ts b/devui/upload/src/file-uploader.ts new file mode 100755 index 0000000000000000000000000000000000000000..1acef9d914ee0521119d18474f9ca1aa34bbf74c --- /dev/null +++ b/devui/upload/src/file-uploader.ts @@ -0,0 +1,133 @@ +import { IUploadOptions, UploadStatus } from './upload-types' + +export class FileUploader { + private xhr: XMLHttpRequest + public status: UploadStatus + public response: any + public percentage = 0 + + constructor(public file: File, public uploadOptions: IUploadOptions) { + this.file = file + this.uploadOptions = uploadOptions + this.status = UploadStatus.preLoad + } + + send(uploadFiles?: FileUploader[]): Promise<{ file: File; response: any; }> { + return new Promise((resolve, reject) => { + const { + uri, + method, + headers, + authToken, + authTokenHeader, + additionalParameter, + fileFieldName, + withCredentials, + responseType, + } = this.uploadOptions + const authTokenHeader_ = authTokenHeader || 'Authorization' + const fileFieldName_ = fileFieldName || 'file' + + this.xhr = new XMLHttpRequest() + this.xhr.open(method || 'POST', uri) + + if (withCredentials) { + this.xhr.withCredentials = withCredentials + } + + if (responseType) { + this.xhr.responseType = responseType + } + + if (authToken) { + this.xhr.setRequestHeader(authTokenHeader_, authToken) + } + + if (headers) { + Object.keys(headers).forEach((key) => { + this.xhr.setRequestHeader(key, headers[key]) + }) + } + + this.xhr.upload.onprogress = (e) => { + this.percentage = Math.round((e.loaded * 100) / e.total) + } + + const formData = + uploadFiles && uploadFiles.length + ? this.oneTimeUploadFiles( + fileFieldName_, + additionalParameter, + uploadFiles + ) + : this.parallelUploadFiles(fileFieldName_, additionalParameter) + + this.xhr.send(formData) + this.status = UploadStatus.uploading + + this.xhr.onabort = () => { + this.status = UploadStatus.preLoad + this.xhr = null + } + + this.xhr.onerror = () => { + this.response = this.xhr.response + this.status = UploadStatus.failed + reject({ file: this.file, response: this.xhr.response }) + } + + this.xhr.onload = () => { + if ( + this.xhr.readyState === 4 && + this.xhr.status >= 200 && + this.xhr.status < 300 + ) { + this.response = this.xhr.response + this.status = UploadStatus.uploaded + resolve({ file: this.file, response: this.xhr.response }) + } else { + this.response = this.xhr.response + this.status = UploadStatus.failed + reject({ file: this.file, response: this.xhr.response }) + } + } + }) + } + + parallelUploadFiles( + fileFieldName_: string, + additionalParameter: Record + ): FormData { + const formData = new FormData() + formData.append(fileFieldName_, this.file, this.file.name) + if (additionalParameter) { + Object.keys(additionalParameter).forEach((key: string) => { + formData.append(key, additionalParameter[key]) + }) + } + return formData + } + + oneTimeUploadFiles( + fileFieldName_: string, + additionalParameter: Record, + uploadFiles: FileUploader[] + ): FormData { + const formData = new FormData() + uploadFiles.forEach((element) => { + formData.append(fileFieldName_, element.file, element.file.name) + if (additionalParameter) { + Object.keys(additionalParameter).forEach((key: string) => { + formData.append(key, additionalParameter[key]) + }) + } + }) + return formData + } + + cancel(): void { + if (this.xhr) { + this.xhr.abort() + } + } +} diff --git a/devui/upload/src/i18n-upload.ts b/devui/upload/src/i18n-upload.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2cbbc3820f514cde84b2dcba6344952a697d3f7 --- /dev/null +++ b/devui/upload/src/i18n-upload.ts @@ -0,0 +1,42 @@ +export const i18nText = { + warning: '提醒', + upload: '上传', + chooseFile: '选择文件', + chooseFiles: '选择多个文件', + preload: '预加载', + uploading: '上传中...', + uploaded: '已上传', + uploadFailed: '上传失败', + uploadSuccess: '上传成功!', + delete: '删除', + reUpload: '重新上传', + cancelUpload: '取消上传', +} + +export const getFailedFilesCount = (failedCount: number): string => + `${failedCount}个文件上传失败!` +export const getUploadingFilesCount = ( + uploadingCount: number, + filesCount: number +): string => `${uploadingCount}/${filesCount}正在上传` +export const getSelectedFilesCount = (filesCount: number): string => + `已添加${filesCount}个文件` +export const getAllFilesBeyondMaximalFileSizeMsg = ( + maximalSize: number +): string => + `最大支持上传${maximalSize}MB的文件, 您本次上传的所有文件超过可上传文件大小` +export const getBeyondMaximalFileSizeMsg = ( + filename: string, + maximalSize: number +): string => { + return `最大支持上传${maximalSize}MB的文件, 您上传的文件"${filename}"超过可上传文件大小` +} +export const getNotAllowedFileTypeMsg = ( + filename: string, + scope: string +): string => { + return `支持的文件类型: "${scope}", 您上传的文件"${filename}"不在允许范围内,请重新选择文件` +} +export const getExistSameNameFilesMsg = (sameNames: string): string => { + return `您上传的 "${sameNames}" 存在重名文件, 请重新选择文件` +} diff --git a/devui/upload/src/multiple-upload.tsx b/devui/upload/src/multiple-upload.tsx new file mode 100644 index 0000000000000000000000000000000000000000..da3a23157352bf820610386afb6b0361ca6b9015 --- /dev/null +++ b/devui/upload/src/multiple-upload.tsx @@ -0,0 +1,436 @@ +import './upload.scss' + +import { defineComponent, toRefs, ref } from 'vue' +import { Observable } from 'rxjs' +import { last, map, debounceTime } from 'rxjs/operators' +import { ToastService } from '../../toast' +import { UploadStatus, multiUploadProps } from './upload-types' +import { useSelectFiles } from './use-select-files' +import { useUpload } from './use-upload' +import { + getFailedFilesCount, + getSelectedFilesCount, + getUploadingFilesCount, + i18nText, + getExistSameNameFilesMsg, +} from './i18n-upload' +import { FileUploader } from './file-uploader' + +export default defineComponent({ + name: 'DMultipleUpload', + props: multiUploadProps, + emits: [ + 'fileDrop', + 'fileOver', + 'fileSelect', + 'successEvent', + 'errorEvent', + 'deleteUploadedFileEvent', + 'update:uploadedFiles', + ], + setup(props, ctx) { + const { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + oneTimeUpload, + showTip, + uploadedFiles, + } = toRefs(props) + const { + triggerSelectFiles, + _validateFiles, + triggerDropFiles, + checkAllFilesSize, + } = useSelectFiles() + const { + getFiles, + fileUploaders, + addFile, + getFullFiles, + deleteFile, + upload, + resetSameNameFiles, + removeFiles, + _oneTimeUpload, + getSameNameFiles, + } = useUpload() + const isDropOVer = ref(false) + const uploadTips = ref('') + const alertMsg = (errorMsg: string) => { + ToastService.open({ + value: [{ severity: 'warn', content: errorMsg }], + }) + } + const checkValid = () => { + let totalFileSize = 0 + + fileUploaders.value.forEach((fileUploader) => { + totalFileSize += fileUploader.file.size + + const checkResult = _validateFiles( + fileUploader.file, + fileOptions.value.accept, + fileUploader.uploadOptions + ) + if (checkResult && checkResult.checkError) { + deleteFile(fileUploader.file) + alertMsg(checkResult.errorMsg) + return + } + }) + + if (oneTimeUpload.value) { + const checkResult = checkAllFilesSize( + totalFileSize, + uploadOptions.value.maximumSize + ) + if (checkResult && checkResult.checkError) { + removeFiles() + alertMsg(checkResult.errorMsg) + } + } + } + + const _dealFiles = (observale) => { + resetSameNameFiles() + observale + .pipe( + map((file) => { + addFile(file, uploadOptions.value) + return file + }), + debounceTime(100) + ) + .subscribe( + () => { + checkValid() + const sameNameFiles = getSameNameFiles() + if (uploadOptions.value.checkSameName && sameNameFiles.length) { + alertMsg(getExistSameNameFilesMsg(sameNameFiles)) + } + // TODO: onChange事件 + const selectedFiles = fileUploaders.value + .filter( + (fileUploader) => fileUploader.status === UploadStatus.preLoad + ) + .map((fileUploader) => fileUploader.file) + ctx.emit('fileSelect', selectedFiles) + if (autoUpload.value) { + upload() + } + }, + (error: Error) => { + alertMsg(error.message) + } + ) + } + + const handleClick = () => { + if (disabled.value) { + return + } + _dealFiles(triggerSelectFiles(fileOptions.value)) + } + + const onFileDrop = (files: File[]) => { + isDropOVer.value = false + _dealFiles( + triggerDropFiles(fileOptions.value, uploadOptions.value, files) + ) + ctx.emit('fileDrop', files) + } + const onFileOver = (event: boolean) => { + isDropOVer.value = event + ctx.emit('fileOver', event) + } + const onDeleteFile = (event: Event, file: File) => { + event.stopPropagation() + deleteFile(file) + } + // 删除已上传文件 + const deleteUploadedFile = (file: File) => { + const newUploadedFiles = uploadedFiles.value.filter((uploadedFile) => { + return uploadedFile.name !== file.name + }) + ctx.emit('deleteUploadedFileEvent', file) + ctx.emit('update:uploadedFiles', newUploadedFiles) + } + const canUpload = () => { + let uploadResult = Promise.resolve(true) + if (beforeUpload.value) { + const result: any = beforeUpload.value(getFullFiles()) + if (typeof result !== 'undefined') { + if (result.then) { + uploadResult = result + } else if (result.subscribe) { + uploadResult = (result as Observable).toPromise() + } else { + uploadResult = Promise.resolve(result) + } + } + } + return uploadResult + } + const fileUpload = (event: Event, fileUploader?: FileUploader) => { + if (event) { + event.stopPropagation() + } + canUpload().then((_canUpload) => { + if (!_canUpload) { + removeFiles() + return + } + const uploadObservable = oneTimeUpload.value + ? _oneTimeUpload() + : upload(fileUploader) + uploadObservable.pipe(last()).subscribe( + (results: Array<{ file: File; response: any; }>) => { + ctx.emit('successEvent', results) + const newFiles = results.map((result) => result.file) + const newUploadedFiles = [...newFiles, ...uploadedFiles.value] + ctx.emit('update:uploadedFiles', newUploadedFiles) + }, + (error) => { + ctx.emit('errorEvent', error) + } + ) + }) + } + + const getStatus = () => { + let uploadingCount = 0 + let uploadedCount = 0 + let failedCount = 0 + const filesCount = fileUploaders.value.length + fileUploaders.value.forEach((fileUploader) => { + if (fileUploader.status === UploadStatus.uploading) { + uploadingCount++ + } else if (fileUploader.status === UploadStatus.uploaded) { + uploadedCount++ + } else if (fileUploader.status === UploadStatus.failed) { + failedCount++ + } + }) + if (failedCount > 0) { + uploadTips.value = getFailedFilesCount(failedCount) + return 'failed' + } + if (uploadingCount > 0) { + uploadTips.value = getUploadingFilesCount(uploadingCount, filesCount) + return 'uploading' + } + if (uploadedCount === filesCount && uploadedCount !== 0) { + return 'uploaded' + } + if (filesCount !== 0) { + uploadTips.value = getSelectedFilesCount(filesCount) + return 'selected' + } + } + + // 取消上传 + const cancelUpload = () => { + fileUploaders.value = fileUploaders.value.map((fileUploader) => { + if (fileUploader.status === UploadStatus.uploading) { + // 取消上传请求 + fileUploader.cancel() + fileUploader.status = UploadStatus.failed + } + return fileUploader + }) + } + + return { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + isDropOVer, + onFileDrop, + onFileOver, + handleClick, + fileUploaders, + onDeleteFile, + fileUpload, + showTip, + getStatus, + uploadTips, + cancelUpload, + deleteUploadedFile, + } + }, + render() { + const { + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + isDropOVer, + onFileDrop, + onFileOver, + handleClick, + fileUploaders, + onDeleteFile, + fileUpload, + showTip, + getStatus, + uploadTips, + cancelUpload, + uploadedFiles, + deleteUploadedFile, + } = this + return ( + <> +
+
+ {fileUploaders.length === 0 && ( +
+ {placeholderText} +
+ )} + {fileUploaders.length > 0 && ( +
    + {fileUploaders.map((fileUploader, index) => ( +
  • + + {fileUploader.file.name} + + + onDeleteFile(event, fileUploader.file) + } + /> + {fileUploader.status === UploadStatus.uploading && ( +
    + +
    + )} + {fileUploader.status === UploadStatus.failed && ( + + )} + {fileUploader.status === UploadStatus.uploaded && ( + + )} +
  • + ))} +
+ )} + + + + + +
+ {!autoUpload && !withoutBtn && ( + + {uploadText} + + )} +
+ {showTip && ( +
+ {getStatus() === 'selected' && ( + {uploadTips} + )} + {getStatus() === 'uploading' && ( + + {uploadTips} + {i18nText.cancelUpload} + + )} + {getStatus() === 'uploaded' && ( +
+ + + {i18nText.uploadSuccess} + +
+ )} + {getStatus() === 'failed' && ( +
+ + + {uploadTips} + {i18nText.reUpload} + +
+ )} +
+ )} +
+ {this.$slots.preloadFiles?.({ + fileUploaders, + deleteFile: onDeleteFile, + })} +
+
+ {this.$slots.uploadedFiles?.({ + uploadedFiles, + deleteFile: deleteUploadedFile, + })} +
+ + ) + }, +}) diff --git a/devui/upload/src/single-upload-view.tsx b/devui/upload/src/single-upload-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..33d4f981a6d4d9a19e3feff00da7b20cf01cffb8 --- /dev/null +++ b/devui/upload/src/single-upload-view.tsx @@ -0,0 +1,14 @@ +import './upload.scss' + +import { defineComponent } from 'vue' +import { singleUploadViewProps } from './upload-types' + +export default defineComponent({ + name: 'DSingleUploadView', + props: singleUploadViewProps, + // setup(props) {}, + render() { + const {} = this + return
+ }, +}) diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9ceb1f5db15568daaa9e0c309e5865d135ef2631 --- /dev/null +++ b/devui/upload/src/single-upload.tsx @@ -0,0 +1,338 @@ +import './upload.scss' + +import { defineComponent, toRefs, computed, ref } from 'vue' +import { Observable } from 'rxjs' +import { last, map } from 'rxjs/operators' +import { ToastService } from '../../toast' +import { uploadProps, UploadProps, UploadStatus } from './upload-types' +import { useUpload } from './use-upload' +import { useSelectFiles } from './use-select-files' +import { i18nText } from './i18n-upload' +export default defineComponent({ + name: 'DSingleUpload', + props: uploadProps, + emits: [ + 'fileDrop', + 'fileOver', + 'fileSelect', + 'successEvent', + 'errorEvent', + 'update:uploadedFiles', + ], + setup(props: UploadProps, ctx) { + const { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + showTip, + uploadedFiles, + } = toRefs(props) + const isDropOVer = ref(false) + const { + getFiles, + fileUploaders, + addFile, + getFullFiles, + deleteFile, + upload, + } = useUpload() + const { triggerSelectFiles, _validateFiles, triggerDropFiles } = + useSelectFiles() + const filename = computed(() => (getFiles()[0] || {}).name || '') + + const alertMsg = (errorMsg: string) => { + ToastService.open({ + value: [{ severity: 'warn', content: errorMsg }], + }) + } + + const canUpload = () => { + let uploadResult = Promise.resolve(true) + if (beforeUpload.value) { + const result: any = beforeUpload.value( + (getFullFiles()[0] as unknown as File) || ({} as File) + ) + if (typeof result !== 'undefined') { + if (result.then) { + uploadResult = result + } else if (result.subscribe) { + uploadResult = (result as Observable).toPromise() + } else { + uploadResult = Promise.resolve(result) + } + } + } + return uploadResult + } + + const fileUpload = () => { + canUpload().then((canUpload) => { + if (!canUpload) { + return + } + upload() + .pipe(last()) + .subscribe( + (results: Array<{ file: File; response: any; }>) => { + ctx.emit('successEvent', results) + const newFiles = results.map((result) => result.file) + const newUploadedFiles = [...newFiles, ...uploadedFiles.value] + ctx.emit('update:uploadedFiles', newUploadedFiles) + }, + (error) => { + console.error(error) + if (fileUploaders.value[0]) { + fileUploaders.value[0].percentage = 0 + } + ctx.emit('errorEvent', error) + } + ) + }) + } + + const checkValid = () => { + fileUploaders.value.forEach((fileUploader) => { + const checkResult = _validateFiles( + fileUploader.file, + fileOptions.value.accept, + fileUploader.uploadOptions + ) + if (checkResult.checkError) { + deleteFile(fileUploader.file) + alertMsg(checkResult.errorMsg) + } + }) + } + + const _dealFiles = (observale) => { + observale + .pipe( + map((file) => { + addFile(file, uploadOptions.value) + return file + }) + ) + .subscribe( + () => { + checkValid() + const file = fileUploaders[0]?.file + if (props.onChange) { + props.onChange(file) + } + if (file) { + ctx.emit('fileSelect', file) + } + if (autoUpload.value) { + fileUpload() + } + }, + (error: Error) => { + alertMsg(error.message) + } + ) + } + + const handleClick = () => { + if ( + disabled.value || + (fileUploaders.value[0] && + fileUploaders.value[0]?.status === UploadStatus.uploading) + ) { + return + } + _dealFiles(triggerSelectFiles(fileOptions.value)) + } + + const onDeleteFile = (event: Event) => { + event.stopPropagation() + const files = getFiles() + deleteFile(files[0]) + } + const onFileDrop = (files: File[]) => { + isDropOVer.value = false + _dealFiles( + triggerDropFiles(fileOptions.value, uploadOptions.value, files) + ) + ctx.emit('fileDrop', files[0]) + } + const onFileOver = (event: boolean) => { + isDropOVer.value = event + ctx.emit('fileOver', event) + } + return { + placeholderText, + filename, + autoUpload, + withoutBtn, + fileUploaders, + uploadText, + handleClick, + onDeleteFile, + fileUpload, + enableDrop, + onFileDrop, + onFileOver, + isDropOVer, + showTip, + } + }, + render() { + const { + placeholderText, + filename, + autoUpload, + withoutBtn, + fileUploaders, + uploadText, + handleClick, + onDeleteFile, + fileUpload, + enableDrop, + onFileDrop, + onFileOver, + isDropOVer, + disabled, + showTip, + } = this + + return ( + <> +
+
+
+ {!filename && ( +
+ {placeholderText} +
+ )} + {!!filename && ( +
+ + {filename} + + onDeleteFile(event)} + /> + {fileUploaders[0]?.status === UploadStatus.uploading && ( +
+ +
+ )} + {fileUploaders[0].status === UploadStatus.failed && ( + + )} + {fileUploaders[0].status === UploadStatus.uploaded && ( + + )} +
+ )} +
+ + + + + +
+ + {!autoUpload && !withoutBtn && ( + + {(!fileUploaders[0] || !fileUploaders[0]?.status) && ( + {uploadText} + )} + {fileUploaders[0]?.status === UploadStatus.uploading && ( + 上传中... + )} + {fileUploaders[0]?.status === UploadStatus.uploaded && ( + 已上传 + )} + {fileUploaders[0]?.status === UploadStatus.failed && ( + 上传失败 + )} + + )} +
+ {showTip && ( +
+ {fileUploaders[0]?.status === UploadStatus.uploading && ( + {i18nText.uploading} + )} + {fileUploaders[0]?.status === UploadStatus.uploaded && ( +
+ + + {i18nText.uploadSuccess} + +
+ )} + {fileUploaders[0]?.status === UploadStatus.failed && ( +
+ + + {i18nText.uploadFailed} + {i18nText.reUpload} + +
+ )} +
+ )} + + ) + }, +}) diff --git a/devui/upload/src/upload-types.ts b/devui/upload/src/upload-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..8820d2387f7a0df48461a8bb971f2a076a6e5932 --- /dev/null +++ b/devui/upload/src/upload-types.ts @@ -0,0 +1,258 @@ +import type { PropType, ExtractPropTypes } from 'vue' +import { Observable } from 'rxjs' +import { FileUploader } from './file-uploader' +export class IUploadOptions { + // 上传接口地址 + uri: string + // http 请求方法 + method?: string + // 上传文件大小限制 + maximumSize?: number + // 自定义请求headers + headers?: { + [key: string]: any + } + // 认证token + authToken?: string + // 认证token header标示 + authTokenHeader?: string + // 上传额外自定义参数 + additionalParameter?: { + [key: string]: any + } + // 上传文件字段名称,默认file + fileFieldName?: string + // 多文件上传,是否检查文件重名,设置为true,重名文件不会覆盖,否则会覆盖上传 + checkSameName?: boolean + // 指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求 + withCredentials?: boolean + // 手动设置返回数据类型 + responseType?: 'arraybuffer' | 'blob' | 'json' | 'text' +} + +export class IFileOptions { + accept?: string + multiple: boolean + webkitdirectory: boolean +} + +export enum UploadStatus { + preLoad = 0, + uploading, + uploaded, + failed, +} + +type DynamicUploadOptionsFn = (files, uploadOptions) => IUploadOptions +type ChangeFn = (_: any) => void +type BeforeUploadFn = ( + file: File +) => boolean | Promise | Observable +export const uploadProps = { + uploadOptions: { + type: Object as PropType, + required: true, + }, + fileOptions: { + type: Object as PropType, + required: true, + }, + filePath: { + type: String, + required: true, + }, + autoUpload: { + type: Boolean, + default: false, + }, + placeholderText: { + type: String, + default: '选择文件', + }, + // TODO + preloadFilesRef: { + type: Object, + }, + uploadText: { + type: String, + default: '上传', + }, + uploadedFiles: { + type: Array, + default: () => [], + }, + // TODO + uploadedFilesRef: { + type: Object, + }, + withoutBtn: { + type: Boolean, + default: false, + }, + enableDrop: { + type: Boolean, + default: false, + }, + beforeUpload: { + type: Function as PropType, + }, + dynamicUploadOptionsFn: { + type: Function as PropType, + }, + disabled: { + type: Boolean, + default: false, + }, + showTip: { + type: Boolean, + default: false, + }, + onChange: { + type: Function as PropType, + }, + fileDrop: { + type: Function as PropType<(v: any) => void>, + default: undefined, + }, + fileOver: { + type: Function as PropType<(v: boolean) => void>, + default: undefined, + }, + fileSelect: { + type: Function as PropType<(v: File) => void>, + default: undefined, + }, + errorEvent: { + type: Function as PropType<(v: { file: File; response: any; }) => void>, + default: undefined, + }, + successEvent: { + type: Function as PropType<(v: { file: File; response: any; }[]) => void>, + default: undefined, + }, + deleteUploadedFileEvent: { + type: Function as PropType<(v: string) => void>, + default: undefined, + }, +} as const +export const singleUploadViewProps = { + uploadOptions: { + type: Object as PropType, + }, + // TODO + preloadFilesRef: { + type: Object, + }, + uploadedFiles: { + type: Array, + }, + // TODO + uploadedFilesRef: { + type: Object, + }, + filePath: { + type: String, + required: true, + }, + dynamicUploadOptionsFn: { + type: Function as PropType, + }, +} +export type UploadProps = ExtractPropTypes +export type singleUploadViewProps = ExtractPropTypes< + typeof singleUploadViewProps +> + +export const multiUploadProps = { + uploadOptions: { + type: Object as PropType, + required: true, + }, + fileOptions: { + type: Object as PropType, + required: true, + }, + filePath: { + type: String, + required: true, + }, + autoUpload: { + type: Boolean, + default: false, + }, + withoutBtn: { + type: Boolean, + default: false, + }, + showTip: { + type: Boolean, + default: false, + }, + uploadedFiles: { + type: Array as PropType, + default: () => [], + }, + enableDrop: { + type: Boolean, + default: false, + }, + // TODO + uploadedFilesRef: { + type: Object, + }, + // TODO + preloadFilesRef: { + type: Object, + }, + placeholderText: { + type: String, + default: '选择文件', + }, + uploadText: { + type: String, + default: '上传', + }, + oneTimeUpload: { + type: Boolean, + default: false, + }, + disabled: { + type: Boolean, + default: false, + }, + beforeUpload: { + type: Function as PropType< + (files: any) => boolean | Promise | Observable + >, + }, + fileDrop: { + type: Function as PropType<(v: any) => void>, + default: undefined, + }, + fileOver: { + type: Function as PropType<(v: boolean) => void>, + default: undefined, + }, + fileSelect: { + type: Function as PropType<(v: File) => void>, + default: undefined, + }, + errorEvent: { + type: Function as PropType<(v: { file: File; response: any; }) => void>, + default: undefined, + }, + successEvent: { + type: Function as PropType<(v: { file: File; response: any; }[]) => void>, + default: undefined, + }, + deleteUploadedFileEvent: { + type: Function as PropType<(v: string) => void>, + default: undefined, + }, + setCustomUploadOptions: { + type: Function as PropType< + (files: File[], uploadOptions: IUploadOptions) => IUploadOptions + >, + default: undefined, + }, +} diff --git a/devui/upload/src/upload.scss b/devui/upload/src/upload.scss new file mode 100644 index 0000000000000000000000000000000000000000..edabc30117fab9d0a1f5d9544b9737fcdfadd479 --- /dev/null +++ b/devui/upload/src/upload.scss @@ -0,0 +1,227 @@ +@import '../../style/theme/color'; +@import '../../style/theme/variables'; +@import '../../style/mixins/index'; +@import '../../style/theme/corner'; +@import '../../style/core/_font'; +@import '../../style/core/animation'; + +.devui-input-group { + position: relative; + display: flex; + align-items: center; + border-collapse: separate; + width: 100%; +} + +.devui-input-group:not(.disabled):hover .devui-input-group-addon { + border-color: $devui-form-control-line-active; + background-color: $devui-dividing-line; + font-weight: bold; +} + +.devui-input-group:not(.disabled):hover .devui-form-control { + border-color: $devui-form-control-line-active; + border-right-color: $devui-form-control-line; +} + +.devui-input-group:not(.disabled) .devui-input-group-addon:active { + border-color: $devui-form-control-line-active; + border-right-color: $devui-form-control-line; + background-color: $devui-dividing-line; +} + +.devui-input-group .devui-input-group-addon { + width: 36px; + white-space: nowrap; + font-size: $devui-font-size-icon; + font-weight: normal; + line-height: 1; + color: $devui-text; + background-color: $devui-area; + border-top: 1px solid $devui-form-control-line; + border-bottom: 1px solid $devui-form-control-line; + border-right: 1px solid $devui-form-control-line; + border-radius: 0 $devui-border-radius $devui-border-radius 0; + transition: + border-color $devui-animation-duration-slow + $devui-animation-ease-in-out-smooth, + background-color $devui-animation-duration-slow + $devui-animation-ease-in-out-smooth; + cursor: pointer; + height: 100%; + position: relative; + display: inline-block; + + svg { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } +} + +.devui-input-group .devui-form-control { + cursor: pointer; + display: block; + width: 100%; + min-height: 28px; + height: unset; + padding: 4px 8px; + font-size: $devui-font-size; + line-height: 1.5; + background-image: none; + border: 1px solid $devui-form-control-line; + border-radius: $devui-border-radius 0 0 $devui-border-radius; + transition: + border-color $devui-animation-duration-slow + $devui-animation-ease-in-out-smooth, + box-shadow $devui-animation-duration-slow + $devui-animation-ease-in-out-smooth; + + &.devui-files-list { + max-height: 52px; + padding: 2px 2px 0 2px; + overflow-x: hidden; + overflow-y: auto; + max-width: 100%; + margin: 0; + + .devui-file-item { + height: 22px; + line-height: 22px; + padding: 0 48px 0 12px; + } + + .devui-file-tag { + position: relative; + background-color: $devui-label-bg; + border-radius: $devui-border-radius; + max-width: 100%; + + .devui-filename { + height: 100%; + display: inline-block; + vertical-align: middle; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .icon { + position: absolute; + cursor: pointer; + right: 8px; + top: 50%; + transform: translateY(-50%); + + &.icon-right { + color: $devui-success; + } + + &.icon-running { + font-size: 16px; + } + + &.devui-upload-delete-file-button { + margin-right: 20px; + } + + &.devui-uploading-delete { + display: none; + } + } + + &:hover { + .devui-upload-progress, + .icon-right { + display: none; + } + + .icon-close { + display: inline-block; + } + } + } + } +} + +.devui-input-group { + &.disabled { + .devui-upload-placeholder { + color: $devui-disabled-text; + } + } + + .devui-upload-placeholder { + max-height: 28px; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: $devui-placeholder; + } +} + +.devui-input-group .devui-upload-progress { + width: 16px; + height: 16px; +} + +.devui-input-group.disabled .devui-form-control, +.devui-input-group.disabled .devui-input-group-addon { + cursor: not-allowed; + background-color: $devui-disabled-bg; + border-color: $devui-disabled-line; + color: $devui-disabled-text; +} + +svg.svg-icon-dot > path { + fill: $devui-icon-text; +} + +.devui-form-control { + outline: none; +} + +.devui-input-group.disabled .devui-upload-delete-file-button { + cursor: not-allowed; + pointer-events: none; +} + +.devui-loading { + color: $devui-aide-text; +} + +.devui-failed-color { + color: $devui-danger; +} + +.devui-upload { + display: flex; +} + +.devui-upload-tip { + height: 18px; + margin-top: 8px; + font-size: 12px; + + .icon { + margin-right: 8px; + font-size: 16px; + vertical-align: middle; + } + + .icon-right-o { + color: $devui-success; + } + + .devui-upload-failed { + color: $devui-danger; + } + + a { + color: $devui-link; + cursor: pointer; + } +} diff --git a/devui/upload/src/use-select-files.ts b/devui/upload/src/use-select-files.ts new file mode 100644 index 0000000000000000000000000000000000000000..cce6f208165cd676353f4dc2db19c24a3ec5607e --- /dev/null +++ b/devui/upload/src/use-select-files.ts @@ -0,0 +1,157 @@ +import { ref } from 'vue' +import { from, Observable } from 'rxjs' +import { mergeMap } from 'rxjs/operators' +import { IFileOptions, IUploadOptions } from './upload-types' +import { + getNotAllowedFileTypeMsg, + getBeyondMaximalFileSizeMsg, + getAllFilesBeyondMaximalFileSizeMsg, +} from './i18n-upload' + +export const useSelectFiles = () => { + const BEYOND_MAXIMAL_FILE_SIZE_MSG = ref('') + const simulateClickEvent = (input) => { + const evt = document.createEvent('MouseEvents') + evt.initMouseEvent( + 'click', + true, + true, + window, + 1, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ) + input.dispatchEvent(evt) + } + const selectFiles = ({ + multiple, + accept, + webkitdirectory, + }: IFileOptions): Promise => { + return new Promise((resolve) => { + const tempNode = document.getElementById('d-upload-temp') + if (tempNode) { + document.body.removeChild(tempNode) + } + const input = document.createElement('input') + + input.style.position = 'fixed' + input.style.left = '-2000px' + input.style.top = '-2000px' + + input.setAttribute('id', 'd-upload-temp') + input.setAttribute('type', 'file') + if (multiple) { + input.setAttribute('multiple', '') + } + if (accept) { + input.setAttribute('accept', accept) + } + + if (webkitdirectory) { + input.setAttribute('webkitdirectory', '') + } + + input.addEventListener('change', (event) => { + resolve( + Array.prototype.slice.call((event.target as HTMLInputElement).files) + ) + }) + document.body.appendChild(input) // Fix compatibility issue with Internet Explorer 11 + simulateClickEvent(input) + }) + } + + const isAllowedFileType = (accept: string, file: File) => { + if (accept) { + const acceptArr = accept.split(',') + const baseMimeType = file.type.replace(/\/.*$/, '') + return acceptArr.some((type: string) => { + const validType = type.trim() + // suffix name (e.g. '.png,.xlsx') + if (validType.startsWith('.')) { + return ( + file.name + .toLowerCase() + .indexOf( + validType.toLowerCase(), + file.name.toLowerCase().length - validType.toLowerCase().length + ) > -1 + ) + // mime type like 'image/*' + } else if (/\/\*$/.test(validType)) { + return baseMimeType === validType.replace(/\/.*$/, '') + } + // mime type like 'text/plain,application/json' + return file.type === validType + }) + } + return true + } + + const beyondMaximalSize = (fileSize, maximumSize) => { + if (maximumSize) { + return fileSize > 1024 * 1024 * maximumSize + } + return false + } + + const _validateFiles = (file, accept, uploadOptions) => { + if (!isAllowedFileType(accept, file)) { + return { + checkError: true, + errorMsg: getNotAllowedFileTypeMsg((file).name, accept), + } + } + if ( + uploadOptions && + beyondMaximalSize((file).size, uploadOptions.maximumSize) + ) { + return { + checkError: true, + errorMsg: getBeyondMaximalFileSizeMsg( + (file).name, + uploadOptions.maximumSize + ), + } + } + return { checkError: false, errorMsg: undefined } + } + + const triggerSelectFiles = (fileOptions: IFileOptions) => { + const { multiple, accept, webkitdirectory } = fileOptions + return from(selectFiles({ multiple, accept, webkitdirectory })).pipe( + mergeMap((file) => file) + ) + } + const triggerDropFiles = ( + fileOptions: IFileOptions, + uploadOptions: IUploadOptions, + files: any + ) => { + return new Observable((observer) => observer.next(files)).pipe( + mergeMap((file) => file) + ) + } + const checkAllFilesSize = (fileSize, maximumSize) => { + if (beyondMaximalSize(fileSize, maximumSize)) { + BEYOND_MAXIMAL_FILE_SIZE_MSG.value = + getAllFilesBeyondMaximalFileSizeMsg(maximumSize) + return { checkError: true, errorMsg: BEYOND_MAXIMAL_FILE_SIZE_MSG.value } + } + } + return { + triggerSelectFiles, + _validateFiles, + triggerDropFiles, + checkAllFilesSize, + } +} diff --git a/devui/upload/src/use-upload.ts b/devui/upload/src/use-upload.ts new file mode 100644 index 0000000000000000000000000000000000000000..48427ba15c20b4f74e11e30a767d093b8816bb49 --- /dev/null +++ b/devui/upload/src/use-upload.ts @@ -0,0 +1,137 @@ +import { ref } from 'vue' +import { from, merge } from 'rxjs' +import { toArray } from 'rxjs/operators' +import { FileUploader } from './file-uploader' +import { UploadStatus } from './upload-types' + +export const useUpload = () => { + const fileUploaders = ref>([]) + const filesWithSameName = ref([]) + + const checkFileSame = (fileName) => { + let checkRel = true + + for (let i = 0; i < fileUploaders.value.length; i++) { + if (fileName === fileUploaders.value[i].file.name) { + checkRel = false + if (filesWithSameName.value.indexOf(fileName) === -1) { + filesWithSameName.value.push(fileName) + } + break + } + } + return checkRel + } + + const addFile = (file, options) => { + if (options && options.checkSameName) { + if (checkFileSame(file.name)) { + fileUploaders.value.push(new FileUploader(file, options)) + } + } else { + fileUploaders.value.push(new FileUploader(file, options)) + } + } + + const getFiles = () => { + return fileUploaders.value.map((fileUploader) => { + return fileUploader.file + }) + } + + const getFullFiles = () => { + return fileUploaders.value.map((fileUploader) => { + return fileUploader + }) + } + + const dealOneTimeUploadFiles = async (uploads) => { + if (!uploads || !uploads.length) { + return Promise.reject('no files') + } + // 触发文件上传 + let finalUploads = [] + await uploads[0].send(uploads).finally( + () => + // 根据uploads[0]的上传状态为其他file设置状态 + (finalUploads = uploads.map((file) => { + file.status = uploads[0].status + file.percentage = uploads[0].percentage + return { file: file.file, response: uploads[0].response } + })) + ) + + return finalUploads + } + + const upload = (oneFile?) => { + let uploads: any[] = [] + if (oneFile) { + oneFile.percentage = 0 + uploads.push(from(oneFile.send())) + } else { + const preFiles = fileUploaders.value.filter( + (fileUploader) => fileUploader.status === UploadStatus.preLoad + ) + const failedFiles = fileUploaders.value.filter( + (fileUploader) => fileUploader.status === UploadStatus.failed + ) + const uploadFiles = preFiles.length > 0 ? preFiles : failedFiles + uploads = uploadFiles.map((fileUploader) => { + fileUploader.percentage = 0 + return from(fileUploader.send()) + }) + } + if (uploads.length > 0) { + return merge< + { + file: File + response: any + }[] + >(...uploads).pipe(toArray()) + } + + return from(Promise.reject('no files')) + } + + const _oneTimeUpload = () => { + const uploads = fileUploaders.value.filter( + (fileUploader) => fileUploader.status !== UploadStatus.uploaded + ) + return from(dealOneTimeUploadFiles(uploads)) + } + + const deleteFile = (file) => { + const deleteUploadFile = fileUploaders.value.find( + (fileUploader) => fileUploader.file === file + ) + deleteUploadFile.cancel() + fileUploaders.value = fileUploaders.value.filter((fileUploader) => { + return file !== fileUploader.file + }) + } + + const removeFiles = () => { + fileUploaders.value = [] + filesWithSameName.value = [] + } + const getSameNameFiles = () => { + return filesWithSameName.value.join() + } + const resetSameNameFiles = () => { + filesWithSameName.value = [] + } + + return { + fileUploaders, + getFiles, + addFile, + getFullFiles, + deleteFile, + upload, + removeFiles, + getSameNameFiles, + resetSameNameFiles, + _oneTimeUpload, + } +} diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md new file mode 100644 index 0000000000000000000000000000000000000000..30ac446efd382b7d8f8b379b726f0028e4409601 --- /dev/null +++ b/sites/components/upload/index.md @@ -0,0 +1,965 @@ +# Upload 上传 + +文件上传组件。 + +### 何时使用 + +当需要将文件上传到后端服务器时。 + +### 基本用法 + +单文件上传、拖动文件上传、禁止上传。 + +

Basic Usage

+ +:::demo + +```vue + + +``` + +::: + +

Dragdrop

+ +:::demo + +```vue + + +``` + +::: + +

Disabled

+ +:::demo + +```vue + + +``` + +::: + +### 多文件上传 + +多文件上传,支持拖动文件上传、禁止上传。 + +

Basic Usage

+ +:::demo + +```vue + + +``` + +::: + +

Upload Directory

+ +:::demo + +```vue + + +``` + +::: + +

Dragdrop

+ +:::demo + +```vue + + +``` + +::: + +

Disabled

+ +:::demo + +```vue + + +``` + +::: + +### 自动上传 + +通过 autoUpload 设置自动上传。 + +:::demo + +```vue + + +``` + +::: + +### 自定义 + +自定义上传按钮,通过 slot preloadFiles 设置已选择文件列表模板,通过 slot uploadedFiles 设置已上传文件列表模版。 + +:::demo + +```vue + + + +``` + +::: + +### 动态上传参数 + +用户可通过 beforeUpload 动态修改上传参数。 + +:::demo + +```vue + + +``` + +::: + +### API + +d-single-upload 参数 + +| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | +| ---------------------- | ---------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | --------------------- | +| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [基本用法](#基本用法) | +| filePath | `string` | -- | 必选,文件路径 | [基本用法](#基本用法) | +| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [基本用法](#基本用法) | +| autoUpload | `boolean` | false | 可选,是否自动上传 | [基本用法](#基本用法) | +| placeholderText | `string` | '选择文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | +| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | +| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [基本用法](#基本用法) | +| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [基本用法](#基本用法) | +| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [基本用法](#基本用法) | +| beforeUpload | `boolean Promise Observable` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [基本用法](#基本用法) | +| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [基本用法](#基本用法) | +| disabled | `boolean` | false | 可选,是否禁用上传组件 | [基本用法](#基本用法) | +| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [自动上传](#自动上传) | + +d-single-upload 事件 + +| 参数 | 类型 | 说明 | 跳转 Demo | +| ----------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------- | +| fileOver | `EventEmitter` | 支持拖拽上传时,文件移动到可拖放区域触发事件,可拖动的元素移出放置目标时返回`false`,元素正在拖动到放置目标时返回`true` | [基本用法](#基本用法) | +| fileDrop | `EventEmitter` | 支持拖拽上传时,当前拖拽的文件列表回调,单文件上传默认返回第一个文件 | [基本用法](#基本用法) | +| successEvent | `EventEmitter>` | 上传成功时的回调函数,返回文件及 xhr 的响应信息 | [基本用法](#基本用法) | +| errorEvent | `EventEmitter<{file: File; response: any}>` | 上传错误时的回调函数,返回上传失败的错误信息 | [基本用法](#基本用法) | +| deleteUploadedFileEvent | `EventEmitter` | 删除上传文件的回调函数,返回删除文件的路径信息 | [基本用法](#基本用法) | +| fileSelect | `EventEmitter` | 文件选择后的回调函数,返回已选择文件信息 | [基本用法](#基本用法) | + +d-multiple-upload 参数 + +| **参数** | **类型** | **默认** | 说明 | **跳转 Demo** | +| ---------------------- | ---------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------- | ------------------------- | +| fileOptions | [IFileOptions](#ifileoptions) | -- | 必选,待上传文件配置 | [多文件上传](#多文件上传) | +| filePath | `string` | -- | 必选,文件路径 | [多文件上传](#多文件上传) | +| uploadOptions | [IUploadOptions](#iuploadoptions) | \-- | 必选,上传配置 | [多文件上传](#多文件上传) | +| autoUpload | `boolean` | false | 可选,是否自动上传 | [自动上传](#自动上传) | +| placeholderText | `string` | '选择多个文件' | 可选,上传输入框中的 Placeholder 文字 | [基本用法](#基本用法) | +| uploadText | `string` | '上传' | 可选,上传按钮文字 | [基本用法](#基本用法) | +| uploadedFiles | `Array` | [] | 可选,获取已上传的文件列表 | [多文件上传](#多文件上传) | +| withoutBtn | `boolean` | false | 可选,是否舍弃按钮 | [自定义](#自定义) | +| enableDrop | `boolean` | false | 可选,是否支持拖拽 | [多文件上传](#多文件上传) | +| beforeUpload | `boolean Promise Observable` | \-- | 可选,上传前的回调,通过返回`true` or `false` ,控制文件是否上传,参数为文件信息及上传配置 | [多文件上传](#多文件上传) | +| dynamicUploadOptionsFn | [IUploadOptions](#iuploadoptions) | \-- | 为文件动态设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [多文件上传](#多文件上传) | +| disabled | `boolean` | false | 可选,是否禁用上传组件 | [多文件上传](#多文件上传) | +| showTip | `boolean` | false | 可选,是否显示上传提示信息 | [多文件上传](#多文件上传) | +| setCustomUploadOptions | [IUploadOptions](#iuploadoptions) | -- | 为每个文件设置自定义的上传参数, 参数为当前选中文件及`uploadOptions`的值 | [自定义](#自定义) | + +d-multiple-upload 事件 + +| 参数 | 类型 | 说明 | 跳转 Demo | +| ----------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| fileOver | `EventEmitter` | 支持拖拽上传时,文件移动到可拖放区域触发事件,可拖动的元素移出放置目标时返回`false`,元素正在拖动到放置目标时返回`true` | [多文件上传](#多文件上传) | +| fileDrop | `EventEmitter` | 支持拖拽上传时,当前拖拽的文件列表回调,单文件上传默认返回第一个文件 | [多文件上传](#多文件上传) | +| successEvent | `EventEmitter>` | 上传成功时的回调函数,返回文件及 xhr 的响应信息 | [多文件上传](#多文件上传) | +| errorEvent | `EventEmitter<{file: File; response: any}>` | 上传错误时的回调函数,返回上传失败的错误信息 | [多文件上传](#多文件上传) | +| deleteUploadedFileEvent | `EventEmitter` | 删除上传文件的回调函数,返回删除文件的路径信息 | [多文件上传](#多文件上传) | +| fileSelect | `EventEmitter` | 文件选择后的回调函数,返回已选择文件信息 | [多文件上传](#多文件上传) | + +### slot + +| name | 默认 | 说明 | 跳转 Demo | +| ------------- | ---- | ----------------------------------------------------------------------------- | ----------------- | +| preloadFiles | -- | 可选,用于创建自定义 已选择文件列表模板,参数为 `{fileUploaders, deleteFile}` | [自定义](#自定义) | +| uploadedFiles | -- | 可选,用于创建自定义 已上传文件列表模板,参数为 `{uploadedFiles, deleteFile}` | [自定义](#自定义) | + +### 接口 & 类型定义 + +### IUploadOptions + +```typescript +export class IUploadOptions { + // 上传接口地址 + uri: string + // http 请求方法 + method?: string + // 上传文件大小限制 + maximumSize?: number + // 自定义请求headers + headers?: { [key: string]: any } + // 认证token + authToken?: string + // 认证token header标示 + authTokenHeader?: string + // 上传额外自定义参数 + additionalParameter?: { [key: string]: any } + // 上传文件字段名称,默认file + fileFieldName?: string + // 多文件上传,是否检查文件重名,设置为true,重名文件不会覆盖,否则会覆盖上传 + checkSameName?: boolean + // 指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求 + withCredentials?: boolean + // 手动设置返回数据类型 + responseType?: 'arraybuffer' | 'blob' | 'json' | 'text' +} +``` + +### IFileOptions + +```typescript +export class IFileOptions { + // 规定能够通过文件上传进行提交的文件类型,例如 accept: '.xls,.xlsx,.pages,.mp3,.png' + accept?: string + // 输入字段可选择多个值 + multiple?: boolean + // 是否允许用户选择文件目录,而不是文件 + webkitdirectory?: boolean +} +```