diff --git a/packages/devui-vue/devui/upload/index.ts b/packages/devui-vue/devui/upload/index.ts index 227a3ba02f..c284f0c870 100644 --- a/packages/devui-vue/devui/upload/index.ts +++ b/packages/devui-vue/devui/upload/index.ts @@ -1,19 +1,16 @@ -import type { App } from 'vue' -import Upload from './src/upload' -import fileDropDirective from './src/file-drop-directive' +import type { App } from 'vue'; +import Upload from './src/upload'; +import fileDropDirective from './src/file-drop-directive'; +export * from './src/upload-types'; -Upload.install = function (app: App) { - app.directive('file-drop', fileDropDirective) - app.component(Upload.name, Upload) -} - -export { Upload } +export { Upload }; export default { title: 'Upload 上传', category: '数据录入', status: '100%', install(app: App): void { - app.use(Upload as any) - } -} + app.directive('file-drop', fileDropDirective); + app.component(Upload.name, Upload); + }, +}; diff --git a/packages/devui-vue/devui/upload/src/composables/use-select-files.ts b/packages/devui-vue/devui/upload/src/composables/use-select-files.ts new file mode 100644 index 0000000000..0f518c91a6 --- /dev/null +++ b/packages/devui-vue/devui/upload/src/composables/use-select-files.ts @@ -0,0 +1,109 @@ +import { ref } from 'vue'; +import { IFileOptions } 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 selectFiles({ multiple, accept, webkitdirectory }); + }; + const triggerDropFiles = (files: File[]) => { + return Promise.resolve(files); + }; + 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/packages/devui-vue/devui/upload/src/composables/use-upload.ts b/packages/devui-vue/devui/upload/src/composables/use-upload.ts new file mode 100644 index 0000000000..1275d9621c --- /dev/null +++ b/packages/devui-vue/devui/upload/src/composables/use-upload.ts @@ -0,0 +1,134 @@ +import { ref } from 'vue'; +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 = async ( + oneFile? + ): Promise< + | never + | { + file: File; + response: any; + }[] + > => { + let uploads: any[] = []; + if (oneFile) { + oneFile.percentage = 0; + const uploadedFile = await oneFile.send(); + uploads.push(uploadedFile); + } 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 = await Promise.all( + uploadFiles.map(async (fileUploader) => { + fileUploader.percentage = 0; + const uploadedFile = await fileUploader.send(); + return uploadedFile; + }) + ); + } + if (uploads.length > 0) { + return Promise.resolve(uploads); + } + + return Promise.reject('no files'); + }; + + const _oneTimeUpload = () => { + const uploads = fileUploaders.value.filter((fileUploader) => fileUploader.status !== UploadStatus.uploaded); + return 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/packages/devui-vue/devui/upload/src/file-drop-directive.ts b/packages/devui-vue/devui/upload/src/file-drop-directive.ts index 86831e711d..14c1ba480b 100644 --- a/packages/devui-vue/devui/upload/src/file-drop-directive.ts +++ b/packages/devui-vue/devui/upload/src/file-drop-directive.ts @@ -1,84 +1,84 @@ interface BindingType { value: { - enableDrop?: boolean - isSingle?: boolean - onFileDrop?: (files: File[]) => void - onFileOver?: (event: any) => void - } + droppable?: boolean; + isSingle?: boolean; + onFileDrop?: (files: File[]) => void; + onFileOver?: (event: any) => void; + }; } const getTransfer = (event: any) => { - return event.dataTransfer ? event.dataTransfer : event.originalEvent?.dataTransfer -} + return event.dataTransfer ? event.dataTransfer : event.originalEvent?.dataTransfer; +}; const haveFiles = (types: any) => { if (!types) { - return false + return false; } if (types.indexOf) { - return types.indexOf('Files') !== -1 + return types.indexOf('Files') !== -1; } else if (types.contains) { - return types.contains('Files') + return types.contains('Files'); } else { - return false + return false; } -} +}; const preventAndStop = (event: any) => { - event.preventDefault() - event.stopPropagation() -} + event.preventDefault(); + event.stopPropagation(); +}; const onDragOver = (el: HTMLElement, binding: BindingType) => { - const { onFileOver } = binding.value + const { onFileOver } = binding.value; el.addEventListener('dragover', (event) => { - const transfer = getTransfer(event) + const transfer = getTransfer(event); if (!haveFiles(transfer.types)) { - return + return; } - preventAndStop(event) - onFileOver && onFileOver(true) - }) -} + preventAndStop(event); + onFileOver && onFileOver(true); + }); +}; const onDragLeave = (el: HTMLElement, binding: BindingType) => { - const { onFileOver } = binding.value + const { onFileOver } = binding.value; el.addEventListener('dragleave', (event) => { if (event.currentTarget === el) { - return + return; } - preventAndStop(event) - onFileOver && onFileOver(true) - }) -} + preventAndStop(event); + onFileOver && onFileOver(true); + }); +}; const onDrop = (el: HTMLElement, binding: BindingType) => { - const { onFileDrop, isSingle } = binding.value + const { onFileDrop, isSingle } = binding.value; el.addEventListener('drop', (event) => { - const transfer = getTransfer(event) + const transfer = getTransfer(event); if (!transfer) { - return + return; } - preventAndStop(event) + preventAndStop(event); if (isSingle) { - onFileDrop && onFileDrop([transfer.files[0]]) + onFileDrop && onFileDrop([transfer.files[0]]); } else { - onFileDrop && onFileDrop(Array.from(transfer.files)) + onFileDrop && onFileDrop(Array.from(transfer.files)); } - }) -} + }); +}; const fileDropDirective = { mounted: (el: HTMLElement, binding: BindingType): void => { - const { enableDrop } = binding.value - if (!enableDrop) { - return + const { droppable } = binding.value; + if (!droppable) { + return; } - onDragOver(el, binding) - onDragLeave(el, binding) - onDrop(el, binding) - } -} + onDragOver(el, binding); + onDragLeave(el, binding); + onDrop(el, binding); + }, +}; -export default fileDropDirective +export default fileDropDirective; diff --git a/packages/devui-vue/devui/upload/src/file-uploader.ts b/packages/devui-vue/devui/upload/src/file-uploader.ts index 1acef9d914..89072682b6 100755 --- a/packages/devui-vue/devui/upload/src/file-uploader.ts +++ b/packages/devui-vue/devui/upload/src/file-uploader.ts @@ -1,133 +1,109 @@ -import { IUploadOptions, UploadStatus } from './upload-types' +import { IUploadOptions, UploadStatus } from './upload-types'; export class FileUploader { - private xhr: XMLHttpRequest - public status: UploadStatus - public response: any - public percentage = 0 + 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 + this.file = file; + this.uploadOptions = uploadOptions; + this.status = UploadStatus.preLoad; } - send(uploadFiles?: FileUploader[]): Promise<{ file: File; response: any; }> { + 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) + 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 + this.xhr.withCredentials = withCredentials; } if (responseType) { - this.xhr.responseType = responseType + this.xhr.responseType = responseType; } if (authToken) { - this.xhr.setRequestHeader(authTokenHeader_, authToken) + this.xhr.setRequestHeader(authTokenHeader_, authToken); } if (headers) { Object.keys(headers).forEach((key) => { - this.xhr.setRequestHeader(key, headers[key]) - }) + this.xhr.setRequestHeader(key, headers[key]); + }); } this.xhr.upload.onprogress = (e) => { - this.percentage = Math.round((e.loaded * 100) / e.total) - } + this.percentage = Math.round((e.loaded * 100) / e.total); + }; const formData = uploadFiles && uploadFiles.length - ? this.oneTimeUploadFiles( - fileFieldName_, - additionalParameter, - uploadFiles - ) - : this.parallelUploadFiles(fileFieldName_, additionalParameter) + ? this.oneTimeUploadFiles(fileFieldName_, additionalParameter, uploadFiles) + : this.parallelUploadFiles(fileFieldName_, additionalParameter); - this.xhr.send(formData) - this.status = UploadStatus.uploading + this.xhr.send(formData); + this.status = UploadStatus.uploading; this.xhr.onabort = () => { - this.status = UploadStatus.preLoad - this.xhr = null - } + 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.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 }) + 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 }) + 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) + 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]) - }) + formData.append(key, additionalParameter[key]); + }); } - return formData + return formData; } - oneTimeUploadFiles( - fileFieldName_: string, - additionalParameter: Record, - uploadFiles: FileUploader[] - ): FormData { - const formData = new FormData() + oneTimeUploadFiles(fileFieldName_: string, additionalParameter: Record, uploadFiles: FileUploader[]): FormData { + const formData = new FormData(); uploadFiles.forEach((element) => { - formData.append(fileFieldName_, element.file, element.file.name) + formData.append(fileFieldName_, element.file, element.file.name); if (additionalParameter) { Object.keys(additionalParameter).forEach((key: string) => { - formData.append(key, additionalParameter[key]) - }) + formData.append(key, additionalParameter[key]); + }); } - }) - return formData + }); + return formData; } cancel(): void { if (this.xhr) { - this.xhr.abort() + this.xhr.abort(); } } } diff --git a/packages/devui-vue/devui/upload/src/i18n-upload.ts b/packages/devui-vue/devui/upload/src/i18n-upload.ts index b2cbbc3820..9e458a1a56 100644 --- a/packages/devui-vue/devui/upload/src/i18n-upload.ts +++ b/packages/devui-vue/devui/upload/src/i18n-upload.ts @@ -11,32 +11,19 @@ export const i18nText = { 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 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}" 存在重名文件, 请重新选择文件` -} + return `您上传的 "${sameNames}" 存在重名文件, 请重新选择文件`; +}; diff --git a/packages/devui-vue/devui/upload/src/single-upload.tsx b/packages/devui-vue/devui/upload/src/single-upload.tsx deleted file mode 100644 index 9dc88c0a9a..0000000000 --- a/packages/devui-vue/devui/upload/src/single-upload.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { defineComponent, toRefs, computed, ref } from 'vue' -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' -import './upload.scss' - -export default defineComponent({ - name: 'DSingleUpload', - props: uploadProps, - emits: [ - 'fileDrop', - 'fileOver', - 'fileSelect', - 'successEvent', - 'errorEvent', - 'deleteUploadedFileEvent', - '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, removeFiles } = - 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 { - uploadResult = Promise.resolve(result) - } - } - } - return uploadResult - } - - const fileUpload = () => { - canUpload().then((canUpload) => { - if (!canUpload) { - return - } - upload() - .then((results: { 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) - }) - .catch((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 = (promise: Promise) => { - promise - .then((files) => { - files.forEach((file) => { - // 单文件上传前先清空数组 - removeFiles() - addFile(file, uploadOptions.value) - }) - checkValid() - const file = fileUploaders.value[0]?.file - if (props.onChange) { - props.onChange(file) - } - if (file) { - ctx.emit('fileSelect', file) - } - if (autoUpload.value) { - fileUpload() - } - }) - .catch((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 deleteUploadedFile = (file: File) => { - const newUploadedFiles = uploadedFiles.value.filter((uploadedFile) => { - return uploadedFile.name !== file.name - }) - ctx.emit('deleteUploadedFileEvent', file) - ctx.emit('update:uploadedFiles', newUploadedFiles) - } - const onFileDrop = (files: File[]) => { - isDropOVer.value = false - _dealFiles(triggerDropFiles(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, - uploadedFiles, - deleteUploadedFile - } - }, - render() { - const { - placeholderText, - filename, - autoUpload, - withoutBtn, - fileUploaders, - uploadText, - handleClick, - onDeleteFile, - fileUpload, - enableDrop, - onFileDrop, - onFileOver, - isDropOVer, - disabled, - showTip, - uploadedFiles, - deleteUploadedFile - } = this - return ( -
-
- {this.$slots.default?.() ? ( -
{this.$slots.default()}
- ) : ( -
-
- {!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} - -
- )} -
- )} -
- {this.$slots.preloadFiles?.({ - fileUploaders, - deleteFile: onDeleteFile - })} -
-
- {this.$slots.uploadedFiles?.({ - uploadedFiles, - deleteFile: deleteUploadedFile - })} -
-
- ) - } -}) diff --git a/packages/devui-vue/devui/upload/src/upload-types.ts b/packages/devui-vue/devui/upload/src/upload-types.ts index d9e564926d..5a4226aaff 100644 --- a/packages/devui-vue/devui/upload/src/upload-types.ts +++ b/packages/devui-vue/devui/upload/src/upload-types.ts @@ -1,133 +1,127 @@ -import type { PropType, ExtractPropTypes } from 'vue' -import { FileUploader } from './file-uploader' +import type { PropType, ExtractPropTypes } from 'vue'; +import { FileUploader } from './file-uploader'; + export class IUploadOptions { // 上传接口地址 - uri: string + uri: string; // http 请求方法 - method?: string + method?: string; // 上传文件大小限制 - maximumSize?: number + maximumSize?: number; // 自定义请求headers headers?: { - [key: string]: any - } + [key: string]: any; + }; // 认证token - authToken?: string + authToken?: string; // 认证token header标示 - authTokenHeader?: string + authTokenHeader?: string; // 上传额外自定义参数 additionalParameter?: { - [key: string]: any - } + [key: string]: any; + }; // 上传文件字段名称,默认file - fileFieldName?: string + fileFieldName?: string; // 多文件上传,是否检查文件重名,设置为true,重名文件不会覆盖,否则会覆盖上传 - checkSameName?: boolean + checkSameName?: boolean; // 指示了是否该使用类似cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求 - withCredentials?: boolean + withCredentials?: boolean; // 手动设置返回数据类型 - responseType?: 'arraybuffer' | 'blob' | 'json' | 'text' + responseType?: 'arraybuffer' | 'blob' | 'json' | 'text'; } export class IFileOptions { - accept?: string - multiple?: boolean - webkitdirectory: boolean + accept?: string; + multiple?: boolean; + webkitdirectory: boolean; } export enum UploadStatus { preLoad = 0, uploading, uploaded, - failed + failed, } -type DynamicUploadOptionsFn = (files, uploadOptions) => IUploadOptions -type ChangeFn = (_: any) => void -type BeforeUploadFn = (file: FileUploader) => boolean | Promise +type DynamicUploadOptionsFn = (files, uploadOptions) => IUploadOptions; +type ChangeFn = (_: any) => void; +type BeforeUploadFn = (file: FileUploader) => boolean | Promise; + export const uploadProps = { // 规定能够通过文件上传进行提交的文件类型,例如 accept: '.xls,.xlsx,.pages,.mp3,.png' accept: { - type: String + type: String, }, // 是否允许用户选择文件目录,而不是文件 webkitdirectory: { type: Boolean, - default: false + default: false, }, uploadOptions: { type: Object as PropType, - required: true }, multiple: { type: Boolean, - default: false + default: false, }, autoUpload: { type: Boolean, - default: false - }, - placeholderText: { - type: String, - default: '选择文件' + default: true, }, - uploadText: { + placeholder: { type: String, - default: '上传' + default: '选择文件', }, - uploadedFiles: { + modelValue: { type: Array as PropType, - default: () => [] + default: () => [], }, - withoutBtn: { + droppable: { type: Boolean, - default: false - }, - enableDrop: { - type: Boolean, - default: false + default: false, }, beforeUpload: { - type: Function as PropType + type: Function as PropType, }, /** @deprecated */ dynamicUploadOptionsFn: { - type: Function as PropType + type: Function as PropType, }, disabled: { type: Boolean, - default: false + default: false, }, onChange: { - type: Function as PropType + type: Function as PropType, }, fileDrop: { type: Function as PropType<(v: any) => void>, - default: undefined + default: undefined, }, fileOver: { type: Function as PropType<(v: boolean) => void>, - default: undefined + default: undefined, }, fileSelect: { type: Function as PropType<(v: File) => void>, - default: undefined + default: undefined, }, deleteUploadedFile: { type: Function as PropType<(v: string) => void>, - default: undefined + default: undefined, }, 'on-error': { - type: Function as PropType<(v: { file: File; response: any; }) => void>, - default: undefined + type: Function as PropType<(v: { file: File; response: any }) => void>, + default: undefined, }, 'on-success': { - type: Function as PropType<(v: { file: File; response: any; }[]) => void>, - default: undefined + type: Function as PropType<(v: { file: File; response: any }[]) => void>, + default: undefined, }, oneTimeUpload: { type: Boolean, - default: false - } -} as const -export type UploadProps = ExtractPropTypes + default: false, + }, +} as const; + +export type UploadProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/upload/src/upload.scss b/packages/devui-vue/devui/upload/src/upload.scss index 287179cd86..54f1657528 100644 --- a/packages/devui-vue/devui/upload/src/upload.scss +++ b/packages/devui-vue/devui/upload/src/upload.scss @@ -7,10 +7,10 @@ .devui-input-group { position: relative; - display: flex; + display: flex !important; align-items: center; border-collapse: separate; - width: 100%; + width: 360px; } .devui-input-group:not(.disabled):hover .devui-input-group-addon { @@ -42,11 +42,8 @@ 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; + 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; @@ -68,11 +65,8 @@ 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; + 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; diff --git a/packages/devui-vue/devui/upload/src/upload.tsx b/packages/devui-vue/devui/upload/src/upload.tsx index e0315671be..2e50fcc528 100644 --- a/packages/devui-vue/devui/upload/src/upload.tsx +++ b/packages/devui-vue/devui/upload/src/upload.tsx @@ -1,8 +1,8 @@ import { defineComponent, toRefs, ref } from 'vue'; import { NotificationService } from '../../notification'; import { UploadStatus, UploadProps, uploadProps } from './upload-types'; -import { useSelectFiles } from './use-select-files'; -import { useUpload } from './use-upload'; +import { useSelectFiles } from './composables/use-select-files'; +import { useUpload } from './composables/use-upload'; import { getFailedFilesCount, getSelectedFilesCount, getUploadingFilesCount, getExistSameNameFilesMsg } from './i18n-upload'; import { FileUploader } from './file-uploader'; import './upload.scss'; @@ -10,19 +10,17 @@ import './upload.scss'; export default defineComponent({ name: 'DUpload', props: uploadProps, - emits: ['fileDrop', 'fileOver', 'fileSelect', 'deleteUploadedFile', 'update:uploadedFiles'], + emits: ['fileDrop', 'fileOver', 'fileSelect', 'deleteUploadedFile', 'update:modelValue'], setup(props: UploadProps, ctx) { const { uploadOptions, - placeholderText, + placeholder, autoUpload, - withoutBtn, - uploadText, disabled, beforeUpload, - enableDrop, + droppable, oneTimeUpload, - uploadedFiles, + modelValue, multiple, accept, webkitdirectory, @@ -36,7 +34,6 @@ export default defineComponent({ NotificationService.open({ type: 'warning', content: errorMsg, - //value: [{ severity: 'warn', content: errorMsg }] }); }; const checkValid = () => { @@ -72,11 +69,10 @@ export default defineComponent({ removeFiles(); } addFile(file, uploadOptions.value); - // debounceTime(100) }); checkValid(); const sameNameFiles = getSameNameFiles(); - if (uploadOptions.value.checkSameName && sameNameFiles.length) { + if (uploadOptions.value && uploadOptions.value.checkSameName && sameNameFiles.length) { alertMsg(getExistSameNameFilesMsg(sameNameFiles)); } const selectedFiles = fileUploaders.value @@ -116,11 +112,11 @@ export default defineComponent({ }; // 删除已上传文件 const deleteUploadedFile = (file: File) => { - const newUploadedFiles = uploadedFiles.value.filter((uploadedFile) => { + const newUploadedFiles = modelValue.value.filter((uploadedFile) => { return uploadedFile.name !== file.name; }); ctx.emit('deleteUploadedFile', file); - ctx.emit('update:uploadedFiles', newUploadedFiles); + ctx.emit('update:modelValue', newUploadedFiles); }; const onDeleteFile = (event: Event, file: File, status: UploadStatus) => { event.stopPropagation(); @@ -143,7 +139,7 @@ export default defineComponent({ } return uploadResult; }; - const fileUpload = (event: Event, fileUploader?: FileUploader) => { + const fileUpload = (event?: Event, fileUploader?: FileUploader) => { if (event) { event.stopPropagation(); } @@ -158,8 +154,8 @@ export default defineComponent({ .then((results: Array<{ file: File; response: any }>) => { props['on-success'] && props['on-success'](results); const newFiles = results.map((result) => result.file); - const newUploadedFiles = [...newFiles, ...uploadedFiles.value]; - ctx.emit('update:uploadedFiles', newUploadedFiles); + const newUploadedFiles = [...newFiles, ...modelValue.value]; + ctx.emit('update:modelValue', newUploadedFiles); }) .catch((error) => { props['on-error'] && props['on-error'](error); @@ -210,69 +206,26 @@ export default defineComponent({ }); }; - return { - uploadOptions, - placeholderText, - autoUpload, - withoutBtn, - uploadText, - disabled, - beforeUpload, - enableDrop, - isDropOVer, - onFileDrop, - onFileOver, - handleClick, - fileUploaders, - onDeleteFile, - fileUpload, - getStatus, - uploadTips, - cancelUpload, - deleteUploadedFile, - multiple, - }; - }, - render() { - const { - placeholderText, - autoUpload, - withoutBtn, - uploadText, - disabled, - enableDrop, - isDropOVer, - onFileDrop, - onFileOver, - handleClick, - fileUploaders, - onDeleteFile, - fileUpload, - uploadedFiles, - deleteUploadedFile, - multiple, - } = this; - - return ( + return () => (
- {this.$slots.default?.() ? ( -
{this.$slots.default()}
+ v-file-drop={{ droppable, isSingle: !multiple, onFileDrop, onFileOver }} + style={`border: ${isDropOVer.value ? '1px solid #15bf15' : '0'}`}> + {ctx.slots.default?.() ? ( +
{ctx.slots.default()}
) : ( -
- {fileUploaders.length === 0 &&
{placeholderText}
} - {fileUploaders.length > 0 && ( +
+ {fileUploaders.value.length === 0 &&
{placeholder.value}
} + {fileUploaders.value.length > 0 && (
    - {fileUploaders.map((fileUploader, index) => ( + {fileUploaders.value.map((fileUploader, index) => (
  • - + {fileUploader.file.name}
)} - {!autoUpload && !withoutBtn && ( - - {uploadText} - - )} -
- {} -
- {this.$slots.preloadFiles?.({ - fileUploaders, - deleteFile: onDeleteFile, - })}
- {this.$slots.uploadedFiles?.({ - uploadedFiles, + {ctx.slots['uploaded-files']?.({ + uploadedFiles: modelValue.value, deleteFile: deleteUploadedFile, })}
diff --git a/packages/devui-vue/devui/upload/src/use-select-files.ts b/packages/devui-vue/devui/upload/src/use-select-files.ts deleted file mode 100644 index 83e5bd353e..0000000000 --- a/packages/devui-vue/devui/upload/src/use-select-files.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { ref } from 'vue' -import { IFileOptions } 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 selectFiles({ multiple, accept, webkitdirectory }) - } - const triggerDropFiles = (files: File[]) => { - return Promise.resolve(files) - } - 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/packages/devui-vue/devui/upload/src/use-upload.ts b/packages/devui-vue/devui/upload/src/use-upload.ts deleted file mode 100644 index 72aa7a48d6..0000000000 --- a/packages/devui-vue/devui/upload/src/use-upload.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ref } from 'vue' -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 = async ( - oneFile? - ): Promise< - | never - | { - file: File - response: any - }[] - > => { - let uploads: any[] = [] - if (oneFile) { - oneFile.percentage = 0 - const uploadedFile = await oneFile.send() - uploads.push(uploadedFile) - } 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 = await Promise.all( - uploadFiles.map(async (fileUploader) => { - fileUploader.percentage = 0 - const uploadedFile = await fileUploader.send() - return uploadedFile - }) - ) - } - if (uploads.length > 0) { - return Promise.resolve(uploads) - } - - return Promise.reject('no files') - } - - const _oneTimeUpload = () => { - const uploads = fileUploaders.value.filter( - (fileUploader) => fileUploader.status !== UploadStatus.uploaded - ) - return 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/packages/devui-vue/docs/components/upload/index.md b/packages/devui-vue/docs/components/upload/index.md index e689eb4c62..ce69a9f9cd 100644 --- a/packages/devui-vue/docs/components/upload/index.md +++ b/packages/devui-vue/docs/components/upload/index.md @@ -2,526 +2,154 @@ 文件上传组件。 -### 何时使用 +#### 何时使用 当需要将文件上传到后端服务器时。 ### 基本用法 -单文件上传、多文件上传、拖动文件上传、禁止上传。 - -

Basic Usage

- :::demo ```vue -``` - -::: - -

Multiple Files

- -:::demo - -```vue - - -``` - -::: - -

Dragdrop

- -:::demo - -```vue - - ``` ::: -

Disabled

+### 多文件上传 :::demo ```vue -``` - -::: - -### 自动上传 - -通过 autoUpload 设置自动上传。 + const uploadedFiles = ref([]); + const uploadOptions = ref({ + uri: 'https://run.mocky.io/v3/132b3ea3-23ea-436b-aed4-c43ef9d116f0', + }); -:::demo - -```vue - - ``` ::: -### 动态上传参数 - -用户可通过 beforeUpload 动态修改上传参数。 +### 拖动文件上传 :::demo ```vue ``` ::: -### 任意区域上传 +### 禁止上传 -用户可通过默认 slot 支持文件任意区域上传。 :::demo ```vue ``` ::: -自定义默认 slot,初始显示已上传文件。 +### 任意区域上传 -:::demo +:::demo 用户可通过默认 slot 支持文件任意区域上传。 ```vue