mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
refactor(components): refactor upload (#6014)
* refactor(components): refactor ElUpload * refactor(components): refactor upload * test: use jsx * refactor: resolve review comments * fix: ts error * refactor: re-order imports * refactor: rename * fix: infinity watch * refactor: rename * refactor: address PR comments Co-authored-by: Kevin <sxzz@sxzz.moe>
This commit is contained in:
parent
ce10babc22
commit
13ffea1114
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -25,5 +25,6 @@
|
||||
"i18n-ally.localesPaths": "packages/locale/lang",
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.enabledFrameworks": ["vue", "vue-sfc"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"iconify.includes": ["ep"]
|
||||
}
|
||||
|
@ -91,33 +91,33 @@ upload/manual
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------- | ------- |
|
||||
| action | required, request URL | string | — | — |
|
||||
| headers | request headers | object | — | — |
|
||||
| method | set upload request method | string | post/put/patch | post |
|
||||
| multiple | whether uploading multiple files is permitted | boolean | — | — |
|
||||
| data | additions options of request | object | — | — |
|
||||
| name | key name for uploaded file | string | — | file |
|
||||
| with-credentials | whether cookies are sent | boolean | — | false |
|
||||
| show-file-list | whether to show the uploaded file list | boolean | — | true |
|
||||
| drag | whether to activate drag and drop mode | boolean | — | false |
|
||||
| accept | accepted [file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept), will not work when `thumbnail-mode` is `true` | string | — | — |
|
||||
| on-preview | hook function when clicking the uploaded files | function(file) | — | — |
|
||||
| on-remove | hook function when files are removed | function(file, fileList) | — | — |
|
||||
| on-success | hook function when uploaded successfully | function(response, file, fileList) | — | — |
|
||||
| on-error | hook function when some errors occurs | function(err, file, fileList) | — | — |
|
||||
| on-progress | hook function when some progress occurs | function(event, file, fileList) | — | — |
|
||||
| on-change | hook function when select file or upload file success or upload file fail | function(file, fileList) | — | — |
|
||||
| before-upload | hook function before uploading with the file to be uploaded as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, uploading will be aborted | function(file) | — | — |
|
||||
| before-remove | hook function before removing a file with the file and file list as its parameters. If `false` is returned or a `Promise` is returned and then is rejected, removing will be aborted. | function(file, fileList) | — | — |
|
||||
| file-list | default uploaded files, e.g. [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] | array | — | [] |
|
||||
| list-type | type of fileList | string | text/picture/picture-card | text |
|
||||
| auto-upload | whether to auto upload file | boolean | — | true |
|
||||
| http-request | override default xhr behavior, allowing you to implement your own upload-file's request | function | — | — |
|
||||
| disabled | whether to disable upload | boolean | — | false |
|
||||
| limit | maximum number of uploads allowed | number | — | — |
|
||||
| on-exceed | hook function when limit is exceeded | function(files, fileList) | — | - |
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------- | ------- |
|
||||
| action | required, request URL | string | — | — |
|
||||
| headers | request headers | object | — | — |
|
||||
| method | set upload request method | string | post/put/patch | post |
|
||||
| multiple | whether uploading multiple files is permitted | boolean | — | — |
|
||||
| data | additions options of request | object | — | — |
|
||||
| name | key name for uploaded file | string | — | file |
|
||||
| with-credentials | whether cookies are sent | boolean | — | false |
|
||||
| show-file-list | whether to show the uploaded file list | boolean | — | true |
|
||||
| drag | whether to activate drag and drop mode | boolean | — | false |
|
||||
| accept | accepted [file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept), will not work when `thumbnail-mode` is `true` | string | — | — |
|
||||
| on-preview | hook function when clicking the uploaded files | function(file: UploadFile): void | — | — |
|
||||
| on-remove | hook function when files are removed | function(file: UploadFile, fileList: UploadFile[]): void | — | — |
|
||||
| on-success | hook function when uploaded successfully | function(response: any, file: UploadFile, fileList: UploadFile[]): void | — | — |
|
||||
| on-error | hook function when some errors occurs | function(error, file: UploadFile, fileList: UploadFile[]): void | — | — |
|
||||
| on-progress | hook function when some progress occurs | function(evt: UploadProgressEvent, file: UploadFile, fileList: UploadFile[]): void | — | — |
|
||||
| on-change | hook function when select file or upload file success or upload file fail | function(file: UploadFile, fileList: UploadFile[]): void | — | — |
|
||||
| before-upload | hook function before uploading with the file to be uploaded as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, uploading will be aborted | function(file: RawFile): Awaitable<void \| undefined \| null \| boolean \| File \| Blob> | — | — |
|
||||
| before-remove | hook function before removing a file with the file and file list as its parameters. If `false` is returned or a `Promise` is returned and then is rejected, removing will be aborted. | function(file: UploadFile, fileList: UploadFile[]): Awaitable\<boolean\> | — | — |
|
||||
| file-list | default uploaded files, e.g. [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] | Array\<UploadFile\> | — | [] |
|
||||
| list-type | type of fileList | string | text/picture/picture-card | text |
|
||||
| auto-upload | whether to auto upload file | boolean | — | true |
|
||||
| http-request | override default xhr behavior, allowing you to implement your own upload-file's request | (options: UploadRequestOptions) => XMLHttpRequest \| Promise\<unknown\> | — | — |
|
||||
| disabled | whether to disable upload | boolean | — | false |
|
||||
| limit | maximum number of uploads allowed | number | — | — |
|
||||
| on-exceed | hook function when limit is exceeded | function(files: File[], uploadFiles: UploadFile[]): void | — | - |
|
||||
|
||||
## Slots
|
||||
|
||||
@ -130,10 +130,10 @@ upload/manual
|
||||
|
||||
## Methods
|
||||
|
||||
| Methods Name | Description | Parameters | Default |
|
||||
| ------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| clearFiles | clear the file list (this method is not supported in the `before-upload` hook). | UploadStatus[] (UploadStatus = 'ready' \| 'uploading' \| 'success' \| 'fail') | ['ready', 'uploading', 'success', 'fail'] |
|
||||
| abort | cancel upload request | ( file: fileList's item ) | - |
|
||||
| submit | upload the file list manually | — | - |
|
||||
| handleStart | select the file manually | ( file: files' item) | - |
|
||||
| handleRemove | remove the file manually | ( file: fileList's item ) | - |
|
||||
| Methods Name | Description | Parameters | Default |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| clearFiles | clear the file list (this method is not supported in the `before-upload` hook). | (status: Array\<"ready"\|"uploading"\|"success"\|"fail"\>) => void | ['ready', 'uploading', 'success', 'fail'] |
|
||||
| abort | cancel upload request | (file: UploadFile) => void | - |
|
||||
| submit | upload the file list manually | — | - |
|
||||
| handleStart | select the file manually | (file: File) => void | - |
|
||||
| handleRemove | remove the file manually. `file` and `rawFile` has been merged. `rawFile` will be removed in `v2.2.0` | (file: UploadFile \| UploadRawFile, rawFile?:UploadRawFile) => void | - |
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-upload action="#" list-type="picture-card" :auto-upload="false">
|
||||
<template #default>
|
||||
<el-icon><plus /></el-icon>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
@ -18,14 +18,14 @@
|
||||
class="el-upload-list__item-delete"
|
||||
@click="handleDownload(file)"
|
||||
>
|
||||
<el-icon><download /></el-icon>
|
||||
<el-icon><Download /></el-icon>
|
||||
</span>
|
||||
<span
|
||||
v-if="!disabled"
|
||||
class="el-upload-list__item-delete"
|
||||
@click="handleRemove(file)"
|
||||
>
|
||||
<el-icon><delete /></el-icon>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-icon><plus /></el-icon>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img width="100%" :src="dialogImageUrl" alt="" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { provide, h, defineComponent } from 'vue'
|
||||
import { provide, h, defineComponent, computed } from 'vue'
|
||||
import makeMount from '@element-plus/test-utils/make-mount'
|
||||
import { uploadContextKey } from '@element-plus/tokens'
|
||||
import UploadDragger from '../src/upload-dragger.vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
@ -9,7 +10,7 @@ const Wrapper = defineComponent({
|
||||
onDrop: Function,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide('uploader', { accept: 'video/*' })
|
||||
provide(uploadContextKey, { accept: computed(() => 'video/*') })
|
||||
return () => h(UploadDragger, props, slots)
|
||||
},
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { h } from 'vue'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
|
||||
import makeMount from '@element-plus/test-utils/make-mount'
|
||||
@ -17,7 +16,7 @@ describe('<upload-list />', () => {
|
||||
test('should render correct', () => {
|
||||
const wrapper = mount({
|
||||
slots: {
|
||||
default: ({ file }: { file: File }) => h('div', null, file.name),
|
||||
default: ({ file }: { file: File }) => <div>{file.name}</div>,
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toBe(testName)
|
@ -1,9 +1,10 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { computed, defineComponent, h, nextTick, provide } from 'vue'
|
||||
import makeMount from '@element-plus/test-utils/make-mount'
|
||||
import { on } from '@element-plus/utils'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
|
||||
import Upload from '../src/upload.vue'
|
||||
import { uploadContextKey } from '@element-plus/tokens'
|
||||
import UploadContent from '../src/upload-content.vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
const action = 'test-action'
|
||||
@ -16,7 +17,20 @@ const mockGetFile = (element: HTMLInputElement, files: File[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
const mount = makeMount(Upload, {
|
||||
const Wrapper = defineComponent({
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide(uploadContextKey, { accept: computed(() => 'video/*') })
|
||||
return () => h(UploadContent, props, slots)
|
||||
},
|
||||
})
|
||||
|
||||
const mount = makeMount(Wrapper, {
|
||||
props: {
|
||||
action,
|
||||
},
|
||||
|
@ -1,13 +1,10 @@
|
||||
import Upload from './src/index.vue'
|
||||
import { withInstall } from '@element-plus/utils'
|
||||
import Upload from './src/upload.vue'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { SFCWithInstall } from '@element-plus/utils'
|
||||
export const ElUpload = withInstall(Upload)
|
||||
export default ElUpload
|
||||
|
||||
Upload.install = (app: App): void => {
|
||||
app.component(Upload.name, Upload)
|
||||
}
|
||||
|
||||
const _Upload = Upload as SFCWithInstall<typeof Upload>
|
||||
|
||||
export default _Upload
|
||||
export const ElUpload = _Upload
|
||||
export * from './src/upload'
|
||||
export * from './src/upload-content'
|
||||
export * from './src/upload-list'
|
||||
export * from './src/upload-dragger'
|
||||
|
@ -1,13 +1,29 @@
|
||||
import { hasOwn } from '@element-plus/utils'
|
||||
import { isNil } from 'lodash-unified'
|
||||
import { throwError } from '@element-plus/utils'
|
||||
import type {
|
||||
ElUploadProgressEvent,
|
||||
ElUploadRequestOptions,
|
||||
ElUploadAjaxError,
|
||||
} from './upload.type'
|
||||
UploadRequestHandler,
|
||||
UploadProgressEvent,
|
||||
UploadRequestOptions,
|
||||
} from './upload'
|
||||
|
||||
const SCOPE = 'ElUpload'
|
||||
|
||||
export class UploadAjaxError extends Error {
|
||||
status: number
|
||||
method: string
|
||||
url: string
|
||||
|
||||
constructor(message: string, status: number, method: string, url: string) {
|
||||
super(message)
|
||||
this.status = status
|
||||
this.method = method
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
|
||||
function getError(
|
||||
action: string,
|
||||
option: ElUploadRequestOptions,
|
||||
option: UploadRequestOptions,
|
||||
xhr: XMLHttpRequest
|
||||
) {
|
||||
let msg: string
|
||||
@ -19,11 +35,7 @@ function getError(
|
||||
msg = `fail to ${option.method} ${action} ${xhr.status}`
|
||||
}
|
||||
|
||||
const err = new Error(msg) as ElUploadAjaxError
|
||||
err.status = xhr.status
|
||||
err.method = option.method
|
||||
err.url = action
|
||||
return err
|
||||
return new UploadAjaxError(msg, xhr.status, option.method, action)
|
||||
}
|
||||
|
||||
function getBody(xhr: XMLHttpRequest): XMLHttpRequestResponseType {
|
||||
@ -39,44 +51,40 @@ function getBody(xhr: XMLHttpRequest): XMLHttpRequestResponseType {
|
||||
}
|
||||
}
|
||||
|
||||
export default function upload(option: ElUploadRequestOptions) {
|
||||
if (typeof XMLHttpRequest === 'undefined') {
|
||||
return
|
||||
}
|
||||
export const ajaxUpload: UploadRequestHandler = (option) => {
|
||||
if (typeof XMLHttpRequest === 'undefined')
|
||||
throwError(SCOPE, 'XMLHttpRequest is undefined')
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
const action = option.action
|
||||
|
||||
if (xhr.upload) {
|
||||
xhr.upload.onprogress = function progress(e) {
|
||||
if (e.total > 0) {
|
||||
;(e as ElUploadProgressEvent).percent = (e.loaded / e.total) * 100
|
||||
}
|
||||
option.onProgress(e)
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
if (option.data) {
|
||||
Object.keys(option.data).forEach((key) => {
|
||||
formData.append(key, option.data[key])
|
||||
xhr.upload.addEventListener('progress', (evt) => {
|
||||
const progressEvt = evt as UploadProgressEvent
|
||||
progressEvt.percent = evt.total > 0 ? (evt.loaded / evt.total) * 100 : 0
|
||||
option.onProgress(progressEvt)
|
||||
})
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
if (option.data) {
|
||||
for (const [key, value] of Object.entries(option.data)) {
|
||||
if (Array.isArray(value)) formData.append(key, ...value)
|
||||
else formData.append(key, value)
|
||||
}
|
||||
}
|
||||
formData.append(option.filename, option.file, option.file.name)
|
||||
|
||||
xhr.onerror = function error() {
|
||||
xhr.addEventListener('error', () => {
|
||||
option.onError(getError(action, option, xhr))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.onload = function onload() {
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
return option.onError(getError(action, option, xhr))
|
||||
}
|
||||
|
||||
option.onSuccess(getBody(xhr))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.open(option.method, action, true)
|
||||
|
||||
@ -85,17 +93,13 @@ export default function upload(option: ElUploadRequestOptions) {
|
||||
}
|
||||
|
||||
const headers = option.headers || {}
|
||||
|
||||
for (const item in headers) {
|
||||
if (hasOwn(headers, item) && headers[item] !== null) {
|
||||
xhr.setRequestHeader(item, headers[item])
|
||||
}
|
||||
}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
xhr.setRequestHeader(key, value)
|
||||
})
|
||||
headers.forEach((value, key) => xhr.setRequestHeader(key, value))
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (isNil(value)) continue
|
||||
xhr.setRequestHeader(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(formData)
|
||||
|
@ -1,254 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
h,
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
ref,
|
||||
provide,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { elFormKey } from '@element-plus/tokens'
|
||||
|
||||
import ajax from './ajax'
|
||||
import UploadList from './upload-list.vue'
|
||||
import Upload from './upload.vue'
|
||||
import useHandlers from './useHandlers'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { ElFormContext } from '@element-plus/tokens'
|
||||
import type { Nullable } from '@element-plus/utils'
|
||||
import type {
|
||||
ListType,
|
||||
UploadFile,
|
||||
FileHandler,
|
||||
FileResultHandler,
|
||||
} from './upload.type'
|
||||
|
||||
type PFileHandler<T> = PropType<FileHandler<T>>
|
||||
type PFileResultHandler<T = any> = PropType<FileResultHandler<T>>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElUpload',
|
||||
components: {
|
||||
Upload,
|
||||
UploadList,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object as PropType<Headers>,
|
||||
default: () => ({}),
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withCredentials: Boolean,
|
||||
showFileList: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'select',
|
||||
},
|
||||
beforeUpload: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
beforeRemove: {
|
||||
type: Function as PFileHandler<boolean>,
|
||||
default: NOOP,
|
||||
},
|
||||
onRemove: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onChange: {
|
||||
type: Function as PFileHandler<void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onPreview: {
|
||||
type: Function as PropType<() => void>,
|
||||
default: NOOP,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function as PFileResultHandler,
|
||||
default: NOOP,
|
||||
},
|
||||
onProgress: {
|
||||
type: Function as PFileResultHandler<ProgressEvent>,
|
||||
default: NOOP,
|
||||
},
|
||||
onError: {
|
||||
type: Function as PFileResultHandler<Error>,
|
||||
default: NOOP,
|
||||
},
|
||||
fileList: {
|
||||
type: Array as PropType<UploadFile[]>,
|
||||
default: () => {
|
||||
return [] as UploadFile[]
|
||||
},
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<ListType>,
|
||||
default: 'text' as ListType, // text,picture,picture-card
|
||||
},
|
||||
httpRequest: {
|
||||
type: Function,
|
||||
default: ajax,
|
||||
},
|
||||
disabled: Boolean,
|
||||
limit: {
|
||||
type: Number as PropType<Nullable<number>>,
|
||||
default: null,
|
||||
},
|
||||
onExceed: {
|
||||
type: Function,
|
||||
default: () => NOOP,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
|
||||
const uploadDisabled = computed(() => {
|
||||
return props.disabled || elForm.disabled
|
||||
})
|
||||
|
||||
const {
|
||||
abort,
|
||||
clearFiles,
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
handleRemove,
|
||||
submit,
|
||||
uploadRef,
|
||||
uploadFiles,
|
||||
} = useHandlers(props)
|
||||
|
||||
provide('uploader', getCurrentInstance())
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uploadFiles.value.forEach((file) => {
|
||||
if (file.url && file.url.indexOf('blob:') === 0) {
|
||||
URL.revokeObjectURL(file.url)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
abort,
|
||||
dragOver: ref(false),
|
||||
draging: ref(false),
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleRemove,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
uploadDisabled,
|
||||
uploadFiles,
|
||||
uploadRef,
|
||||
submit,
|
||||
clearFiles,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
let uploadList
|
||||
if (this.showFileList) {
|
||||
uploadList = h(
|
||||
UploadList,
|
||||
{
|
||||
disabled: this.uploadDisabled,
|
||||
listType: this.listType,
|
||||
files: this.uploadFiles,
|
||||
onRemove: this.handleRemove,
|
||||
handlePreview: this.onPreview,
|
||||
},
|
||||
this.$slots.file
|
||||
? {
|
||||
default: (props: { file: UploadFile }) => {
|
||||
return this.$slots.file({
|
||||
file: props.file,
|
||||
})
|
||||
},
|
||||
}
|
||||
: null
|
||||
)
|
||||
} else {
|
||||
uploadList = null
|
||||
}
|
||||
|
||||
const uploadData = {
|
||||
type: this.type,
|
||||
drag: this.drag,
|
||||
action: this.action,
|
||||
multiple: this.multiple,
|
||||
'before-upload': this.beforeUpload,
|
||||
'with-credentials': this.withCredentials,
|
||||
headers: this.headers,
|
||||
method: this.method,
|
||||
name: this.name,
|
||||
data: this.data,
|
||||
accept: this.accept,
|
||||
fileList: this.uploadFiles,
|
||||
autoUpload: this.autoUpload,
|
||||
listType: this.listType,
|
||||
disabled: this.uploadDisabled,
|
||||
limit: this.limit,
|
||||
'on-exceed': this.onExceed,
|
||||
'on-start': this.handleStart,
|
||||
'on-progress': this.handleProgress,
|
||||
'on-success': this.handleSuccess,
|
||||
'on-error': this.handleError,
|
||||
'on-preview': this.onPreview,
|
||||
'on-remove': this.handleRemove,
|
||||
'http-request': this.httpRequest,
|
||||
ref: 'uploadRef',
|
||||
}
|
||||
const trigger = this.$slots.trigger || this.$slots.default
|
||||
const uploadComponent = h(Upload, uploadData, {
|
||||
default: () => trigger?.(),
|
||||
})
|
||||
return h('div', [
|
||||
this.listType === 'picture-card' ? uploadList : null,
|
||||
this.$slots.trigger
|
||||
? [uploadComponent, this.$slots.default()]
|
||||
: uploadComponent,
|
||||
this.$slots.tip?.(),
|
||||
this.listType !== 'picture-card' ? uploadList : null,
|
||||
])
|
||||
},
|
||||
})
|
||||
</script>
|
57
packages/components/upload/src/upload-content.ts
Normal file
57
packages/components/upload/src/upload-content.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
import { uploadBaseProps } from './upload'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type {
|
||||
UploadRawFile,
|
||||
UploadFile,
|
||||
UploadProgressEvent,
|
||||
UploadHooks,
|
||||
} from './upload'
|
||||
import type UploadContent from './upload-content.vue'
|
||||
import type { UploadAjaxError } from './ajax'
|
||||
|
||||
export const uploadContentProps = buildProps({
|
||||
...uploadBaseProps,
|
||||
beforeUpload: {
|
||||
type: definePropType<UploadHooks['beforeUpload']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onRemove: {
|
||||
type: definePropType<
|
||||
(file: UploadFile | UploadRawFile, rawFile?: UploadRawFile) => void
|
||||
>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onStart: {
|
||||
type: definePropType<(rawFile: UploadRawFile) => void>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onSuccess: {
|
||||
type: definePropType<(response: any, rawFile: UploadRawFile) => unknown>(
|
||||
Function
|
||||
),
|
||||
default: NOOP,
|
||||
},
|
||||
onProgress: {
|
||||
type: definePropType<
|
||||
(evt: UploadProgressEvent, rawFile: UploadRawFile) => void
|
||||
>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onError: {
|
||||
type: definePropType<
|
||||
(err: UploadAjaxError, rawFile: UploadRawFile) => void
|
||||
>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onExceed: {
|
||||
type: definePropType<UploadHooks['onExceed']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type UploadContentProps = ExtractPropTypes<typeof uploadContentProps>
|
||||
|
||||
export type UploadContentInstance = InstanceType<typeof UploadContent>
|
186
packages/components/upload/src/upload-content.vue
Normal file
186
packages/components/upload/src/upload-content.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[ns.b(), ns.m(listType)]"
|
||||
tabindex="0"
|
||||
@click="handleClick"
|
||||
@keydown.self.enter.space="handleKeydown"
|
||||
>
|
||||
<template v-if="drag">
|
||||
<upload-dragger :disabled="disabled" @file="uploadFiles">
|
||||
<slot />
|
||||
</upload-dragger>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
<input
|
||||
ref="inputRef"
|
||||
:class="ns.e('input')"
|
||||
:name="name"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
type="file"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { entriesOf } from '@element-plus/utils'
|
||||
import UploadDragger from './upload-dragger.vue'
|
||||
import { uploadContentProps } from './upload-content'
|
||||
import { genFileId } from './upload'
|
||||
|
||||
import type {
|
||||
UploadRequestOptions,
|
||||
UploadRawFile,
|
||||
UploadFile,
|
||||
UploadHooks,
|
||||
} from './upload'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElUploadContent',
|
||||
})
|
||||
|
||||
const props = defineProps(uploadContentProps)
|
||||
const ns = useNamespace('upload')
|
||||
|
||||
const requests = shallowRef<Record<string, XMLHttpRequest | Promise<unknown>>>(
|
||||
{}
|
||||
)
|
||||
const inputRef = shallowRef<HTMLInputElement>()
|
||||
|
||||
const uploadFiles = (files: File[]) => {
|
||||
if (files.length === 0) return
|
||||
|
||||
const { autoUpload, limit, fileList, multiple, onStart, onExceed } = props
|
||||
|
||||
if (limit && fileList.length + files.length > limit) {
|
||||
onExceed(files, fileList)
|
||||
return
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
files = files.slice(0, 1)
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const rawFile = file as UploadRawFile
|
||||
rawFile.uid = genFileId()
|
||||
onStart(rawFile)
|
||||
if (autoUpload) upload(rawFile)
|
||||
}
|
||||
}
|
||||
|
||||
const upload = async (rawFile: UploadRawFile) => {
|
||||
inputRef.value!.value = ''
|
||||
|
||||
if (!props.beforeUpload) {
|
||||
return doUpload(rawFile)
|
||||
}
|
||||
|
||||
let hookResult: Exclude<ReturnType<UploadHooks['beforeUpload']>, Promise<any>>
|
||||
try {
|
||||
hookResult = await props.beforeUpload(rawFile)
|
||||
} catch {
|
||||
hookResult = false
|
||||
}
|
||||
|
||||
if (hookResult === false) {
|
||||
props.onRemove(rawFile)
|
||||
return
|
||||
}
|
||||
|
||||
let file: File = rawFile
|
||||
if (hookResult instanceof Blob) {
|
||||
if (hookResult instanceof File) {
|
||||
file = hookResult
|
||||
} else {
|
||||
file = new File([hookResult], rawFile.name, {
|
||||
type: rawFile.type,
|
||||
})
|
||||
}
|
||||
for (const key of Object.keys(rawFile)) {
|
||||
file[key] = rawFile[key]
|
||||
}
|
||||
}
|
||||
|
||||
doUpload(rawFile)
|
||||
}
|
||||
|
||||
const doUpload = (rawFile: UploadRawFile) => {
|
||||
const {
|
||||
headers,
|
||||
data,
|
||||
method,
|
||||
withCredentials,
|
||||
name: filename,
|
||||
action,
|
||||
onProgress,
|
||||
onSuccess,
|
||||
onError,
|
||||
httpRequest,
|
||||
} = props
|
||||
|
||||
const { uid } = rawFile
|
||||
const options: UploadRequestOptions = {
|
||||
headers: headers || {},
|
||||
withCredentials,
|
||||
file: rawFile,
|
||||
data,
|
||||
method,
|
||||
filename,
|
||||
action,
|
||||
onProgress: (evt) => {
|
||||
onProgress(evt, rawFile)
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
onSuccess(res, rawFile)
|
||||
delete requests.value[uid]
|
||||
},
|
||||
onError: (err) => {
|
||||
onError(err, rawFile)
|
||||
delete requests.value[uid]
|
||||
},
|
||||
}
|
||||
const request = httpRequest(options)
|
||||
requests.value[uid] = request
|
||||
if (request instanceof Promise) {
|
||||
request.then(options.onSuccess, options.onError)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
uploadFiles(Array.from(files))
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
inputRef.value!.value = ''
|
||||
inputRef.value!.click()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeydown = () => {
|
||||
handleClick()
|
||||
}
|
||||
|
||||
const abort = (file?: UploadFile) => {
|
||||
const _reqs = entriesOf(requests.value).filter(
|
||||
file ? ([uid]) => String(file.uid) === uid : () => true
|
||||
)
|
||||
_reqs.forEach(([uid, req]) => {
|
||||
if (req instanceof XMLHttpRequest) req.abort()
|
||||
delete requests.value[uid]
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
abort,
|
||||
upload,
|
||||
})
|
||||
</script>
|
19
packages/components/upload/src/upload-dragger.ts
Normal file
19
packages/components/upload/src/upload-dragger.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { buildProps, isArray } from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type UploadDragger from './upload-dragger.vue'
|
||||
|
||||
export const uploadDraggerProps = buildProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
} as const)
|
||||
export type UploadDraggerProps = ExtractPropTypes<typeof uploadDraggerProps>
|
||||
|
||||
export const uploadDraggerEmits = {
|
||||
file: (file: File[]) => isArray(file),
|
||||
}
|
||||
export type UploadDraggerEmits = typeof uploadDraggerEmits
|
||||
|
||||
export type UploadDraggerInstance = InstanceType<typeof UploadDragger>
|
@ -8,71 +8,71 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject } from 'vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
|
||||
import type { ElUpload } from './upload.type'
|
||||
import { uploadContextKey } from '@element-plus/tokens'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import { uploadDraggerEmits, uploadDraggerProps } from './upload-dragger'
|
||||
|
||||
export default defineComponent({
|
||||
const COMPONENT_NAME = 'ElUploadDrag'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElUploadDrag',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['file'],
|
||||
setup(props, { emit }) {
|
||||
const uploader = inject('uploader', {} as ElUpload)
|
||||
const ns = useNamespace('upload')
|
||||
const dragover = ref(false)
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
if (props.disabled || !uploader) return
|
||||
const accept = uploader.props?.accept || uploader.accept
|
||||
dragover.value = false
|
||||
if (!accept) {
|
||||
emit('file', e.dataTransfer.files)
|
||||
return
|
||||
}
|
||||
emit(
|
||||
'file',
|
||||
Array.from(e.dataTransfer.files).filter((file) => {
|
||||
const { type, name } = file
|
||||
const extension =
|
||||
name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
|
||||
const baseType = type.replace(/\/.*$/, '')
|
||||
return accept
|
||||
.split(',')
|
||||
.map((type) => type.trim())
|
||||
.filter((type) => type)
|
||||
.some((acceptedType) => {
|
||||
if (acceptedType.startsWith('.')) {
|
||||
return extension === acceptedType
|
||||
}
|
||||
if (/\/\*$/.test(acceptedType)) {
|
||||
return baseType === acceptedType.replace(/\/\*$/, '')
|
||||
}
|
||||
if (/^[^/]+\/[^/]+$/.test(acceptedType)) {
|
||||
return type === acceptedType
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function onDragover() {
|
||||
if (!props.disabled) dragover.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
ns,
|
||||
dragover,
|
||||
onDrop,
|
||||
onDragover,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps(uploadDraggerProps)
|
||||
const emit = defineEmits(uploadDraggerEmits)
|
||||
|
||||
const uploaderContext = inject(uploadContextKey)
|
||||
if (!uploaderContext) {
|
||||
throwError(
|
||||
COMPONENT_NAME,
|
||||
'usage: <el-upload><el-upload-dragger /></el-upload>'
|
||||
)
|
||||
}
|
||||
|
||||
const ns = useNamespace('upload')
|
||||
const dragover = ref(false)
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
if (props.disabled) return
|
||||
dragover.value = false
|
||||
|
||||
const files = Array.from(e.dataTransfer!.files)
|
||||
const accept = uploaderContext.accept.value
|
||||
if (!accept) {
|
||||
emit('file', files)
|
||||
return
|
||||
}
|
||||
|
||||
const filesFiltered = files.filter((file) => {
|
||||
const { type, name } = file
|
||||
const extension = name.includes('.') ? `.${name.split('.').pop()}` : ''
|
||||
const baseType = type.replace(/\/.*$/, '')
|
||||
return accept
|
||||
.split(',')
|
||||
.map((type) => type.trim())
|
||||
.filter((type) => type)
|
||||
.some((acceptedType) => {
|
||||
if (acceptedType.startsWith('.')) {
|
||||
return extension === acceptedType
|
||||
}
|
||||
if (/\/\*$/.test(acceptedType)) {
|
||||
return baseType === acceptedType.replace(/\/\*$/, '')
|
||||
}
|
||||
if (/^[^/]+\/[^/]+$/.test(acceptedType)) {
|
||||
return type === acceptedType
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
emit('file', filesFiltered)
|
||||
}
|
||||
|
||||
const onDragover = () => {
|
||||
if (!props.disabled) dragover.value = true
|
||||
}
|
||||
</script>
|
||||
|
33
packages/components/upload/src/upload-list.ts
Normal file
33
packages/components/upload/src/upload-list.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { buildProps, definePropType, mutable } from '@element-plus/utils'
|
||||
import { uploadListTypes } from './upload'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { UploadFile, UploadHooks, UploadFiles } from './upload'
|
||||
import type UploadList from './upload-list.vue'
|
||||
|
||||
export const uploadListProps = buildProps({
|
||||
files: {
|
||||
type: definePropType<UploadFiles>(Array),
|
||||
default: () => mutable([]),
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
handlePreview: {
|
||||
type: definePropType<UploadHooks['onPreview']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
listType: {
|
||||
type: String,
|
||||
values: uploadListTypes,
|
||||
default: 'text',
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type UploadListProps = ExtractPropTypes<typeof uploadListProps>
|
||||
export const uploadListEmits = {
|
||||
remove: (file: UploadFile) => !!file,
|
||||
}
|
||||
export type UploadListEmits = typeof uploadListEmits
|
||||
export type UploadListInstance = InstanceType<typeof UploadList>
|
@ -10,7 +10,7 @@
|
||||
>
|
||||
<li
|
||||
v-for="file in files"
|
||||
:key="file.uid || file"
|
||||
:key="file.uid || file.name"
|
||||
:class="[
|
||||
nsUpload.be('list', 'item'),
|
||||
nsUpload.is(file.status),
|
||||
@ -33,7 +33,7 @@
|
||||
alt=""
|
||||
/>
|
||||
<a :class="nsUpload.be('list', 'item-name')" @click="handleClick(file)">
|
||||
<el-icon :class="nsIcon.m('document')"><document /></el-icon>
|
||||
<el-icon :class="nsIcon.m('document')"><Document /></el-icon>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<label :class="nsUpload.be('list', 'item-status-label')">
|
||||
@ -47,7 +47,7 @@
|
||||
v-else-if="['picture-card', 'picture'].includes(listType)"
|
||||
:class="[nsIcon.m('upload-success'), nsIcon.m('check')]"
|
||||
>
|
||||
<check />
|
||||
<Check />
|
||||
</el-icon>
|
||||
</label>
|
||||
<el-icon
|
||||
@ -55,7 +55,7 @@
|
||||
:class="nsIcon.m('close')"
|
||||
@click="handleRemove(file)"
|
||||
>
|
||||
<close />
|
||||
<Close />
|
||||
</el-icon>
|
||||
<!-- Due to close btn only appears when li gets focused disappears after li gets blurred, thus keyboard navigation can never reach close btn-->
|
||||
<!-- This is a bug which needs to be fixed -->
|
||||
@ -67,7 +67,7 @@
|
||||
v-if="file.status === 'uploading'"
|
||||
:type="listType === 'picture-card' ? 'circle' : 'line'"
|
||||
:stroke-width="listType === 'picture-card' ? 6 : 2"
|
||||
:percentage="+file.percentage"
|
||||
:percentage="Number(file.percentage)"
|
||||
style="margin-top: 0.5rem"
|
||||
/>
|
||||
<span
|
||||
@ -85,16 +85,15 @@
|
||||
:class="nsUpload.be('list', 'item-delete')"
|
||||
@click="handleRemove(file)"
|
||||
>
|
||||
<el-icon :class="nsIcon.m('delete')"><delete /></el-icon>
|
||||
<el-icon :class="nsIcon.m('delete')"><Delete /></el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</slot>
|
||||
</li>
|
||||
</transition-group>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import {
|
||||
Document,
|
||||
@ -107,67 +106,32 @@ import {
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import ElProgress from '@element-plus/components/progress'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { UploadFile } from './upload.type'
|
||||
import { uploadListEmits, uploadListProps } from './upload-list'
|
||||
import type { UploadFile } from './upload'
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElUploadList',
|
||||
components: {
|
||||
ElProgress,
|
||||
ElIcon,
|
||||
Document,
|
||||
Delete,
|
||||
Close,
|
||||
ZoomIn,
|
||||
Check,
|
||||
CircleCheck,
|
||||
},
|
||||
props: {
|
||||
files: {
|
||||
type: Array as PropType<UploadFile[]>,
|
||||
default: () => [] as File[],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
handlePreview: {
|
||||
type: Function as PropType<(file: UploadFile) => void>,
|
||||
default: () => NOOP,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
},
|
||||
emits: ['remove'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useLocale()
|
||||
const nsUpload = useNamespace('upload')
|
||||
const nsIcon = useNamespace('icon')
|
||||
const nsList = useNamespace('list')
|
||||
|
||||
const handleClick = (file: UploadFile) => {
|
||||
props.handlePreview(file)
|
||||
}
|
||||
|
||||
const onFileClicked = (e: Event) => {
|
||||
;(e.target as HTMLElement).focus()
|
||||
}
|
||||
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
emit('remove', file)
|
||||
}
|
||||
return {
|
||||
focusing: ref(false),
|
||||
handleClick,
|
||||
handleRemove,
|
||||
onFileClicked,
|
||||
t,
|
||||
nsUpload,
|
||||
nsIcon,
|
||||
nsList,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps(uploadListProps)
|
||||
const emit = defineEmits(uploadListEmits)
|
||||
|
||||
const { t } = useLocale()
|
||||
const nsUpload = useNamespace('upload')
|
||||
const nsIcon = useNamespace('icon')
|
||||
const nsList = useNamespace('list')
|
||||
|
||||
const focusing = ref(false)
|
||||
|
||||
const handleClick = (file: UploadFile) => {
|
||||
props.handlePreview(file)
|
||||
}
|
||||
|
||||
const onFileClicked = (e: Event) => {
|
||||
;(e.target as HTMLElement).focus()
|
||||
}
|
||||
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
emit('remove', file)
|
||||
}
|
||||
</script>
|
||||
|
181
packages/components/upload/src/upload.ts
Normal file
181
packages/components/upload/src/upload.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { buildProps, definePropType, mutable } from '@element-plus/utils'
|
||||
import { ajaxUpload } from './ajax'
|
||||
|
||||
import type { UploadAjaxError } from './ajax'
|
||||
import type { Awaitable } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Upload from './upload.vue'
|
||||
|
||||
export const uploadListTypes = ['text', 'picture', 'picture-card'] as const
|
||||
|
||||
let fileId = 1
|
||||
export const genFileId = () => Date.now() + fileId++
|
||||
|
||||
export type UploadStatus = 'ready' | 'uploading' | 'success' | 'fail'
|
||||
export interface UploadProgressEvent extends ProgressEvent {
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface UploadRequestOptions {
|
||||
action: string
|
||||
method: string
|
||||
data: Record<string, string | Blob | [string | Blob, string]>
|
||||
filename: string
|
||||
file: File
|
||||
headers: Headers | Record<string, string | number | null | undefined>
|
||||
onError: (evt: UploadAjaxError) => void
|
||||
onProgress: (evt: UploadProgressEvent) => void
|
||||
onSuccess: (response: any) => void
|
||||
withCredentials: boolean
|
||||
}
|
||||
export interface UploadFile {
|
||||
name: string
|
||||
percentage?: number
|
||||
status: UploadStatus
|
||||
size: number
|
||||
response?: unknown
|
||||
uid: number
|
||||
url?: string
|
||||
raw: UploadRawFile
|
||||
}
|
||||
export type UploadFiles = UploadFile[]
|
||||
export interface UploadRawFile extends File {
|
||||
uid: number
|
||||
}
|
||||
export type UploadRequestHandler = (
|
||||
options: UploadRequestOptions
|
||||
) => XMLHttpRequest | Promise<unknown>
|
||||
export interface UploadHooks {
|
||||
beforeUpload: (
|
||||
rawFile: UploadRawFile
|
||||
) => Awaitable<void | undefined | null | boolean | File | Blob>
|
||||
beforeRemove: (
|
||||
uploadFile: UploadFile,
|
||||
uploadFiles: UploadFiles
|
||||
) => Awaitable<boolean>
|
||||
onRemove: (uploadFile: UploadFile, uploadFiles: UploadFiles) => void
|
||||
onChange: (uploadFile: UploadFile, uploadFiles: UploadFiles) => void
|
||||
onPreview: (uploadFile: UploadFile) => void
|
||||
onSuccess: (
|
||||
response: any,
|
||||
uploadFile: UploadFile,
|
||||
uploadFiles: UploadFiles
|
||||
) => void
|
||||
onProgress: (
|
||||
evt: UploadProgressEvent,
|
||||
uploadFile: UploadFile,
|
||||
uploadFiles: UploadFiles
|
||||
) => void
|
||||
onError: (
|
||||
error: Error,
|
||||
uploadFile: UploadFile,
|
||||
uploadFiles: UploadFiles
|
||||
) => void
|
||||
onExceed: (files: File[], uploadFiles: UploadFiles) => void
|
||||
}
|
||||
|
||||
export const uploadBaseProps = buildProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: definePropType<Headers | Record<string, any>>(Object),
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => mutable({} as const),
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withCredentials: Boolean,
|
||||
showFileList: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'select',
|
||||
},
|
||||
fileList: {
|
||||
type: definePropType<UploadFiles>(Array),
|
||||
default: () => mutable([] as const),
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listType: {
|
||||
type: String,
|
||||
values: uploadListTypes,
|
||||
default: 'text',
|
||||
},
|
||||
httpRequest: {
|
||||
type: definePropType<UploadRequestHandler>(Function),
|
||||
default: ajaxUpload,
|
||||
},
|
||||
disabled: Boolean,
|
||||
limit: Number,
|
||||
} as const)
|
||||
|
||||
export const uploadProps = buildProps({
|
||||
...uploadBaseProps,
|
||||
beforeUpload: {
|
||||
type: definePropType<UploadHooks['beforeUpload']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
beforeRemove: {
|
||||
type: definePropType<UploadHooks['beforeRemove']>(Function),
|
||||
},
|
||||
onRemove: {
|
||||
type: definePropType<UploadHooks['onRemove']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onChange: {
|
||||
type: definePropType<UploadHooks['onChange']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onPreview: {
|
||||
type: definePropType<UploadHooks['onPreview']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onSuccess: {
|
||||
type: definePropType<UploadHooks['onSuccess']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onProgress: {
|
||||
type: definePropType<UploadHooks['onProgress']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onError: {
|
||||
type: definePropType<UploadHooks['onError']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
onExceed: {
|
||||
type: definePropType<UploadHooks['onExceed']>(Function),
|
||||
default: NOOP,
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type UploadProps = ExtractPropTypes<typeof uploadProps>
|
||||
|
||||
export type UploadInstance = InstanceType<typeof Upload>
|
@ -1,88 +0,0 @@
|
||||
export type ListType = 'text' | 'picture' | 'picture-card'
|
||||
|
||||
export type UploadStatus = 'ready' | 'uploading' | 'success' | 'fail'
|
||||
|
||||
export type UploadFile = {
|
||||
name: string
|
||||
percentage?: number
|
||||
status: UploadStatus
|
||||
size: number
|
||||
response?: unknown
|
||||
uid: number
|
||||
url?: string
|
||||
raw: ElFile
|
||||
}
|
||||
|
||||
export interface ElFile extends File {
|
||||
uid: number
|
||||
}
|
||||
|
||||
export interface ElUploadProgressEvent extends ProgressEvent {
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface ElUploadAjaxError extends Error {
|
||||
status: number
|
||||
method: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ElUploadRequestOptions {
|
||||
action: string
|
||||
method: string
|
||||
data: Record<string, string | Blob>
|
||||
filename: string
|
||||
file: File
|
||||
headers: Headers
|
||||
onError: (e: Error) => void
|
||||
onProgress: (e: ProgressEvent) => void
|
||||
onSuccess: (response: XMLHttpRequestResponseType) => unknown
|
||||
withCredentials: boolean
|
||||
}
|
||||
|
||||
export type FileHandler<T = void> = (
|
||||
file: UploadFile,
|
||||
uploadFiles: UploadFile[]
|
||||
) => T
|
||||
export type FileResultHandler<T = any> = (
|
||||
param: T,
|
||||
file: UploadFile,
|
||||
uploadFiles: UploadFile[]
|
||||
) => void
|
||||
|
||||
export interface IUseHandlersProps {
|
||||
listType: ListType
|
||||
fileList: UploadFile[]
|
||||
beforeUpload?: FileHandler
|
||||
beforeRemove?: FileHandler<Promise<any> | boolean>
|
||||
onRemove?: FileHandler
|
||||
onChange?: FileHandler
|
||||
onPreview?: () => void
|
||||
onSuccess?: FileResultHandler
|
||||
onProgress?: FileResultHandler<ProgressEvent>
|
||||
onError?: FileResultHandler<Error>
|
||||
}
|
||||
|
||||
export interface ElUpload extends IUseHandlersProps {
|
||||
accept: string
|
||||
headers?: Headers
|
||||
data?: Record<string, unknown>
|
||||
multiple?: boolean
|
||||
name?: string
|
||||
drag?: boolean
|
||||
withCredentials?: boolean
|
||||
showFileList?: boolean
|
||||
type?: string
|
||||
dragOver: boolean
|
||||
genUid: () => number
|
||||
tempIndex: number
|
||||
handleError: () => void
|
||||
handleProgress: () => void
|
||||
handleRemove: () => void
|
||||
handleStart: () => void
|
||||
handleSuccess: () => void
|
||||
uploadDisabled: boolean
|
||||
uploadFiles: UploadFile[]
|
||||
submit: () => void
|
||||
clearFiles: () => void
|
||||
}
|
@ -1,292 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[ns.b(), ns.m(listType)]"
|
||||
tabindex="0"
|
||||
@click="handleClick"
|
||||
@keydown.self.enter.space="handleKeydown"
|
||||
>
|
||||
<template v-if="drag">
|
||||
<upload-dragger :disabled="disabled" @file="uploadFiles">
|
||||
<slot></slot>
|
||||
</upload-dragger>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<input
|
||||
ref="inputRef"
|
||||
:class="ns.e('input')"
|
||||
type="file"
|
||||
:name="name"
|
||||
<div>
|
||||
<upload-list
|
||||
v-if="isPictureCard && showFileList"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:files="uploadFiles"
|
||||
:handle-preview="onPreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<template v-if="$slots.file" #default="{ file }">
|
||||
<slot name="file" :file="file"></slot>
|
||||
</template>
|
||||
</upload-list>
|
||||
<upload-content
|
||||
ref="uploadRef"
|
||||
:type="type"
|
||||
:drag="drag"
|
||||
:action="action"
|
||||
:multiple="multiple"
|
||||
:with-credentials="withCredentials"
|
||||
:headers="headers"
|
||||
:method="method"
|
||||
:name="name"
|
||||
:data="data"
|
||||
:accept="accept"
|
||||
@change="handleChange"
|
||||
/>
|
||||
:file-list="uploadFiles"
|
||||
:auto-upload="autoUpload"
|
||||
:list-type="listType"
|
||||
:disabled="disabled"
|
||||
:limit="limit"
|
||||
:http-request="httpRequest"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-start="handleStart"
|
||||
:on-progress="handleProgress"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<template #default>
|
||||
<slot v-if="$slots.trigger" name="trigger" />
|
||||
<slot v-if="!$slots.trigger && $slots.default" />
|
||||
</template>
|
||||
</upload-content>
|
||||
<slot v-if="$slots.trigger" />
|
||||
<slot name="tip" />
|
||||
<upload-list
|
||||
v-if="!isPictureCard && showFileList"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:files="uploadFiles"
|
||||
:handle-preview="onPreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<template v-if="$slots.file" #default="{ file }">
|
||||
<slot name="file" :file="file" />
|
||||
</template>
|
||||
</upload-list>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, onBeforeUnmount, toRef, shallowRef } from 'vue'
|
||||
import { uploadContextKey } from '@element-plus/tokens'
|
||||
import { useDisabled } from '@element-plus/hooks'
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { hasOwn } from '@element-plus/utils'
|
||||
import UploadList from './upload-list.vue'
|
||||
import UploadContent from './upload-content.vue'
|
||||
import { useHandlers } from './use-handlers'
|
||||
import { uploadProps } from './upload'
|
||||
import type { UploadContentInstance } from './upload-content'
|
||||
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import ajax from './ajax'
|
||||
import UploadDragger from './upload-dragger.vue'
|
||||
defineOptions({
|
||||
name: 'ElUpload',
|
||||
})
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { Nullable } from '@element-plus/utils'
|
||||
import type { ListType, UploadFile, ElFile } from './upload.type'
|
||||
const props = defineProps(uploadProps)
|
||||
const disabled = useDisabled()
|
||||
|
||||
type IFileHanlder = (
|
||||
file: Nullable<ElFile[]>,
|
||||
fileList?: UploadFile[]
|
||||
) => unknown
|
||||
const uploadRef = shallowRef<UploadContentInstance>()
|
||||
const {
|
||||
abort,
|
||||
submit,
|
||||
clearFiles,
|
||||
uploadFiles,
|
||||
handleStart,
|
||||
handleError,
|
||||
handleRemove,
|
||||
handleSuccess,
|
||||
handleProgress,
|
||||
} = useHandlers(props, uploadRef)
|
||||
|
||||
type AjaxEventListener = (e: ProgressEvent, file: ElFile) => unknown
|
||||
const isPictureCard = computed(() => props.listType === 'picture-card')
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UploadDragger,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => null,
|
||||
},
|
||||
headers: {
|
||||
type: Object as PropType<Nullable<Partial<Headers>>>,
|
||||
default: () => null,
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
withCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean as PropType<Nullable<boolean>>,
|
||||
default: null,
|
||||
},
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
onStart: {
|
||||
type: Function as PropType<(file: File) => void>,
|
||||
default: NOOP as (file: File) => void,
|
||||
},
|
||||
onProgress: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
onError: {
|
||||
type: Function as PropType<AjaxEventListener>,
|
||||
default: NOOP as AjaxEventListener,
|
||||
},
|
||||
beforeUpload: {
|
||||
type: Function as PropType<
|
||||
(file: File) => Promise<File | Blob> | boolean | unknown
|
||||
>,
|
||||
default: NOOP as (file: File) => void,
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onPreview: {
|
||||
type: Function as PropType<IFileHanlder>,
|
||||
default: NOOP as IFileHanlder,
|
||||
},
|
||||
onRemove: {
|
||||
type: Function as PropType<
|
||||
(file: Nullable<FileList>, rawFile: ElFile) => void
|
||||
>,
|
||||
default: NOOP as (file: Nullable<FileList>, rawFile: ElFile) => void,
|
||||
},
|
||||
fileList: {
|
||||
type: Array as PropType<UploadFile[]>,
|
||||
default: () => [] as UploadFile[],
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<ListType>,
|
||||
default: 'text',
|
||||
},
|
||||
httpRequest: {
|
||||
type: Function as
|
||||
| PropType<typeof ajax>
|
||||
| PropType<(...args: unknown[]) => Promise<unknown>>,
|
||||
default: () => ajax,
|
||||
},
|
||||
disabled: Boolean,
|
||||
limit: {
|
||||
type: Number as PropType<Nullable<number>>,
|
||||
default: null,
|
||||
},
|
||||
onExceed: {
|
||||
type: Function as PropType<
|
||||
(files: FileList, fileList: UploadFile[]) => void
|
||||
>,
|
||||
default: NOOP,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const reqs = ref({} as Record<string, XMLHttpRequest | Promise<any>>)
|
||||
const ns = useNamespace('upload')
|
||||
const mouseover = ref(false)
|
||||
const inputRef = ref(null as Nullable<HTMLInputElement>)
|
||||
onBeforeUnmount(() => {
|
||||
uploadFiles.value.forEach(({ url }) => {
|
||||
if (url?.startsWith('blob:')) URL.revokeObjectURL(url)
|
||||
})
|
||||
})
|
||||
|
||||
function uploadFiles(files: FileList) {
|
||||
if (props.limit && props.fileList.length + files.length > props.limit) {
|
||||
props.onExceed(files, props.fileList)
|
||||
return
|
||||
}
|
||||
let postFiles = Array.from(files)
|
||||
if (!props.multiple) {
|
||||
postFiles = postFiles.slice(0, 1)
|
||||
}
|
||||
if (postFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
postFiles.forEach((rawFile) => {
|
||||
props.onStart(rawFile)
|
||||
if (props.autoUpload) upload(rawFile as ElFile)
|
||||
})
|
||||
}
|
||||
provide(uploadContextKey, {
|
||||
accept: toRef(props, 'accept'),
|
||||
})
|
||||
|
||||
function upload(rawFile: ElFile) {
|
||||
inputRef.value.value = null
|
||||
if (!props.beforeUpload) {
|
||||
return post(rawFile)
|
||||
}
|
||||
const before = props.beforeUpload(rawFile)
|
||||
if (before instanceof Promise) {
|
||||
before
|
||||
.then((processedFile) => {
|
||||
const fileType = Object.prototype.toString.call(processedFile)
|
||||
if (fileType === '[object File]' || fileType === '[object Blob]') {
|
||||
if (fileType === '[object Blob]') {
|
||||
processedFile = new File([processedFile], rawFile.name, {
|
||||
type: rawFile.type,
|
||||
})
|
||||
}
|
||||
for (const p in rawFile) {
|
||||
if (hasOwn(rawFile, p)) {
|
||||
processedFile[p] = rawFile[p]
|
||||
}
|
||||
}
|
||||
post(processedFile)
|
||||
} else {
|
||||
post(rawFile)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
props.onRemove(null, rawFile)
|
||||
})
|
||||
} else if (before !== false) {
|
||||
post(rawFile)
|
||||
} else {
|
||||
props.onRemove(null, rawFile)
|
||||
}
|
||||
}
|
||||
function abort(file) {
|
||||
const _reqs = reqs.value
|
||||
if (file) {
|
||||
let uid = file
|
||||
if (file.uid) uid = file.uid
|
||||
if (_reqs[uid]) {
|
||||
;(_reqs[uid] as XMLHttpRequest).abort()
|
||||
}
|
||||
} else {
|
||||
Object.keys(_reqs).forEach((uid) => {
|
||||
if (_reqs[uid]) (_reqs[uid] as XMLHttpRequest).abort()
|
||||
delete _reqs[uid]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function post(rawFile: ElFile) {
|
||||
const { uid } = rawFile
|
||||
const options = {
|
||||
headers: props.headers,
|
||||
withCredentials: props.withCredentials,
|
||||
file: rawFile,
|
||||
data: props.data,
|
||||
method: props.method,
|
||||
filename: props.name,
|
||||
action: props.action,
|
||||
onProgress: (e) => {
|
||||
props.onProgress(e, rawFile)
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
props.onSuccess(res, rawFile)
|
||||
delete reqs.value[uid]
|
||||
},
|
||||
onError: (err) => {
|
||||
props.onError(err, rawFile)
|
||||
delete reqs.value[uid]
|
||||
},
|
||||
}
|
||||
const req = props.httpRequest(options)
|
||||
reqs.value[uid] = req
|
||||
if (req instanceof Promise) {
|
||||
req.then(options.onSuccess, options.onError)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(e: DragEvent) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files) return
|
||||
uploadFiles(files)
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
inputRef.value.value = null
|
||||
inputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown() {
|
||||
handleClick()
|
||||
}
|
||||
|
||||
return {
|
||||
ns,
|
||||
reqs,
|
||||
mouseover,
|
||||
inputRef,
|
||||
abort,
|
||||
post,
|
||||
handleChange,
|
||||
handleClick,
|
||||
handleKeydown,
|
||||
upload,
|
||||
uploadFiles,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description cancel upload request */
|
||||
abort,
|
||||
/** @description upload the file list manually */
|
||||
submit,
|
||||
/** @description clear the file list */
|
||||
clearFiles,
|
||||
/** @description select the file manually */
|
||||
handleStart,
|
||||
/** @description remove the file manually */
|
||||
handleRemove,
|
||||
})
|
||||
</script>
|
||||
|
187
packages/components/upload/src/use-handlers.ts
Normal file
187
packages/components/upload/src/use-handlers.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { debugWarn, throwError } from '@element-plus/utils'
|
||||
import { useDeprecated } from '@element-plus/hooks'
|
||||
import { genFileId } from './upload'
|
||||
import type { ShallowRef } from 'vue'
|
||||
import type {
|
||||
UploadContentProps,
|
||||
UploadContentInstance,
|
||||
} from './upload-content'
|
||||
import type {
|
||||
UploadRawFile,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
UploadStatus,
|
||||
UploadFiles,
|
||||
} from './upload'
|
||||
|
||||
const SCOPE = 'ElUpload'
|
||||
|
||||
const revokeObjectURL = (file: UploadFile) => {
|
||||
if (file.url?.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(file.url)
|
||||
}
|
||||
}
|
||||
|
||||
export const useHandlers = (
|
||||
props: UploadProps,
|
||||
uploadRef: ShallowRef<UploadContentInstance | undefined>
|
||||
) => {
|
||||
const uploadFiles = ref<UploadFiles>([])
|
||||
|
||||
const getFile = (rawFile: UploadRawFile) =>
|
||||
uploadFiles.value.find((file) => file.uid === rawFile.uid)
|
||||
|
||||
function abort(file: UploadFile) {
|
||||
uploadRef.value?.abort(file)
|
||||
}
|
||||
|
||||
function clearFiles(
|
||||
/** @default ['ready', 'uploading', 'success', 'fail'] */
|
||||
states: UploadStatus[] = ['ready', 'uploading', 'success', 'fail']
|
||||
) {
|
||||
uploadFiles.value = uploadFiles.value.filter(
|
||||
(row) => !states.includes(row.status)
|
||||
)
|
||||
}
|
||||
|
||||
const handleError: UploadContentProps['onError'] = (err, rawFile) => {
|
||||
const file = getFile(rawFile)
|
||||
if (!file) return
|
||||
|
||||
file.status = 'fail'
|
||||
uploadFiles.value.splice(uploadFiles.value.indexOf(file), 1)
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
const handleProgress: UploadContentProps['onProgress'] = (evt, rawFile) => {
|
||||
const file = getFile(rawFile)
|
||||
if (!file) return
|
||||
|
||||
props.onProgress(evt, file, uploadFiles.value)
|
||||
file.status = 'uploading'
|
||||
file.percentage = evt.percent
|
||||
}
|
||||
|
||||
const handleSuccess: UploadContentProps['onSuccess'] = (
|
||||
response,
|
||||
rawFile
|
||||
) => {
|
||||
const file = getFile(rawFile)
|
||||
if (!file) return
|
||||
|
||||
file.status = 'success'
|
||||
file.response = response
|
||||
props.onSuccess(response, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
const handleStart: UploadContentProps['onStart'] = (file) => {
|
||||
const uploadFile: UploadFile = {
|
||||
name: file.name,
|
||||
percentage: 0,
|
||||
status: 'ready',
|
||||
size: file.size,
|
||||
raw: file,
|
||||
uid: file.uid,
|
||||
}
|
||||
if (props.listType === 'picture-card' || props.listType === 'picture') {
|
||||
try {
|
||||
uploadFile.url = URL.createObjectURL(file)
|
||||
} catch (err: unknown) {
|
||||
debugWarn(SCOPE, (err as Error).message)
|
||||
props.onError(err as Error, uploadFile, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
uploadFiles.value.push(uploadFile)
|
||||
props.onChange(uploadFile, uploadFiles.value)
|
||||
}
|
||||
|
||||
const handleRemove: UploadContentProps['onRemove'] = async (
|
||||
file,
|
||||
rawFile // TODO: deprecated in 2.2
|
||||
): Promise<void> => {
|
||||
if (rawFile) {
|
||||
useDeprecated(
|
||||
{
|
||||
scope: SCOPE,
|
||||
from: 'handleRemove second argument',
|
||||
version: '2.2',
|
||||
replacement: 'first argument `file`',
|
||||
ref: 'https://element-plus.org/en-US/component/upload.html#methods',
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const _file = rawFile || file
|
||||
const uploadFile = _file instanceof File ? getFile(_file) : _file
|
||||
if (!uploadFile) throwError(SCOPE, 'file to be removed not found')
|
||||
|
||||
const doRemove = (file: UploadFile) => {
|
||||
abort(file)
|
||||
const fileList = uploadFiles.value
|
||||
fileList.splice(fileList.indexOf(file), 1)
|
||||
props.onRemove(file, fileList)
|
||||
revokeObjectURL(file)
|
||||
}
|
||||
|
||||
if (props.beforeRemove) {
|
||||
const before = await props.beforeRemove(uploadFile, uploadFiles.value)
|
||||
if (before !== false) doRemove(uploadFile)
|
||||
} else {
|
||||
doRemove(uploadFile)
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
uploadFiles.value
|
||||
.filter(({ status }) => status === 'ready')
|
||||
.forEach(({ raw }) => uploadRef.value?.upload(raw))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.listType,
|
||||
(val) => {
|
||||
if (val !== 'picture-card' && val !== 'picture') {
|
||||
return
|
||||
}
|
||||
|
||||
uploadFiles.value = uploadFiles.value.map((file) => {
|
||||
const { raw, url } = file
|
||||
if (!url && raw) {
|
||||
try {
|
||||
file.url = URL.createObjectURL(raw)
|
||||
} catch (err: unknown) {
|
||||
props.onError(err as Error, file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fileList,
|
||||
(fileList) => {
|
||||
for (const file of fileList) {
|
||||
file.uid = genFileId()
|
||||
file.status ||= 'success'
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
abort,
|
||||
clearFiles,
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
handleRemove,
|
||||
submit,
|
||||
uploadFiles,
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
import { cloneDeep } from 'lodash-unified'
|
||||
|
||||
// Inline types
|
||||
import type {
|
||||
ListType,
|
||||
UploadFile,
|
||||
UploadStatus,
|
||||
ElFile,
|
||||
ElUploadProgressEvent,
|
||||
IUseHandlersProps,
|
||||
} from './upload.type'
|
||||
type UploadRef = {
|
||||
abort: (file: UploadFile) => void
|
||||
upload: (file: ElFile) => void
|
||||
}
|
||||
// helpers
|
||||
function getFile(rawFile: ElFile, uploadFiles: UploadFile[]) {
|
||||
return uploadFiles.find((file) => file.uid === rawFile.uid)
|
||||
}
|
||||
|
||||
function genUid(seed: number) {
|
||||
return Date.now() + seed
|
||||
}
|
||||
|
||||
export default (props: IUseHandlersProps) => {
|
||||
const uploadFiles = ref<UploadFile[]>([])
|
||||
const uploadRef = ref<UploadRef>(null)
|
||||
|
||||
let tempIndex = 1
|
||||
|
||||
function abort(file: UploadFile) {
|
||||
uploadRef.value.abort(file)
|
||||
}
|
||||
|
||||
function clearFiles(
|
||||
status: UploadStatus[] = ['ready', 'uploading', 'success', 'fail']
|
||||
) {
|
||||
uploadFiles.value = uploadFiles.value.filter((row) => {
|
||||
return !status.includes(row.status)
|
||||
})
|
||||
}
|
||||
|
||||
function handleError(err: Error, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
file.status = 'fail'
|
||||
uploadFiles.value.splice(uploadFiles.value.indexOf(file), 1)
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
function handleProgress(ev: ElUploadProgressEvent, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
props.onProgress(ev, file, uploadFiles.value)
|
||||
file.status = 'uploading'
|
||||
file.percentage = ev.percent || 0
|
||||
}
|
||||
|
||||
function handleSuccess(res: any, rawFile: ElFile) {
|
||||
const file = getFile(rawFile, uploadFiles.value)
|
||||
if (file) {
|
||||
file.status = 'success'
|
||||
file.response = res
|
||||
props.onSuccess(res, file, uploadFiles.value)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleStart(rawFile: ElFile) {
|
||||
const uid = genUid(tempIndex++)
|
||||
rawFile.uid = uid
|
||||
const file: UploadFile = {
|
||||
name: rawFile.name,
|
||||
percentage: 0,
|
||||
status: 'ready',
|
||||
size: rawFile.size,
|
||||
raw: rawFile,
|
||||
uid,
|
||||
}
|
||||
if (props.listType === 'picture-card' || props.listType === 'picture') {
|
||||
try {
|
||||
file.url = URL.createObjectURL(rawFile)
|
||||
} catch (err) {
|
||||
console.error('[Element Error][Upload]', err)
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
uploadFiles.value.push(file)
|
||||
props.onChange(file, uploadFiles.value)
|
||||
}
|
||||
|
||||
function handleRemove(file: UploadFile, raw: ElFile) {
|
||||
if (raw) {
|
||||
file = getFile(raw, uploadFiles.value)
|
||||
}
|
||||
const revokeObjectURL = () => {
|
||||
if (file.url && file.url.indexOf('blob:') === 0) {
|
||||
URL.revokeObjectURL(file.url)
|
||||
}
|
||||
}
|
||||
const doRemove = () => {
|
||||
abort(file)
|
||||
const fileList = uploadFiles.value
|
||||
fileList.splice(fileList.indexOf(file), 1)
|
||||
props.onRemove(file, fileList)
|
||||
revokeObjectURL()
|
||||
}
|
||||
if (!props.beforeRemove) {
|
||||
doRemove()
|
||||
} else if (typeof props.beforeRemove === 'function') {
|
||||
const before = props.beforeRemove(file, uploadFiles.value)
|
||||
if (before instanceof Promise) {
|
||||
before
|
||||
.then(() => {
|
||||
doRemove()
|
||||
})
|
||||
.catch(NOOP)
|
||||
} else if (before !== false) {
|
||||
doRemove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
uploadFiles.value
|
||||
.filter((file) => file.status === 'ready')
|
||||
.forEach((file) => {
|
||||
uploadRef.value.upload(file.raw)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.listType,
|
||||
(val: ListType) => {
|
||||
if (val === 'picture-card' || val === 'picture') {
|
||||
uploadFiles.value = uploadFiles.value.map((file) => {
|
||||
if (!file.url && file.raw) {
|
||||
try {
|
||||
file.url = URL.createObjectURL(file.raw)
|
||||
} catch (err) {
|
||||
props.onError(err, file, uploadFiles.value)
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fileList,
|
||||
(fileList: UploadFile[]) => {
|
||||
uploadFiles.value = fileList.map((file) => {
|
||||
const cloneFile = cloneDeep(file)
|
||||
return {
|
||||
...cloneFile,
|
||||
uid: file.uid || genUid(tempIndex++),
|
||||
status: file.status || 'success',
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
abort,
|
||||
clearFiles,
|
||||
handleError,
|
||||
handleProgress,
|
||||
handleStart,
|
||||
handleSuccess,
|
||||
handleRemove,
|
||||
submit,
|
||||
uploadFiles,
|
||||
uploadRef,
|
||||
}
|
||||
}
|
@ -8,3 +8,4 @@ export * from './pagination'
|
||||
export * from './radio'
|
||||
export * from './scrollbar'
|
||||
export * from './tabs'
|
||||
export * from './upload'
|
||||
|
8
packages/tokens/upload.ts
Normal file
8
packages/tokens/upload.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { ComputedRef, InjectionKey } from 'vue'
|
||||
|
||||
export interface UploadContext {
|
||||
accept: ComputedRef<string>
|
||||
}
|
||||
|
||||
export const uploadContextKey: InjectionKey<UploadContext> =
|
||||
Symbol('uploadContextKey')
|
@ -12,3 +12,4 @@ export type HTMLElementCustomized<T> = HTMLElement & T
|
||||
export type Nullable<T> = T | null
|
||||
|
||||
export type Arrayable<T> = T | T[]
|
||||
export type Awaitable<T> = Promise<T> | T
|
||||
|
Loading…
Reference in New Issue
Block a user