From e9b1fb68408d25d8101a66baac3e9b92b4a31435 Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Sat, 14 Aug 2021 21:38:59 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20upload=E7=BB=84=E4=BB=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=8D=95=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E9=83=A8?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/style/theme/_animation.scss | 30 +- devui/upload/index.ts | 18 ++ devui/upload/src/file-drop-directive.ts | 86 ++++++ devui/upload/src/file-uploader.ts | 126 ++++++++ devui/upload/src/multiple-upload.tsx | 33 ++ devui/upload/src/single-upload-view.tsx | 15 + devui/upload/src/single-upload.tsx | 289 ++++++++++++++++++ devui/upload/src/upload-types.ts | 159 ++++++++++ devui/upload/src/upload.scss | 226 ++++++++++++++ devui/upload/src/use-select-files.ts | 148 +++++++++ devui/upload/src/use-upload.ts | 133 ++++++++ .../devui-theme/components/SideBarLink.js | 140 +++++---- .../upload/demos/basic/single-basic.vue | 111 +++++++ sites/components/upload/index.md | 30 ++ 14 files changed, 1475 insertions(+), 69 deletions(-) create mode 100644 devui/upload/index.ts create mode 100644 devui/upload/src/file-drop-directive.ts create mode 100755 devui/upload/src/file-uploader.ts create mode 100644 devui/upload/src/multiple-upload.tsx create mode 100644 devui/upload/src/single-upload-view.tsx create mode 100644 devui/upload/src/single-upload.tsx create mode 100644 devui/upload/src/upload-types.ts create mode 100644 devui/upload/src/upload.scss create mode 100644 devui/upload/src/use-select-files.ts create mode 100644 devui/upload/src/use-upload.ts create mode 100644 sites/components/upload/demos/basic/single-basic.vue create mode 100644 sites/components/upload/index.md diff --git a/devui/style/theme/_animation.scss b/devui/style/theme/_animation.scss index 7604080d..33d65e18 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 00000000..df4f877c --- /dev/null +++ b/devui/upload/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import Upload from './src/single-upload' +import fileDropDirective from './src/file-drop-directive' + +Upload.install = function (app: App) { + app.directive('file-drop', fileDropDirective) + app.component(Upload.name, Upload) +} + +export { Upload } + +export default { + title: 'Upload 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 00000000..f6bd9fd0 --- /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) => { + 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 00000000..73a0afea --- /dev/null +++ b/devui/upload/src/file-uploader.ts @@ -0,0 +1,126 @@ +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?): 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_, additionalParameter) { + 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_, additionalParameter, uploadFiles) { + 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() { + if (this.xhr) { + this.xhr.abort() + } + } +} diff --git a/devui/upload/src/multiple-upload.tsx b/devui/upload/src/multiple-upload.tsx new file mode 100644 index 00000000..d0615b11 --- /dev/null +++ b/devui/upload/src/multiple-upload.tsx @@ -0,0 +1,33 @@ +import './upload.scss' + +import { defineComponent, toRefs } from 'vue' +import { uploadProps, UploadProps, UploadStatus } from './upload-types' + +export default defineComponent({ + name: 'DUpload', + props: uploadProps, + emits: [], + setup(props: UploadProps, ctx) { + const { uploadOptions, fileOptions, placeholderText } = toRefs(props) + const uploadStatus = UploadStatus + + return { + placeholderText, + } + }, + render() { + const { placeholderText } = this + + return ( +
+
+
+
+ {placeholderText} +
+
+
+
+ ) + }, +}) diff --git a/devui/upload/src/single-upload-view.tsx b/devui/upload/src/single-upload-view.tsx new file mode 100644 index 00000000..3948908d --- /dev/null +++ b/devui/upload/src/single-upload-view.tsx @@ -0,0 +1,15 @@ +import './upload.scss' + +import { defineComponent } from 'vue' +import { singleUploadViewProps } from './upload-types' +import { useUpload } from './use-upload' + +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 00000000..de5cc9e7 --- /dev/null +++ b/devui/upload/src/single-upload.tsx @@ -0,0 +1,289 @@ +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' + +export default defineComponent({ + name: 'DSingleUpload', + props: uploadProps, + emits: ['fileDrop', 'fileOver', 'fileSelect', 'successEvent', 'errorEvent'], + setup(props: UploadProps, ctx) { + const { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + } = 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) => { + 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) + // results.forEach((result) => { + // this.singleUploadViewComponent.uploadedFilesComponent.addAndOverwriteFile( + // result.file + // ) + // }) + }, + (error) => { + console.error(error) + if (fileUploaders.value[0]) { + fileUploaders.value[0].percentage = 0 + } + // this.singleUploadViewComponent.uploadedFilesComponent.cleanUploadedFiles() + 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( + () => { + // this.singleUploadViewComponent.uploadedFilesComponent.cleanUploadedFiles(); + checkValid() + const file = fileUploaders[0]?.file + if (props.onChange) { + props.onChange(file) + } + if (file) { + ctx.emit('fileSelect', file) + } + if (autoUpload) { + 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.stopPropagation() + const files = getFiles() + deleteFile(files[0]) + } + const onFileDrop = (files) => { + isDropOVer.value = false + _dealFiles( + triggerDropFiles(fileOptions.value, uploadOptions.value, files) + ) + ctx.emit('fileDrop', files[0]) + } + const onFileOver = (event) => { + isDropOVer.value = event + ctx.emit('fileOver', event) + } + return { + placeholderText, + filename, + autoUpload, + withoutBtn, + fileUploaders, + uploadText, + handleClick, + onDeleteFile, + fileUpload, + enableDrop, + onFileDrop, + onFileOver, + isDropOVer, + } + }, + render() { + const { + placeholderText, + filename, + autoUpload, + withoutBtn, + fileUploaders, + uploadText, + handleClick, + onDeleteFile, + fileUpload, + enableDrop, + onFileDrop, + onFileOver, + isDropOVer, + disabled, + } = this + + return ( +
+
+
+ {!filename && ( +
+ {placeholderText} +
+ )} + {!!filename && ( +
+ + {filename} + + onDeleteFile(event)} + > + {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 && ( + 上传失败 + )} + + )} +
+ ) + }, +}) diff --git a/devui/upload/src/upload-types.ts b/devui/upload/src/upload-types.ts new file mode 100644 index 00000000..7fa0eeca --- /dev/null +++ b/devui/upload/src/upload-types.ts @@ -0,0 +1,159 @@ +import type { PropType, ExtractPropTypes } from 'vue' +import { Observable } from 'rxjs' +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, + }, +} 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 +> diff --git a/devui/upload/src/upload.scss b/devui/upload/src/upload.scss new file mode 100644 index 00000000..c8f8eaca --- /dev/null +++ b/devui/upload/src/upload.scss @@ -0,0 +1,226 @@ +@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%; + + .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; + } +} + +:host ::ng-deep .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 00000000..4ae1e79c --- /dev/null +++ b/devui/upload/src/use-select-files.ts @@ -0,0 +1,148 @@ +import { from, Observable } from 'rxjs' +import { mergeMap } from 'rxjs/operators' +import { IFileOptions, IUploadOptions } from './upload-types' + +export const useSelectFiles = () => { + const getNotAllowedFileTypeMsg = (filename, scope) => { + return `支持的文件类型: "${scope}", 您上传的文件"${filename}"不在允许范围内,请重新选择文件` + } + const getBeyondMaximalFileSizeMsg = (filename, maximalSize) => { + return `最大支持上传${maximalSize}MB的文件, 您上传的文件"${filename}"超过可上传文件大小` + } + 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) + ) + } + return { + triggerSelectFiles, + _validateFiles, + triggerDropFiles, + } +} diff --git a/devui/upload/src/use-upload.ts b/devui/upload/src/use-upload.ts new file mode 100644 index 00000000..9385e877 --- /dev/null +++ b/devui/upload/src/use-upload.ts @@ -0,0 +1,133 @@ +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) => { + 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/.vitepress/devui-theme/components/SideBarLink.js b/sites/.vitepress/devui-theme/components/SideBarLink.js index 086f3c12..256526d2 100644 --- a/sites/.vitepress/devui-theme/components/SideBarLink.js +++ b/sites/.vitepress/devui-theme/components/SideBarLink.js @@ -1,75 +1,87 @@ -import { h } from 'vue'; -import { useRoute, useData } from 'vitepress'; -import { joinUrl, isActive } from '../utils'; +import { h } from 'vue' +import { useRoute, useData } from 'vitepress' +import { joinUrl, isActive } from '../utils' export const SideBarLink = (props) => { - const route = useRoute(); - const { site, frontmatter } = useData(); - const depth = props.depth || 1; - const maxDepth = frontmatter.value.sidebarDepth || Infinity; - const headers = route.data.headers; - const text = props.item.text; - const status = props.item.status; - console.log('text status:', text, status); - const link = resolveLink(site.value.base, props.item.link); - const children = props.item.children; - const active = isActive(route, props.item.link); - const childItems = depth < maxDepth - ? createChildren(active, children, headers, depth + 1) - : null; - return h('li', { class: 'sidebar-link' }, [ - h(link ? 'a' : 'p', { - class: { 'sidebar-link-item': true, active }, - href: link - }, [ - text, - status && h('span', { - class: 'sidebar-link-status' - }, status), - ]), - childItems - ]); -}; + const route = useRoute() + const { site, frontmatter } = useData() + const depth = props.depth || 1 + const maxDepth = frontmatter.value.sidebarDepth || Infinity + const headers = route.data.headers + const text = props.item.text + const status = props.item.status + console.log('text status:', text, status) + const link = resolveLink(site.value.base, props.item.link) + const children = props.item.children + const active = isActive(route, props.item.link) + const childItems = + depth < maxDepth + ? createChildren(active, children, headers, depth + 1) + : null + return h('li', { class: 'sidebar-link' }, [ + h( + link ? 'a' : 'p', + { + class: { 'sidebar-link-item': true, active }, + href: link, + }, + [ + text, + status && + h( + 'span', + { + class: 'sidebar-link-status', + }, + status + ), + ] + ), + childItems, + ]) +} function resolveLink(base, path) { - if (path === undefined) { - return path; - } - // keep relative hash to the same page - if (path.startsWith('#')) { - return path; - } - return joinUrl(base, path); + if (path === undefined) { + return path + } + // keep relative hash to the same page + if (path.startsWith('#')) { + return path + } + return joinUrl(base, path) } function createChildren(active, children, headers, depth = 1) { - if (children && children.length > 0) { - return h('ul', { class: 'sidebar-links' }, children.map((c) => { - return h(SideBarLink, { item: c, depth }); - })); - } - return active && headers - ? createChildren(false, resolveHeaders(headers), undefined, depth) - : null; + if (children && children.length > 0) { + return h( + 'ul', + { class: 'sidebar-links' }, + children.map((c) => { + return h(SideBarLink, { item: c, depth }) + }) + ) + } + return active && headers + ? createChildren(false, resolveHeaders(headers), undefined, depth) + : null } function resolveHeaders(headers) { - return mapHeaders(groupHeaders(headers)); + return mapHeaders(groupHeaders(headers)) } function groupHeaders(headers) { - headers = headers.map((h) => Object.assign({}, h)); - let lastH2; - headers.forEach((h) => { - if (h.level === 2) { - lastH2 = h; - } - else if (lastH2) { - ; - (lastH2.children || (lastH2.children = [])).push(h); - } - }); - return headers.filter((h) => h.level === 2); + headers = headers.map((h) => Object.assign({}, h)) + let lastH2 + headers.forEach((h) => { + if (h.level === 2) { + lastH2 = h + } else if (lastH2) { + ;(lastH2.children || (lastH2.children = [])).push(h) + } + }) + return headers.filter((h) => h.level === 2) } function mapHeaders(headers) { - return headers.map((header) => ({ - text: header.title, - link: `#${header.slug}`, - children: header.children ? mapHeaders(header.children) : undefined - })); + return headers.map((header) => ({ + text: header.title, + link: `#${header.slug}`, + children: header.children ? mapHeaders(header.children) : undefined, + })) } diff --git a/sites/components/upload/demos/basic/single-basic.vue b/sites/components/upload/demos/basic/single-basic.vue new file mode 100644 index 00000000..b3afc375 --- /dev/null +++ b/sites/components/upload/demos/basic/single-basic.vue @@ -0,0 +1,111 @@ + + + + diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md new file mode 100644 index 00000000..c2d56272 --- /dev/null +++ b/sites/components/upload/index.md @@ -0,0 +1,30 @@ +# Upload 上传 + +文件上传组件。 + +### 何时使用 + +当需要将文件上传到后端服务器时。 + +### Demo + +
+ +
+ + -- Gitee From 0226c6c327d4946a2c0479398935cb8e164cec6c Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Wed, 18 Aug 2021 14:49:57 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9upload=20md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/upload/src/single-upload.tsx | 8 +- .../upload/demos/basic/single-basic.vue | 111 --------- sites/components/upload/index.md | 215 ++++++++++++++++-- 3 files changed, 200 insertions(+), 134 deletions(-) delete mode 100644 sites/components/upload/demos/basic/single-basic.vue diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx index de5cc9e7..f0200fef 100644 --- a/devui/upload/src/single-upload.tsx +++ b/devui/upload/src/single-upload.tsx @@ -123,7 +123,7 @@ export default defineComponent({ if (file) { ctx.emit('fileSelect', file) } - if (autoUpload) { + if (autoUpload.value) { fileUpload() } }, @@ -215,7 +215,11 @@ export default defineComponent({ )} {!!filename && ( -
+
-
-

Basic Usage

- -

Dragdrop

- -
- Upload -
-

Disabled

- -
- - - - diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md index c2d56272..7f6aebca 100644 --- a/sites/components/upload/index.md +++ b/sites/components/upload/index.md @@ -6,25 +6,198 @@ 当需要将文件上传到后端服务器时。 -### Demo - -
- -
- - +``` + +::: + +

Dragdrop

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

Disabled

+ +:::demo + +```vue + + +``` + +::: -- Gitee From d8c09ee2f5d58214f57bee4f122a1f2518ea0343 Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Thu, 19 Aug 2021 10:54:18 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0showTip,=20?= =?UTF-8?q?=E5=B0=86i18n=E7=9B=B8=E5=85=B3=E5=B8=B8=E9=87=8F=E5=92=8C?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=8F=90=E5=87=BA=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/upload/index.ts | 4 +- devui/upload/src/i18n-upload.ts | 39 +++ devui/upload/src/multiple-upload.tsx | 361 +++++++++++++++++++++++++-- devui/upload/src/single-upload.tsx | 208 ++++++++------- devui/upload/src/upload-types.ts | 84 +++++++ devui/upload/src/upload.scss | 1 + devui/upload/src/use-select-files.ts | 21 +- devui/upload/src/use-upload.ts | 4 +- sites/components/upload/index.md | 287 +++++++++++++++++++++ 9 files changed, 901 insertions(+), 108 deletions(-) create mode 100644 devui/upload/src/i18n-upload.ts diff --git a/devui/upload/index.ts b/devui/upload/index.ts index df4f877c..9bcf7969 100644 --- a/devui/upload/index.ts +++ b/devui/upload/index.ts @@ -1,13 +1,15 @@ 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 } +export { Upload, MultiUpload } export default { title: 'Upload Upload 上传', diff --git a/devui/upload/src/i18n-upload.ts b/devui/upload/src/i18n-upload.ts new file mode 100644 index 00000000..62a10300 --- /dev/null +++ b/devui/upload/src/i18n-upload.ts @@ -0,0 +1,39 @@ +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}"不在允许范围内,请重新选择文件` +} diff --git a/devui/upload/src/multiple-upload.tsx b/devui/upload/src/multiple-upload.tsx index d0615b11..2105b9f3 100644 --- a/devui/upload/src/multiple-upload.tsx +++ b/devui/upload/src/multiple-upload.tsx @@ -1,33 +1,364 @@ import './upload.scss' -import { defineComponent, toRefs } from 'vue' -import { uploadProps, UploadProps, UploadStatus } from './upload-types' +import { defineComponent, toRefs, ref } from 'vue' +import { Observable } from 'rxjs' +import { last, map, debounceTime } from 'rxjs/operators' +import { ToastService } from '../../toast' +import { + uploadProps, + UploadProps, + UploadStatus, + multiUploadProps, +} from './upload-types' +import { useSelectFiles } from './use-select-files' +import { useUpload } from './use-upload' +import { + getFailedFilesCount, + getSelectedFilesCount, + getUploadingFilesCount, + i18nText, +} from './i18n-upload' export default defineComponent({ - name: 'DUpload', - props: uploadProps, + name: 'DMultipleUpload', + props: multiUploadProps, emits: [], - setup(props: UploadProps, ctx) { - const { uploadOptions, fileOptions, placeholderText } = toRefs(props) - const uploadStatus = UploadStatus + setup(props, ctx) { + const { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + oneTimeUpload, + showTip, + } = toRefs(props) + const { + triggerSelectFiles, + _validateFiles, + triggerDropFiles, + checkAllFilesSize, + } = useSelectFiles() + const { + getFiles, + fileUploaders, + addFile, + getFullFiles, + deleteFile, + upload, + resetSameNameFiles, + removeFiles, + _oneTimeUpload, + } = 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 handleClick = () => { + if (disabled.value) { + return + } + _dealFiles(triggerSelectFiles(fileOptions.value)) + } + + const onFileDrop = (files) => { + isDropOVer.value = false + _dealFiles( + triggerDropFiles(fileOptions.value, uploadOptions.value, files) + ) + ctx.emit('fileDrop', files) + } + const onFileOver = (event) => { + isDropOVer.value = event + ctx.emit('fileOver', event) + } + const onDeleteFile = (event, file) => { + event.stopPropagation() + deleteFile(file) + } + 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, fileUploader?) => { + if (event) { + event.stopPropagation() + } + canUpload().then((_canUpload) => { + if (!_canUpload) { + removeFiles() + return + } + const uploadObservable = oneTimeUpload.value + ? _oneTimeUpload() + : upload(fileUploader) + }) + } + + 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' + } + } return { + uploadOptions, + fileOptions, placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + isDropOVer, + onFileDrop, + onFileOver, + handleClick, + fileUploaders, + onDeleteFile, + fileUpload, + showTip, + getStatus, + uploadTips, } }, render() { - const { placeholderText } = this + const { + uploadOptions, + fileOptions, + placeholderText, + autoUpload, + withoutBtn, + uploadText, + disabled, + beforeUpload, + enableDrop, + isDropOVer, + onFileDrop, + onFileOver, + handleClick, + fileUploaders, + onDeleteFile, + fileUpload, + showTip, + getStatus, + uploadTips, + } = this return ( -
-
-
-
- {placeholderText} -
+ <> +
+
+ {fileUploaders.length === 0 && ( +
+ {placeholderText} +
+ )} + {fileUploaders.length > 0 && ( +
    + {fileUploaders.map((fileUploader, index) => ( +
  • + + {fileUploader.file.name} + + + onDeleteFile(event, fileUploader.file) + } + > + {fileUploader.status === UploadStatus.uploaded && ( +
    + +
    + )} + {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} + +
+ )} +
+ )} + ) }, }) diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx index f0200fef..b4eca616 100644 --- a/devui/upload/src/single-upload.tsx +++ b/devui/upload/src/single-upload.tsx @@ -7,7 +7,7 @@ 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, @@ -23,6 +23,7 @@ export default defineComponent({ disabled, beforeUpload, enableDrop, + showTip, } = toRefs(props) const isDropOVer = ref(false) const { @@ -174,6 +175,7 @@ export default defineComponent({ onFileDrop, onFileOver, isDropOVer, + showTip, } }, render() { @@ -192,102 +194,140 @@ export default defineComponent({ onFileOver, isDropOVer, disabled, + showTip, } = this return ( -
+ <>
-
- {!filename && ( -
- {placeholderText} -
- )} - {!!filename && ( -
- +
+ {!filename && ( +
+ {placeholderText} +
+ )} + {!!filename && ( +
- {filename} - - onDeleteFile(event)} - > - {fileUploaders[0].status === UploadStatus.failed && ( - - )} - {fileUploaders[0].status === UploadStatus.uploaded && ( - - )} -
- )} + + {filename} + + onDeleteFile(event)} + > + {fileUploaders[0]?.status === UploadStatus.uploaded && ( +
+ +
+ )} + {fileUploaders[0].status === UploadStatus.failed && ( + + )} + {fileUploaders[0].status === UploadStatus.uploaded && ( + + )} +
+ )} +
+ + + + +
- - - - - + {(!fileUploaders[0] || !fileUploaders[0]?.status) && ( + {uploadText} + )} + {fileUploaders[0]?.status === UploadStatus.uploading && ( + 上传中... + )} + {fileUploaders[0]?.status === UploadStatus.uploaded && ( + 已上传 + )} + {fileUploaders[0]?.status === UploadStatus.failed && ( + 上传失败 + )} + + )}
- - {!autoUpload && !withoutBtn && ( - - {(!fileUploaders[0] || !fileUploaders[0]?.status) && ( - {uploadText} - )} - {fileUploaders[0]?.status === UploadStatus.uploading && ( - 上传中... + {showTip && ( +
+ {fileUploaders[0].status === UploadStatus.uploading && ( + {i18nText.uploading} )} - {fileUploaders[0]?.status === UploadStatus.uploaded && ( - 已上传 + {fileUploaders[0].status === UploadStatus.uploaded && ( +
+ + + {i18nText.uploadSuccess} + +
)} - {fileUploaders[0]?.status === UploadStatus.failed && ( - 上传失败 + {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 index 7fa0eeca..4284d6ed 100644 --- a/devui/upload/src/upload-types.ts +++ b/devui/upload/src/upload-types.ts @@ -157,3 +157,87 @@ 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, + 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, + }, +} diff --git a/devui/upload/src/upload.scss b/devui/upload/src/upload.scss index c8f8eaca..42c4d4a6 100644 --- a/devui/upload/src/upload.scss +++ b/devui/upload/src/upload.scss @@ -84,6 +84,7 @@ overflow-x: hidden; overflow-y: auto; max-width: 100%; + margin: 0; .devui-file-item { height: 22px; diff --git a/devui/upload/src/use-select-files.ts b/devui/upload/src/use-select-files.ts index 4ae1e79c..cce6f208 100644 --- a/devui/upload/src/use-select-files.ts +++ b/devui/upload/src/use-select-files.ts @@ -1,14 +1,15 @@ +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 getNotAllowedFileTypeMsg = (filename, scope) => { - return `支持的文件类型: "${scope}", 您上传的文件"${filename}"不在允许范围内,请重新选择文件` - } - const getBeyondMaximalFileSizeMsg = (filename, maximalSize) => { - return `最大支持上传${maximalSize}MB的文件, 您上传的文件"${filename}"超过可上传文件大小` - } + const BEYOND_MAXIMAL_FILE_SIZE_MSG = ref('') const simulateClickEvent = (input) => { const evt = document.createEvent('MouseEvents') evt.initMouseEvent( @@ -140,9 +141,17 @@ export const useSelectFiles = () => { 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 index 9385e877..a1efde77 100644 --- a/devui/upload/src/use-upload.ts +++ b/devui/upload/src/use-upload.ts @@ -94,7 +94,7 @@ export const useUpload = () => { return from(Promise.reject('no files')) } - const oneTimeUpload = () => { + const _oneTimeUpload = () => { const uploads = fileUploaders.value.filter( (fileUploader) => fileUploader.status !== UploadStatus.uploaded ) @@ -128,6 +128,6 @@ export const useUpload = () => { removeFiles, getSameNameFiles, resetSameNameFiles, - oneTimeUpload, + _oneTimeUpload, } } diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md index 7f6aebca..7a25c7cc 100644 --- a/sites/components/upload/index.md +++ b/sites/components/upload/index.md @@ -201,3 +201,290 @@ export default { ``` ::: + +### 多文件上传 + +多文件上传,支持拖动文件上传、禁止上传。 + +

Basic Usage

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

Upload Directory

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

Dragdrop

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

Disabled

+ +:::demo + +```vue + + +``` + +::: -- Gitee From 3eed81f9cc0ac5017f4ac23ee2989df82831c61a Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Thu, 19 Aug 2021 16:13:49 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8Icon=E7=BB=84?= =?UTF-8?q?=E4=BB=B6,=E8=A1=A5=E5=85=85=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/upload/src/file-drop-directive.ts | 2 +- devui/upload/src/file-uploader.ts | 15 +++- devui/upload/src/multiple-upload.tsx | 75 ++++++++++++----- devui/upload/src/single-upload-view.tsx | 1 - devui/upload/src/single-upload.tsx | 31 +++---- devui/upload/src/upload-types.ts | 8 ++ devui/upload/src/upload.scss | 2 +- sites/components/upload/index.md | 107 +++++++++++++++++++----- 8 files changed, 173 insertions(+), 68 deletions(-) diff --git a/devui/upload/src/file-drop-directive.ts b/devui/upload/src/file-drop-directive.ts index f6bd9fd0..eae3e67e 100644 --- a/devui/upload/src/file-drop-directive.ts +++ b/devui/upload/src/file-drop-directive.ts @@ -72,7 +72,7 @@ const onDrop = (el: HTMLElement, binding: BindingType) => { } const fileDropDirective = { - mounted: (el: HTMLElement, binding: BindingType) => { + mounted: (el: HTMLElement, binding: BindingType): void => { const { enableDrop } = binding.value if (!enableDrop) { return diff --git a/devui/upload/src/file-uploader.ts b/devui/upload/src/file-uploader.ts index 73a0afea..1acef9d9 100755 --- a/devui/upload/src/file-uploader.ts +++ b/devui/upload/src/file-uploader.ts @@ -12,7 +12,7 @@ export class FileUploader { this.status = UploadStatus.preLoad } - send(uploadFiles?): Promise<{ file: File; response: any; }> { + send(uploadFiles?: FileUploader[]): Promise<{ file: File; response: any; }> { return new Promise((resolve, reject) => { const { uri, @@ -94,7 +94,10 @@ export class FileUploader { }) } - parallelUploadFiles(fileFieldName_, additionalParameter) { + parallelUploadFiles( + fileFieldName_: string, + additionalParameter: Record + ): FormData { const formData = new FormData() formData.append(fileFieldName_, this.file, this.file.name) if (additionalParameter) { @@ -105,7 +108,11 @@ export class FileUploader { return formData } - oneTimeUploadFiles(fileFieldName_, additionalParameter, uploadFiles) { + oneTimeUploadFiles( + fileFieldName_: string, + additionalParameter: Record, + uploadFiles: FileUploader[] + ): FormData { const formData = new FormData() uploadFiles.forEach((element) => { formData.append(fileFieldName_, element.file, element.file.name) @@ -118,7 +125,7 @@ export class FileUploader { return formData } - cancel() { + cancel(): void { if (this.xhr) { this.xhr.abort() } diff --git a/devui/upload/src/multiple-upload.tsx b/devui/upload/src/multiple-upload.tsx index 2105b9f3..857a6b65 100644 --- a/devui/upload/src/multiple-upload.tsx +++ b/devui/upload/src/multiple-upload.tsx @@ -4,12 +4,7 @@ import { defineComponent, toRefs, ref } from 'vue' import { Observable } from 'rxjs' import { last, map, debounceTime } from 'rxjs/operators' import { ToastService } from '../../toast' -import { - uploadProps, - UploadProps, - UploadStatus, - multiUploadProps, -} from './upload-types' +import { UploadStatus, multiUploadProps } from './upload-types' import { useSelectFiles } from './use-select-files' import { useUpload } from './use-upload' import { @@ -18,11 +13,19 @@ import { getUploadingFilesCount, i18nText, } from './i18n-upload' +import { FileUploader } from './file-uploader' export default defineComponent({ name: 'DMultipleUpload', props: multiUploadProps, - emits: [], + emits: [ + 'fileDrop', + 'fileOver', + 'fileSelect', + 'successEvent', + 'errorEvent', + 'deleteUploadedFileEvent', + ], setup(props, ctx) { const { uploadOptions, @@ -113,20 +116,21 @@ export default defineComponent({ _dealFiles(triggerSelectFiles(fileOptions.value)) } - const onFileDrop = (files) => { + const onFileDrop = (files: File[]) => { isDropOVer.value = false _dealFiles( triggerDropFiles(fileOptions.value, uploadOptions.value, files) ) ctx.emit('fileDrop', files) } - const onFileOver = (event) => { + const onFileOver = (event: boolean) => { isDropOVer.value = event ctx.emit('fileOver', event) } - const onDeleteFile = (event, file) => { + const onDeleteFile = (event: Event, file: File) => { event.stopPropagation() deleteFile(file) + ctx.emit('deleteUploadedFileEvent', file) } const canUpload = () => { let uploadResult = Promise.resolve(true) @@ -144,7 +148,7 @@ export default defineComponent({ } return uploadResult } - const fileUpload = (event, fileUploader?) => { + const fileUpload = (event: Event, fileUploader?: FileUploader) => { if (event) { event.stopPropagation() } @@ -156,6 +160,20 @@ export default defineComponent({ const uploadObservable = oneTimeUpload.value ? _oneTimeUpload() : upload(fileUploader) + uploadObservable.pipe(last()).subscribe( + (results: Array<{ file: File; response: any; }>) => { + console.log('results', results) + + ctx.emit('successEvent', results) + results.forEach((result) => { + // TODO + // uploadedFiles add file + }) + }, + (error) => { + ctx.emit('errorEvent', error) + } + ) }) } @@ -190,6 +208,16 @@ export default defineComponent({ } } + // 取消上传 + const cancelUpload = () => { + fileUploaders.value = fileUploaders.value.map((fileUploader) => { + if (fileUploader.status === UploadStatus.uploading) { + fileUploader.status = UploadStatus.failed + } + return fileUploader + }) + } + return { uploadOptions, fileOptions, @@ -210,12 +238,11 @@ export default defineComponent({ showTip, getStatus, uploadTips, + cancelUpload, } }, render() { const { - uploadOptions, - fileOptions, placeholderText, autoUpload, withoutBtn, @@ -233,6 +260,7 @@ export default defineComponent({ showTip, getStatus, uploadTips, + cancelUpload, } = this return ( @@ -269,8 +297,9 @@ export default defineComponent({ > {fileUploader.file.name} - onDeleteFile(event, fileUploader.file) } - > - {fileUploader.status === UploadStatus.uploaded && ( + /> + {fileUploader.status === UploadStatus.uploading && (
)} {fileUploader.status === UploadStatus.failed && ( - + )} {fileUploader.status === UploadStatus.uploaded && ( - + )} ))} @@ -336,12 +365,12 @@ export default defineComponent({ {getStatus() === 'uploading' && ( {uploadTips} - {i18nText.cancelUpload} + {i18nText.cancelUpload} )} {getStatus() === 'uploaded' && (
- + {i18nText.uploadSuccess} @@ -349,10 +378,10 @@ export default defineComponent({ )} {getStatus() === 'failed' && ( )} diff --git a/devui/upload/src/single-upload-view.tsx b/devui/upload/src/single-upload-view.tsx index 3948908d..33d4f981 100644 --- a/devui/upload/src/single-upload-view.tsx +++ b/devui/upload/src/single-upload-view.tsx @@ -2,7 +2,6 @@ import './upload.scss' import { defineComponent } from 'vue' import { singleUploadViewProps } from './upload-types' -import { useUpload } from './use-upload' export default defineComponent({ name: 'DSingleUploadView', diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx index b4eca616..d0e78b34 100644 --- a/devui/upload/src/single-upload.tsx +++ b/devui/upload/src/single-upload.tsx @@ -38,7 +38,7 @@ export default defineComponent({ useSelectFiles() const filename = computed(() => (getFiles()[0] || {}).name || '') - const alertMsg = (errorMsg) => { + const alertMsg = (errorMsg: string) => { ToastService.open({ value: [{ severity: 'warn', content: errorMsg }], }) @@ -145,19 +145,19 @@ export default defineComponent({ _dealFiles(triggerSelectFiles(fileOptions.value)) } - const onDeleteFile = (event) => { + const onDeleteFile = (event: Event) => { event.stopPropagation() const files = getFiles() deleteFile(files[0]) } - const onFileDrop = (files) => { + const onFileDrop = (files: File[]) => { isDropOVer.value = false _dealFiles( triggerDropFiles(fileOptions.value, uploadOptions.value, files) ) ctx.emit('fileDrop', files[0]) } - const onFileOver = (event) => { + const onFileOver = (event: boolean) => { isDropOVer.value = event ctx.emit('fileOver', event) } @@ -233,8 +233,9 @@ export default defineComponent({ > {filename} - onDeleteFile(event)} - > - {fileUploaders[0]?.status === UploadStatus.uploaded && ( + /> + {fileUploaders[0]?.status === UploadStatus.uploading && (
)} {fileUploaders[0].status === UploadStatus.failed && ( - + )} {fileUploaders[0].status === UploadStatus.uploaded && ( - + )}
)} @@ -305,20 +306,20 @@ export default defineComponent({
{showTip && (
- {fileUploaders[0].status === UploadStatus.uploading && ( + {fileUploaders[0]?.status === UploadStatus.uploading && ( {i18nText.uploading} )} - {fileUploaders[0].status === UploadStatus.uploaded && ( + {fileUploaders[0]?.status === UploadStatus.uploaded && (
- + {i18nText.uploadSuccess}
)} - {fileUploaders[0].status === UploadStatus.failed && ( + {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 index 4284d6ed..a8cc189c 100644 --- a/devui/upload/src/upload-types.ts +++ b/devui/upload/src/upload-types.ts @@ -129,6 +129,10 @@ export const uploadProps = { 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: { @@ -240,4 +244,8 @@ export const multiUploadProps = { type: Function as PropType<(v: { file: File; response: any; }[]) => void>, default: undefined, }, + deleteUploadedFileEvent: { + type: Function as PropType<(v: string) => void>, + default: undefined, + }, } diff --git a/devui/upload/src/upload.scss b/devui/upload/src/upload.scss index 42c4d4a6..edabc301 100644 --- a/devui/upload/src/upload.scss +++ b/devui/upload/src/upload.scss @@ -163,7 +163,7 @@ } } -:host ::ng-deep .devui-upload-progress { +.devui-input-group .devui-upload-progress { width: 16px; height: 16px; } diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md index 7a25c7cc..12c1a489 100644 --- a/sites/components/upload/index.md +++ b/sites/components/upload/index.md @@ -35,13 +35,12 @@ export default { webkitdirectory: false, }) const uploadOptions = reactive({ - uri: '/upload', + uri: 'http://localhost:4000/files/upload', headers: {}, additionalParameter, maximumSize: 0.5, method: 'POST', - fileFieldName: 'dFile', - withCredentials: true, + fileFieldName: 'file', responseType: 'json', }) return { @@ -71,10 +70,10 @@ export default { :without-btn="true" file-path="name" :before-upload="beforeUpload" - :success-event="onSuccess" - :error-event="onError" - :file-drop="fileDrop" - :file-over="fileOver" + @success-event="onSuccess" + @error-event="onError" + @file-drop="fileDrop" + @file-over="fileOver" />
Upload @@ -150,8 +149,8 @@ export default { placeholder-text="Upload" file-path="name" :before-upload="beforeUpload" - :success-event="onSuccess" - :error-event="onError" + @success-event="onSuccess" + @error-event="onError" :disabled="true" /> @@ -217,10 +216,10 @@ export default { :upload-options="uploadOptions" :uploaded-files="uploadedFiles" filePath="name" - :success-event="onSuccess" - :error-event="onError" + @success-event="onSuccess" + @error-event="onError" :showTip="true" - :file-select="fileSelect" + @file-select="fileSelect" /> +``` + +::: -- Gitee From e7638282b7b1d78af25060fc2a557466cf0ef11b Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Fri, 20 Aug 2021 14:54:35 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85upload=E6=96=87?= =?UTF-8?q?=E6=A1=A3,=20=E6=B7=BB=E5=8A=A0multi-upload,fix=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/upload/index.ts | 2 +- devui/upload/src/i18n-upload.ts | 3 + devui/upload/src/multiple-upload.tsx | 71 ++++- devui/upload/src/single-upload.tsx | 28 +- devui/upload/src/upload-types.ts | 9 +- devui/upload/src/use-upload.ts | 4 + sites/components/upload/index.md | 436 ++++++++++++++++++++++++++- 7 files changed, 514 insertions(+), 39 deletions(-) diff --git a/devui/upload/index.ts b/devui/upload/index.ts index 9bcf7969..6283e55c 100644 --- a/devui/upload/index.ts +++ b/devui/upload/index.ts @@ -12,7 +12,7 @@ Upload.install = function (app: App) { export { Upload, MultiUpload } export default { - title: 'Upload Upload 上传', + title: 'Upload 上传', category: '数据录入', install(app: App): void { app.use(Upload as any) diff --git a/devui/upload/src/i18n-upload.ts b/devui/upload/src/i18n-upload.ts index 62a10300..b2cbbc38 100644 --- a/devui/upload/src/i18n-upload.ts +++ b/devui/upload/src/i18n-upload.ts @@ -37,3 +37,6 @@ export const getNotAllowedFileTypeMsg = ( ): 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 index 857a6b65..da3a2315 100644 --- a/devui/upload/src/multiple-upload.tsx +++ b/devui/upload/src/multiple-upload.tsx @@ -12,6 +12,7 @@ import { getSelectedFilesCount, getUploadingFilesCount, i18nText, + getExistSameNameFilesMsg, } from './i18n-upload' import { FileUploader } from './file-uploader' @@ -25,6 +26,7 @@ export default defineComponent({ 'successEvent', 'errorEvent', 'deleteUploadedFileEvent', + 'update:uploadedFiles', ], setup(props, ctx) { const { @@ -39,6 +41,7 @@ export default defineComponent({ enableDrop, oneTimeUpload, showTip, + uploadedFiles, } = toRefs(props) const { triggerSelectFiles, @@ -56,6 +59,7 @@ export default defineComponent({ resetSameNameFiles, removeFiles, _oneTimeUpload, + getSameNameFiles, } = useUpload() const isDropOVer = ref(false) const uploadTips = ref('') @@ -104,9 +108,28 @@ export default defineComponent({ }), debounceTime(100) ) - .subscribe(() => { - checkValid() - }) + .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 = () => { @@ -130,7 +153,14 @@ export default defineComponent({ 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) @@ -162,13 +192,10 @@ export default defineComponent({ : upload(fileUploader) uploadObservable.pipe(last()).subscribe( (results: Array<{ file: File; response: any; }>) => { - console.log('results', results) - ctx.emit('successEvent', results) - results.forEach((result) => { - // TODO - // uploadedFiles add file - }) + const newFiles = results.map((result) => result.file) + const newUploadedFiles = [...newFiles, ...uploadedFiles.value] + ctx.emit('update:uploadedFiles', newUploadedFiles) }, (error) => { ctx.emit('errorEvent', error) @@ -212,6 +239,8 @@ export default defineComponent({ const cancelUpload = () => { fileUploaders.value = fileUploaders.value.map((fileUploader) => { if (fileUploader.status === UploadStatus.uploading) { + // 取消上传请求 + fileUploader.cancel() fileUploader.status = UploadStatus.failed } return fileUploader @@ -239,6 +268,7 @@ export default defineComponent({ getStatus, uploadTips, cancelUpload, + deleteUploadedFile, } }, render() { @@ -261,8 +291,9 @@ export default defineComponent({ getStatus, uploadTips, cancelUpload, + uploadedFiles, + deleteUploadedFile, } = this - return ( <>
)} {fileUploader.status === UploadStatus.failed && ( - + )} {fileUploader.status === UploadStatus.uploaded && ( @@ -370,7 +401,7 @@ export default defineComponent({ )} {getStatus() === 'uploaded' && (
- + {i18nText.uploadSuccess} @@ -378,7 +409,7 @@ export default defineComponent({ )} {getStatus() === 'failed' && (
- + {uploadTips} {i18nText.reUpload} @@ -387,6 +418,18 @@ export default defineComponent({ )}
)} +
+ {this.$slots.preloadFiles?.({ + fileUploaders, + deleteFile: onDeleteFile, + })} +
+
+ {this.$slots.uploadedFiles?.({ + uploadedFiles, + deleteFile: deleteUploadedFile, + })} +
) }, diff --git a/devui/upload/src/single-upload.tsx b/devui/upload/src/single-upload.tsx index d0e78b34..9ceb1f5d 100644 --- a/devui/upload/src/single-upload.tsx +++ b/devui/upload/src/single-upload.tsx @@ -11,7 +11,14 @@ import { i18nText } from './i18n-upload' export default defineComponent({ name: 'DSingleUpload', props: uploadProps, - emits: ['fileDrop', 'fileOver', 'fileSelect', 'successEvent', 'errorEvent'], + emits: [ + 'fileDrop', + 'fileOver', + 'fileSelect', + 'successEvent', + 'errorEvent', + 'update:uploadedFiles', + ], setup(props: UploadProps, ctx) { const { uploadOptions, @@ -24,6 +31,7 @@ export default defineComponent({ beforeUpload, enableDrop, showTip, + uploadedFiles, } = toRefs(props) const isDropOVer = ref(false) const { @@ -73,18 +81,15 @@ export default defineComponent({ .subscribe( (results: Array<{ file: File; response: any; }>) => { ctx.emit('successEvent', results) - // results.forEach((result) => { - // this.singleUploadViewComponent.uploadedFilesComponent.addAndOverwriteFile( - // result.file - // ) - // }) + 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 } - // this.singleUploadViewComponent.uploadedFilesComponent.cleanUploadedFiles() ctx.emit('errorEvent', error) } ) @@ -115,7 +120,6 @@ export default defineComponent({ ) .subscribe( () => { - // this.singleUploadViewComponent.uploadedFilesComponent.cleanUploadedFiles(); checkValid() const file = fileUploaders[0]?.file if (props.onChange) { @@ -253,7 +257,7 @@ export default defineComponent({ isCircle={true} percentage={fileUploaders[0].percentage} barbgcolor="#50D4AB" - strokeWidth="8" + strokeWidth={8} showContent={false} >
@@ -311,7 +315,7 @@ export default defineComponent({ )} {fileUploaders[0]?.status === UploadStatus.uploaded && (
- + {i18nText.uploadSuccess} @@ -319,10 +323,10 @@ export default defineComponent({ )} {fileUploaders[0]?.status === UploadStatus.failed && (
- + {i18nText.uploadFailed} - {i18nText.reUpload} + {i18nText.reUpload}
)} diff --git a/devui/upload/src/upload-types.ts b/devui/upload/src/upload-types.ts index a8cc189c..8820d238 100644 --- a/devui/upload/src/upload-types.ts +++ b/devui/upload/src/upload-types.ts @@ -1,5 +1,6 @@ import type { PropType, ExtractPropTypes } from 'vue' import { Observable } from 'rxjs' +import { FileUploader } from './file-uploader' export class IUploadOptions { // 上传接口地址 uri: string @@ -188,7 +189,7 @@ export const multiUploadProps = { default: false, }, uploadedFiles: { - type: Array, + type: Array as PropType, default: () => [], }, enableDrop: { @@ -248,4 +249,10 @@ export const multiUploadProps = { 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/use-upload.ts b/devui/upload/src/use-upload.ts index a1efde77..48427ba1 100644 --- a/devui/upload/src/use-upload.ts +++ b/devui/upload/src/use-upload.ts @@ -102,6 +102,10 @@ export const useUpload = () => { } const deleteFile = (file) => { + const deleteUploadFile = fileUploaders.value.find( + (fileUploader) => fileUploader.file === file + ) + deleteUploadFile.cancel() fileUploaders.value = fileUploaders.value.filter((fileUploader) => { return file !== fileUploader.file }) diff --git a/sites/components/upload/index.md b/sites/components/upload/index.md index 12c1a489..30ac446e 100644 --- a/sites/components/upload/index.md +++ b/sites/components/upload/index.md @@ -8,7 +8,7 @@ ### 基本用法 -单文件上传、拖动文件上传、v-model 双向绑定、禁止上传。 +单文件上传、拖动文件上传、禁止上传。

Basic Usage

@@ -62,6 +62,7 @@ export default { ```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 +} +``` -- Gitee From 1d102f0297f6f6da2f912e1c4635c6ea8e0e45c6 Mon Sep 17 00:00:00 2001 From: ElsaOOo Date: Fri, 20 Aug 2021 15:05:15 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80SideBarLink.js?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui-theme/components/SideBarLink.js | 140 ++++++++---------- 1 file changed, 64 insertions(+), 76 deletions(-) diff --git a/sites/.vitepress/devui-theme/components/SideBarLink.js b/sites/.vitepress/devui-theme/components/SideBarLink.js index 256526d2..086f3c12 100644 --- a/sites/.vitepress/devui-theme/components/SideBarLink.js +++ b/sites/.vitepress/devui-theme/components/SideBarLink.js @@ -1,87 +1,75 @@ -import { h } from 'vue' -import { useRoute, useData } from 'vitepress' -import { joinUrl, isActive } from '../utils' +import { h } from 'vue'; +import { useRoute, useData } from 'vitepress'; +import { joinUrl, isActive } from '../utils'; export const SideBarLink = (props) => { - const route = useRoute() - const { site, frontmatter } = useData() - const depth = props.depth || 1 - const maxDepth = frontmatter.value.sidebarDepth || Infinity - const headers = route.data.headers - const text = props.item.text - const status = props.item.status - console.log('text status:', text, status) - const link = resolveLink(site.value.base, props.item.link) - const children = props.item.children - const active = isActive(route, props.item.link) - const childItems = - depth < maxDepth - ? createChildren(active, children, headers, depth + 1) - : null - return h('li', { class: 'sidebar-link' }, [ - h( - link ? 'a' : 'p', - { - class: { 'sidebar-link-item': true, active }, - href: link, - }, - [ - text, - status && - h( - 'span', - { - class: 'sidebar-link-status', - }, - status - ), - ] - ), - childItems, - ]) -} + const route = useRoute(); + const { site, frontmatter } = useData(); + const depth = props.depth || 1; + const maxDepth = frontmatter.value.sidebarDepth || Infinity; + const headers = route.data.headers; + const text = props.item.text; + const status = props.item.status; + console.log('text status:', text, status); + const link = resolveLink(site.value.base, props.item.link); + const children = props.item.children; + const active = isActive(route, props.item.link); + const childItems = depth < maxDepth + ? createChildren(active, children, headers, depth + 1) + : null; + return h('li', { class: 'sidebar-link' }, [ + h(link ? 'a' : 'p', { + class: { 'sidebar-link-item': true, active }, + href: link + }, [ + text, + status && h('span', { + class: 'sidebar-link-status' + }, status), + ]), + childItems + ]); +}; function resolveLink(base, path) { - if (path === undefined) { - return path - } - // keep relative hash to the same page - if (path.startsWith('#')) { - return path - } - return joinUrl(base, path) + if (path === undefined) { + return path; + } + // keep relative hash to the same page + if (path.startsWith('#')) { + return path; + } + return joinUrl(base, path); } function createChildren(active, children, headers, depth = 1) { - if (children && children.length > 0) { - return h( - 'ul', - { class: 'sidebar-links' }, - children.map((c) => { - return h(SideBarLink, { item: c, depth }) - }) - ) - } - return active && headers - ? createChildren(false, resolveHeaders(headers), undefined, depth) - : null + if (children && children.length > 0) { + return h('ul', { class: 'sidebar-links' }, children.map((c) => { + return h(SideBarLink, { item: c, depth }); + })); + } + return active && headers + ? createChildren(false, resolveHeaders(headers), undefined, depth) + : null; } function resolveHeaders(headers) { - return mapHeaders(groupHeaders(headers)) + return mapHeaders(groupHeaders(headers)); } function groupHeaders(headers) { - headers = headers.map((h) => Object.assign({}, h)) - let lastH2 - headers.forEach((h) => { - if (h.level === 2) { - lastH2 = h - } else if (lastH2) { - ;(lastH2.children || (lastH2.children = [])).push(h) - } - }) - return headers.filter((h) => h.level === 2) + headers = headers.map((h) => Object.assign({}, h)); + let lastH2; + headers.forEach((h) => { + if (h.level === 2) { + lastH2 = h; + } + else if (lastH2) { + ; + (lastH2.children || (lastH2.children = [])).push(h); + } + }); + return headers.filter((h) => h.level === 2); } function mapHeaders(headers) { - return headers.map((header) => ({ - text: header.title, - link: `#${header.slug}`, - children: header.children ? mapHeaders(header.children) : undefined, - })) + return headers.map((header) => ({ + text: header.title, + link: `#${header.slug}`, + children: header.children ? mapHeaders(header.children) : undefined + })); } -- Gitee