diff --git a/packages/components/upload/demo/MaxCount.vue b/packages/components/upload/demo/MaxCount.vue
index 81c9cdef5..930a9a8cf 100644
--- a/packages/components/upload/demo/MaxCount.vue
+++ b/packages/components/upload/demo/MaxCount.vue
@@ -4,6 +4,8 @@
v-model:files="files"
action="https://run.mocky.io/v3/7564bc4f-780e-43f7-bc58-467959ae3354"
:maxCount="maxCount"
+ @select="onSelect"
+ @maxCountExceeded="onMaxCountExceeded"
>
Upload
@@ -13,8 +15,17 @@
diff --git a/packages/components/upload/index.ts b/packages/components/upload/index.ts
index 8aa9deef5..5d62324ae 100644
--- a/packages/components/upload/index.ts
+++ b/packages/components/upload/index.ts
@@ -31,4 +31,5 @@ export type {
UploadFilesInstance,
UploadFilesComponent,
UploadFilesPublicProps as UploadFilesProps,
+ FilteredFile,
} from './src/types'
diff --git a/packages/components/upload/src/Upload.tsx b/packages/components/upload/src/Upload.tsx
index b9c9ecb93..71d3662d8 100644
--- a/packages/components/upload/src/Upload.tsx
+++ b/packages/components/upload/src/Upload.tsx
@@ -7,13 +7,16 @@
import { type Ref, defineComponent, provide, ref, shallowRef } from 'vue'
-import { useControlledProp } from '@idux/cdk/utils'
import { useGlobalConfig } from '@idux/components/config'
import { IxImageViewer } from '@idux/components/image'
import FileSelector from './component/Selector'
import { useCmpClasses } from './composables/useDisplay'
+import { useDrag } from './composables/useDrag'
+import { useFileSelect } from './composables/useFileSelect'
+import { useFilesData } from './composables/useFilesData'
import { useRequest } from './composables/useRequest'
+import { useUploadControl } from './composables/useUploadControl'
import { uploadToken } from './token'
import { uploadProps } from './types'
@@ -24,20 +27,25 @@ export default defineComponent({
const locale = useGlobalConfig('locale')
const cpmClasses = useCmpClasses()
const [showSelector, setSelectorVisible] = useShowSelector()
- const [files, onUpdateFiles] = useControlledProp(props, 'files', [])
- const { fileUploading, abort, startUpload, upload } = useRequest(props, files)
+ const filesDataContext = useFilesData(props)
+ const selectFiles = useFileSelect(props, filesDataContext)
+ const dragContext = useDrag(props, selectFiles)
+
+ const uploadRequest = useRequest(props, filesDataContext)
+ const { abort, upload } = uploadRequest
const { viewerVisible, images, setViewerVisible } = useImageViewer()
+
+ useUploadControl(filesDataContext.fileList, uploadRequest)
+
provide(uploadToken, {
props,
locale,
- files,
- fileUploading,
- onUpdateFiles,
- abort,
- startUpload,
- upload,
+ selectFiles,
setViewerVisible,
setSelectorVisible,
+ ...uploadRequest,
+ ...filesDataContext,
+ ...dragContext,
})
return () => (
diff --git a/packages/components/upload/src/component/ImageCardList.tsx b/packages/components/upload/src/component/ImageCardList.tsx
index 4e20703e3..e924a8708 100644
--- a/packages/components/upload/src/component/ImageCardList.tsx
+++ b/packages/components/upload/src/component/ImageCardList.tsx
@@ -24,26 +24,22 @@ import {
import { type FileOperation, useOperation } from '../composables/useOperation'
import { uploadToken } from '../token'
import { UploadFile, type UploadFileStatus, type UploadProps, uploadFilesProps } from '../types'
-import { type IconsMap, renderOprIcon } from '../util/icon'
-import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../util/visible'
+import { type IconsMap, renderOprIcon } from '../utils/icon'
+import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../utils/visible'
export default defineComponent({
name: 'IxUploadImageCardList',
props: uploadFilesProps,
setup(listProps) {
- const { props: uploadProps, locale, files, upload, abort, onUpdateFiles, setViewerVisible } = inject(uploadToken)!
+ const uploadContext = inject(uploadToken)!
+ const { props: uploadProps, locale, fileList } = uploadContext
const icons = useIcon(listProps)
const cpmClasses = useCmpClasses()
const listClasses = useListClasses(uploadProps, 'imageCard')
const [, imageCardVisible] = useSelectorVisible(uploadProps, 'imageCard')
- const showSelector = useShowSelector(uploadProps, files, imageCardVisible)
+ const showSelector = useShowSelector(uploadProps, fileList, imageCardVisible)
const { getThumbNode, revokeAll } = useThumb()
- const fileOperation = useOperation(files, listProps, uploadProps, {
- abort,
- upload,
- onUpdateFiles,
- setViewerVisible,
- })
+ const fileOperation = useOperation(listProps, uploadContext)
const selectorNode = renderSelector(cpmClasses)
onBeforeUnmount(revokeAll)
@@ -51,7 +47,9 @@ export default defineComponent({
return () => (
{showSelector.value && selectorNode}
- {files.value.map(file => renderItem(uploadProps, file, icons, cpmClasses, fileOperation, locale, getThumbNode))}
+ {fileList.value.map(file =>
+ renderItem(uploadProps, file, icons, cpmClasses, fileOperation, locale, getThumbNode),
+ )}
)
},
diff --git a/packages/components/upload/src/component/ImageList.tsx b/packages/components/upload/src/component/ImageList.tsx
index edf1074b9..c5e6f39b6 100644
--- a/packages/components/upload/src/component/ImageList.tsx
+++ b/packages/components/upload/src/component/ImageList.tsx
@@ -8,7 +8,7 @@
import type { UseThumb } from '../composables/useDisplay'
import type { FileOperation } from '../composables/useOperation'
import type { UploadFile, UploadProps } from '../types'
-import type { IconsMap } from '../util/icon'
+import type { IconsMap } from '../utils/icon'
import type { Locale } from '@idux/components/locales'
import type { ComputedRef } from 'vue'
@@ -21,31 +21,27 @@ import { useCmpClasses, useIcon, useListClasses, useThumb } from '../composables
import { useOperation } from '../composables/useOperation'
import { uploadToken } from '../token'
import { uploadFilesProps } from '../types'
-import { renderIcon, renderOprIcon } from '../util/icon'
-import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../util/visible'
+import { renderIcon, renderOprIcon } from '../utils/icon'
+import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../utils/visible'
export default defineComponent({
name: 'IxUploadImageList',
props: uploadFilesProps,
setup(listProps) {
- const { props: uploadProps, locale, files, upload, abort, onUpdateFiles, setViewerVisible } = inject(uploadToken)!
+ const uploadContext = inject(uploadToken)!
+ const { props: uploadProps, locale, fileList } = uploadContext
const icons = useIcon(listProps)
const cpmClasses = useCmpClasses()
const listClasses = useListClasses(uploadProps, 'image')
const { getThumbNode, revokeAll } = useThumb()
- const fileOperation = useOperation(files, listProps, uploadProps, {
- abort,
- upload,
- onUpdateFiles,
- setViewerVisible,
- })
+ const fileOperation = useOperation(listProps, uploadContext)
onBeforeUnmount(revokeAll)
return () =>
- files.value.length > 0 && (
+ fileList.value.length > 0 && (
- {files.value.map(file =>
+ {fileList.value.map(file =>
renderItem(uploadProps, file, icons, cpmClasses, fileOperation, locale, getThumbNode),
)}
diff --git a/packages/components/upload/src/component/Selector.tsx b/packages/components/upload/src/component/Selector.tsx
index ccbc16a7c..f16f8eaaa 100644
--- a/packages/components/upload/src/component/Selector.tsx
+++ b/packages/components/upload/src/component/Selector.tsx
@@ -5,54 +5,48 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { UploadDrag } from '../composables/useDrag'
-import type { UploadToken } from '../token'
-import type { UploadFile, UploadFileStatus, UploadProps } from '../types'
+import type { UploadProps } from '../types'
import type { UploadConfig } from '@idux/components/config'
-import type { ComputedRef, Ref, ShallowRef } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
-import { computed, defineComponent, inject, nextTick, normalizeClass, ref, shallowRef, watch } from 'vue'
+import { computed, defineComponent, inject, normalizeClass, ref } from 'vue'
-import { callEmit } from '@idux/cdk/utils'
import { useGlobalConfig } from '@idux/components/config'
import { useCmpClasses } from '../composables/useDisplay'
-import { useDrag } from '../composables/useDrag'
import { uploadToken } from '../token'
-import { getFileInfo, getFilesAcceptAllow, getFilesCountAllow } from '../util/fileHandle'
export default defineComponent({
name: 'IxUploadSelector',
- setup(props, { slots }) {
- const { props: uploadProps, files, onUpdateFiles, abort, startUpload } = inject(uploadToken)!
+ setup(_, { slots }) {
+ const { props: uploadProps, selectFiles, onDrop, onDragOver, onDragLeave, isDraggingOver } = inject(uploadToken)!
const cpmClasses = useCmpClasses()
const config = useGlobalConfig('upload')
const dir = useDir(uploadProps, config)
const multiple = useMultiple(uploadProps, config)
const dragable = useDragable(uploadProps, config)
- const accept = useAccept(uploadProps)
- const maxCount = computed(() => uploadProps.maxCount ?? 0)
- const {
- allowDrag,
- dragOver,
- filesSelected: dragFilesSelected,
- onDrop,
- onDragOver,
- onDragLeave,
- } = useDrag(uploadProps)
- const [filesSelected, updateFilesSelected] = useFilesSelected(dragFilesSelected, allowDrag)
- const filesReady = useFilesAllowed(files, filesSelected, accept, maxCount)
+
const fileInputRef: Ref = ref(null)
const inputClasses = computed(() => `${cpmClasses.value}-input`)
- const selectorClasses = useSelectorClasses(uploadProps, cpmClasses, dragable, dragOver)
-
- syncUploadHandle(uploadProps, files, filesReady, onUpdateFiles, abort, startUpload)
+ const selectorClasses = useSelectorClasses(uploadProps, cpmClasses, dragable, isDraggingOver)
+
+ const onClick = () => {
+ if (uploadProps.disabled || !fileInputRef.value) {
+ return
+ }
+ fileInputRef.value.value = ''
+ fileInputRef.value.click()
+ }
+ const onFileChange = () => {
+ const files = Array.prototype.slice.call(fileInputRef.value?.files ?? []) as File[]
+ selectFiles(files)
+ }
return () => {
return (
onClick(fileInputRef, uploadProps)}
+ onClick={onClick}
onDragover={onDragOver}
onDrop={onDrop}
onDragleave={onDragLeave}
@@ -65,7 +59,7 @@ export default defineComponent({
accept={uploadProps.accept}
multiple={multiple.value}
onClick={e => e.stopPropagation()}
- onChange={() => onSelect(fileInputRef, updateFilesSelected)}
+ onChange={onFileChange}
/>
{slots.default?.()}
@@ -95,16 +89,6 @@ function useDir(props: UploadProps, config: UploadConfig) {
return computed(() => (props.directory ?? config.directory ? directoryCfg : {}))
}
-function useAccept(props: UploadProps) {
- return computed(
- () =>
- props.accept
- ?.split(',')
- .map(type => type.trim())
- .filter(type => type),
- )
-}
-
function useMultiple(props: UploadProps, config: UploadConfig) {
return computed(() => props.multiple ?? config.multiple)
}
@@ -112,104 +96,3 @@ function useMultiple(props: UploadProps, config: UploadConfig) {
function useDragable(props: UploadProps, config: UploadConfig) {
return computed(() => props.dragable ?? config.dragable)
}
-
-function useFilesSelected(
- dragFilesSelected: UploadDrag['filesSelected'],
- allowDrag: UploadDrag['allowDrag'],
-): [ShallowRef, (files: File[]) => void] {
- const filesSelected: ShallowRef = shallowRef([])
-
- watch(dragFilesSelected, files => {
- if (allowDrag.value) {
- filesSelected.value = files
- }
- })
-
- function updateFilesSelected(files: File[]) {
- filesSelected.value = files
- }
-
- return [filesSelected, updateFilesSelected]
-}
-
-function useFilesAllowed(
- files: ComputedRef,
- filesSelected: ShallowRef,
- accept: ComputedRef,
- maxCount: ComputedRef,
-) {
- const filesAllowed: ShallowRef = shallowRef([])
-
- watch(filesSelected, filesSelected$$ => {
- const filesCheckAccept = getFilesAcceptAllow(filesSelected$$, accept.value)
- filesAllowed.value = getFilesCountAllow(filesCheckAccept, files.value.length, maxCount.value)
- })
-
- return filesAllowed
-}
-
-// 选中文件变化就处理上传
-function syncUploadHandle(
- uploadProps: UploadProps,
- files: ComputedRef,
- filesReady: ShallowRef,
- onUpdateFiles: UploadToken['onUpdateFiles'],
- abort: UploadToken['abort'],
- startUpload: UploadToken['startUpload'],
-) {
- watch(filesReady, async filesReady$$ => {
- if (filesReady$$.length === 0) {
- return
- }
- const filesAfterHandle = uploadProps.onSelect ? await callEmit(uploadProps.onSelect, filesReady$$) : filesReady$$
- const filesReadyUpload = getFilesHandled(filesAfterHandle!, filesReady$$)
- const filesFormat = getFormatFiles(filesReadyUpload, uploadProps, 'selected')
- const filesIds = filesFormat.map(file => file.key)
- if (uploadProps.maxCount === 1) {
- files.value.forEach(file => abort(file))
- callEmit(onUpdateFiles, filesFormat)
- } else {
- callEmit(onUpdateFiles, files.value.concat(filesFormat))
- }
-
- await nextTick(() => {
- files.value
- .filter(item => filesIds.includes(item.key))
- .forEach(file => {
- startUpload(file)
- })
- })
- })
-}
-
-function onClick(fileInputRef: Ref, props: UploadProps) {
- if (props.disabled || !fileInputRef.value) {
- return
- }
- fileInputRef.value.value = ''
- fileInputRef.value.click()
-}
-
-function onSelect(fileInputRef: Ref, updateFilesSelected: (files: File[]) => void) {
- const files = Array.prototype.slice.call(fileInputRef.value?.files ?? []) as File[]
- updateFilesSelected(files)
-}
-
-// 文件对象初始化
-function getFormatFiles(files: File[], props: UploadProps, status: UploadFileStatus) {
- return files.map(item => {
- const fileInfo = getFileInfo(item, { status })
- callEmit(props.onFileStatusChange, fileInfo)
- return fileInfo
- })
-}
-
-function getFilesHandled(handleResult: boolean | File[], allowedFiles: File[]) {
- if (handleResult === true) {
- return allowedFiles
- }
- if (handleResult === false) {
- return []
- }
- return handleResult
-}
diff --git a/packages/components/upload/src/component/TextList.tsx b/packages/components/upload/src/component/TextList.tsx
index e6d073220..dbf0c89fa 100644
--- a/packages/components/upload/src/component/TextList.tsx
+++ b/packages/components/upload/src/component/TextList.tsx
@@ -7,7 +7,7 @@
import type { FileOperation } from '../composables/useOperation'
import type { UploadFile, UploadProps } from '../types'
-import type { IconsMap } from '../util/icon'
+import type { IconsMap } from '../utils/icon'
import type { Locale } from '@idux/components/locales'
import type { ComputedRef } from 'vue'
@@ -20,28 +20,24 @@ import { useCmpClasses, useIcon, useListClasses } from '../composables/useDispla
import { useOperation } from '../composables/useOperation'
import { uploadToken } from '../token'
import { uploadFilesProps } from '../types'
-import { renderIcon, renderOprIcon } from '../util/icon'
-import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../util/visible'
+import { renderIcon, renderOprIcon } from '../utils/icon'
+import { showDownload, showErrorTip, showPreview, showProgress, showRetry } from '../utils/visible'
export default defineComponent({
name: 'IxUploadTextList',
props: uploadFilesProps,
setup(listProps) {
- const { props: uploadProps, locale, files, upload, abort, onUpdateFiles, setViewerVisible } = inject(uploadToken)!
+ const uploadContext = inject(uploadToken)!
+ const { props: uploadProps, locale, fileList } = uploadContext
const icons = useIcon(listProps)
const cpmClasses = useCmpClasses()
const listClasses = useListClasses(uploadProps, 'text')
- const fileOperation = useOperation(files, listProps, uploadProps, {
- abort,
- upload,
- onUpdateFiles,
- setViewerVisible,
- })
+ const fileOperation = useOperation(listProps, uploadContext)
return () =>
- files.value.length > 0 && (
+ fileList.value.length > 0 && (
- {files.value.map(file => renderItem(uploadProps, file, icons, cpmClasses, fileOperation, locale))}
+ {fileList.value.map(file => renderItem(uploadProps, file, icons, cpmClasses, fileOperation, locale))}
)
},
diff --git a/packages/components/upload/src/composables/useDisplay.ts b/packages/components/upload/src/composables/useDisplay.ts
index 1649c2a47..16f5827a5 100644
--- a/packages/components/upload/src/composables/useDisplay.ts
+++ b/packages/components/upload/src/composables/useDisplay.ts
@@ -6,14 +6,14 @@
*/
import type { UploadFile, UploadFilesProps, UploadFilesType, UploadProps } from '../types'
-import type { IconsMap } from '../util/icon'
+import type { IconsMap } from '../utils/icon'
import type { ComputedRef, ShallowRef, VNode } from 'vue'
import { computed, h, isProxy, normalizeClass, shallowRef } from 'vue'
import { useGlobalConfig } from '@idux/components/config'
-import { isImage } from '../util/fileHandle'
+import { isImage } from '../utils/files'
export function useCmpClasses(): ComputedRef {
const commonPrefix = useGlobalConfig('common')
diff --git a/packages/components/upload/src/composables/useDrag.ts b/packages/components/upload/src/composables/useDrag.ts
index be03caac9..fb6056f91 100644
--- a/packages/components/upload/src/composables/useDrag.ts
+++ b/packages/components/upload/src/composables/useDrag.ts
@@ -6,22 +6,20 @@
*/
import type { UploadProps } from '../types'
-import type { ComputedRef, Ref, ShallowRef } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
-import { computed, ref, shallowRef } from 'vue'
+import { computed, ref } from 'vue'
export interface UploadDrag {
allowDrag: ComputedRef
- dragOver: Ref
- filesSelected: ShallowRef
+ isDraggingOver: Ref
onDrop: (e: DragEvent) => void
onDragOver: (e: DragEvent) => void
onDragLeave: (e: DragEvent) => void
}
-export function useDrag(props: UploadProps): UploadDrag {
- const dragOver = ref(false)
- const filesSelected: ShallowRef = shallowRef([])
+export function useDrag(props: UploadProps, selectFiles: (files: File[]) => Promise): UploadDrag {
+ const isDraggingOver = ref(false)
const allowDrag = computed(() => !!props.dragable && !props.disabled)
function onDrop(e: DragEvent) {
@@ -29,8 +27,8 @@ export function useDrag(props: UploadProps): UploadDrag {
if (!allowDrag.value) {
return
}
- dragOver.value = false
- filesSelected.value = Array.prototype.slice.call(e.dataTransfer?.files ?? []) as File[]
+ isDraggingOver.value = false
+ selectFiles(Array.prototype.slice.call(e.dataTransfer?.files ?? []) as File[])
}
function onDragOver(e: DragEvent) {
@@ -38,7 +36,7 @@ export function useDrag(props: UploadProps): UploadDrag {
if (!allowDrag.value) {
return
}
- dragOver.value = true
+ isDraggingOver.value = true
}
function onDragLeave(e: DragEvent) {
@@ -46,13 +44,12 @@ export function useDrag(props: UploadProps): UploadDrag {
if (!allowDrag.value) {
return
}
- dragOver.value = false
+ isDraggingOver.value = false
}
return {
allowDrag,
- dragOver,
- filesSelected,
+ isDraggingOver,
onDrop,
onDragOver,
onDragLeave,
diff --git a/packages/components/upload/src/composables/useFileSelect.ts b/packages/components/upload/src/composables/useFileSelect.ts
new file mode 100644
index 000000000..79f3bd945
--- /dev/null
+++ b/packages/components/upload/src/composables/useFileSelect.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import type { FilesDataContext } from './useFilesData'
+import type { FilteredFile, UploadProps } from '../types'
+
+import { computed } from 'vue'
+
+import { isArray, isNil } from 'lodash-es'
+
+import { callEmit } from '@idux/cdk/utils'
+
+import { createUploadFile, filterFilesByAccept, filterFilesByMaxCount } from '../utils/files'
+
+export function useFileSelect(
+ props: UploadProps,
+ filesDataContext: FilesDataContext,
+): (files: File[]) => Promise {
+ const { fileList, updateFiles } = filesDataContext
+ const accept = computed(
+ () =>
+ props.accept
+ ?.split(',')
+ .map(type => type.trim())
+ .filter(Boolean),
+ )
+ const maxCount = computed(() => props.maxCount ?? 0)
+
+ const filterSelectFiles = (files: File[]): [File[], FilteredFile[]] => {
+ const [acceptedFiles, acceptFilteredFiles] = filterFilesByAccept(files, accept.value)
+ const [countAllowdFiles, countFilteredFiles] = filterFilesByMaxCount(
+ acceptedFiles,
+ fileList.value.length,
+ maxCount.value,
+ )
+
+ const filteredFiles: FilteredFile[] = [
+ ...acceptFilteredFiles.map(file => ({ file, reason: 'acceptNotMatch' as const })),
+ ...countFilteredFiles.map(file => ({ file, reason: 'maxCountExceeded' as const })),
+ ]
+
+ return [countAllowdFiles, filteredFiles]
+ }
+
+ const selecFiles = async (files: File[]) => {
+ const [allowdFiles, filteredFiles] = filterSelectFiles(files)
+
+ const onSelectResult = props.onSelect && (await callEmit(props.onSelect, allowdFiles, filteredFiles))
+
+ /* eslint-disable indent */
+ const resolvedAllowedFiles =
+ isNil(onSelectResult) || onSelectResult === true
+ ? allowdFiles
+ : onSelectResult === false
+ ? []
+ : isArray(onSelectResult)
+ ? onSelectResult
+ : []
+ /* eslint-enable indent */
+
+ const uploadFiles = resolvedAllowedFiles.map(file => createUploadFile(file, { status: 'selected' }))
+
+ if (
+ filteredFiles.filter(file => file.reason === 'maxCountExceeded').length > 0 ||
+ (props.maxCount === 1 && fileList.value.length > 0)
+ ) {
+ callEmit(props.onMaxCountExceeded)
+ }
+
+ if (props.maxCount === 1) {
+ updateFiles(uploadFiles, true)
+ } else {
+ updateFiles(uploadFiles)
+ }
+ }
+
+ return selecFiles
+}
diff --git a/packages/components/upload/src/composables/useFilesData.ts b/packages/components/upload/src/composables/useFilesData.ts
new file mode 100644
index 000000000..70bcecbac
--- /dev/null
+++ b/packages/components/upload/src/composables/useFilesData.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import type { UploadFile, UploadProps } from '../types'
+import type { ComputedRef } from 'vue'
+
+import { callEmit, useControlledProp } from '@idux/cdk/utils'
+
+import { getTargetFileIndex } from '../utils/files'
+
+export interface FilesDataContext {
+ fileList: ComputedRef
+ updateFiles: (files: UploadFile[], replace?: boolean) => void
+}
+
+export function useFilesData(props: UploadProps): FilesDataContext {
+ const [fileList, setFileList] = useControlledProp(props, 'files')
+
+ const updateFiles = (files: UploadFile[], replace = false) => {
+ const newFileList = replace ? files : [...fileList.value]
+ const statusChangeFiles: UploadFile[] = []
+
+ files.forEach(file => {
+ const index = getTargetFileIndex(file, fileList.value)
+ const oldFile = fileList.value[index]
+
+ if (index > -1 && !oldFile) {
+ return
+ }
+
+ if (oldFile?.status !== file.status) {
+ statusChangeFiles.push(file)
+ }
+
+ if (replace) {
+ return
+ }
+
+ if (index === -1) {
+ newFileList.push(file)
+ } else {
+ newFileList.splice(index, 1, file)
+ }
+ })
+
+ setFileList(newFileList)
+
+ statusChangeFiles.forEach(file => {
+ callEmit(props.onFileStatusChange, file)
+ })
+ }
+
+ return {
+ fileList,
+ updateFiles,
+ }
+}
diff --git a/packages/components/upload/src/composables/useOperation.ts b/packages/components/upload/src/composables/useOperation.ts
index 37eeaa9f6..77aa91ae4 100644
--- a/packages/components/upload/src/composables/useOperation.ts
+++ b/packages/components/upload/src/composables/useOperation.ts
@@ -5,13 +5,12 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { UploadToken } from '../token'
-import type { UploadFile, UploadFilesProps, UploadProps } from '../types'
-import type { ComputedRef } from 'vue'
+import type { UploadContext } from '../token'
+import type { UploadFile, UploadFilesProps } from '../types'
import { callEmit } from '@idux/cdk/utils'
-import { getTargetFile, getTargetFileIndex } from '../util/fileHandle'
+import { getTargetFile, getTargetFileIndex } from '../utils/files'
export interface FileOperation {
abort: (file: UploadFile) => void
@@ -21,21 +20,17 @@ export interface FileOperation {
remove: (file: UploadFile) => void
}
-export function useOperation(
- files: ComputedRef,
- listProps: UploadFilesProps,
- uploadProps: UploadProps,
- opr: Pick,
-): FileOperation {
+export function useOperation(listProps: UploadFilesProps, context: UploadContext): FileOperation {
+ const { fileList, props: uploadProps, abort: _abort, upload, setViewerVisible, updateFiles } = context
const abort = (file: UploadFile) => {
- opr.abort(file)
+ _abort(file)
}
const retry = (file: UploadFile) => {
if (uploadProps.disabled) {
return
}
- opr.upload(file)
+ upload(file)
callEmit(listProps.onRetry, file)
}
@@ -51,7 +46,7 @@ export function useOperation(
return
}
if (!listProps.onPreview && file.thumbUrl) {
- opr.setViewerVisible(true, file.thumbUrl)
+ setViewerVisible(true, file.thumbUrl)
return
}
callEmit(listProps.onPreview, file)
@@ -61,7 +56,7 @@ export function useOperation(
if (uploadProps.disabled) {
return
}
- const curFile = getTargetFile(file, files.value)
+ const curFile = getTargetFile(file, fileList.value)
if (!curFile) {
return
}
@@ -78,9 +73,9 @@ export function useOperation(
if (curFile.status === 'uploading') {
abort(curFile)
}
- const preFiles = [...files.value]
- preFiles.splice(getTargetFileIndex(curFile, files.value), 1)
- opr.onUpdateFiles(preFiles)
+ const preFiles = [...fileList.value]
+ preFiles.splice(getTargetFileIndex(curFile, fileList.value), 1)
+ updateFiles(preFiles, true)
}
return {
diff --git a/packages/components/upload/src/composables/useRequest.ts b/packages/components/upload/src/composables/useRequest.ts
index fce49d5fe..b5c8a3244 100644
--- a/packages/components/upload/src/composables/useRequest.ts
+++ b/packages/components/upload/src/composables/useRequest.ts
@@ -5,9 +5,17 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { UploadFile, UploadProgressEvent, UploadProps, UploadRequestError, UploadRequestOption } from '../types'
+import type { FilesDataContext } from '../composables/useFilesData'
+import type {
+ UploadFile,
+ UploadFileStatus,
+ UploadProgressEvent,
+ UploadProps,
+ UploadRequestError,
+ UploadRequestOption,
+} from '../types'
import type { VKey } from '@idux/cdk/utils'
-import type { ComputedRef, Ref } from 'vue'
+import type { Ref } from 'vue'
import { ref } from 'vue'
@@ -16,8 +24,8 @@ import { isFunction, isUndefined } from 'lodash-es'
import { callEmit } from '@idux/cdk/utils'
import { useGlobalConfig } from '@idux/components/config'
-import { getTargetFile, getTargetFileIndex, setFileStatus } from '../util/fileHandle'
-import defaultUpload from '../util/request'
+import { getTargetFile, getTargetFileIndex } from '../utils/files'
+import defaultUpload from '../utils/request'
export interface UploadRequest {
fileUploading: Ref
@@ -26,19 +34,25 @@ export interface UploadRequest {
upload: (file: UploadFile) => void
}
-export function useRequest(props: UploadProps, files: ComputedRef): UploadRequest {
+export function useRequest(props: UploadProps, filesDataContext: FilesDataContext): UploadRequest {
const fileUploading: Ref = ref([])
const aborts = new Map void>([])
const config = useGlobalConfig('upload')
+ const { fileList, updateFiles } = filesDataContext
+
+ const updateFileStatus = (file: UploadFile, status: UploadFileStatus) => {
+ updateFiles([{ ...file, status }])
+ }
+
function abort(file: UploadFile): void {
- const curFile = getTargetFile(file, files.value)
+ const curFile = getTargetFile(file, fileList.value)
if (!curFile) {
return
}
const curAbort = aborts.get(curFile.key)
curAbort?.()
- setFileStatus(curFile, 'abort', props.onFileStatusChange)
+ updateFileStatus(curFile, 'abort')
fileUploading.value.splice(getTargetFileIndex(curFile, fileUploading.value), 1)
aborts.delete(curFile.key)
props.onRequestChange &&
@@ -65,21 +79,21 @@ export function useRequest(props: UploadProps, files: ComputedRef)
await upload(result)
}
} catch (e) {
- setFileStatus(file, 'error', props.onFileStatusChange)
+ updateFileStatus(file, 'error')
}
} else if (before === true) {
await upload(file)
} else if (typeof before === 'object') {
await upload(before)
} else {
- setFileStatus(file, 'cancel', props.onFileStatusChange)
+ updateFileStatus(file, 'cancel')
}
}
async function upload(file: UploadFile) {
if (!file.raw) {
file.error = new Error('file error')
- setFileStatus(file, 'error', props.onFileStatusChange)
+ updateFileStatus(file, 'error')
}
const action = await getAction(props, file)
const requestData = await getRequestData(props, file)
@@ -103,7 +117,7 @@ export function useRequest(props: UploadProps, files: ComputedRef)
status: 'loadstart',
file: { ...file },
})
- setFileStatus(file, 'uploading', props.onFileStatusChange)
+ updateFileStatus(file, 'uploading')
file.percent = 0
const requestHandler = await uploadHttpRequest(requestOption)
aborts.set(file.key, requestHandler?.abort ?? (() => {}))
@@ -111,7 +125,7 @@ export function useRequest(props: UploadProps, files: ComputedRef)
}
function _onProgress(event: UploadProgressEvent, file: UploadFile): void {
- const curFile = getTargetFile(file, files.value)
+ const curFile = getTargetFile(file, fileList.value)
if (!curFile) {
return
}
@@ -125,7 +139,7 @@ export function useRequest(props: UploadProps, files: ComputedRef)
}
function _onError(error: UploadRequestError, file: UploadFile): void {
- const curFile = getTargetFile(file, files.value)
+ const curFile = getTargetFile(file, fileList.value)
if (!curFile) {
return
}
@@ -137,12 +151,12 @@ export function useRequest(props: UploadProps, files: ComputedRef)
status: 'error',
error,
})
- setFileStatus(curFile, 'error', props.onFileStatusChange)
+ updateFileStatus(curFile, 'error')
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _onSuccess(res: any, file: UploadFile): void {
- const curFile = getTargetFile(file, files.value)
+ const curFile = getTargetFile(file, fileList.value)
if (!curFile) {
return
}
@@ -153,7 +167,7 @@ export function useRequest(props: UploadProps, files: ComputedRef)
file: { ...curFile },
response: res,
})
- setFileStatus(curFile, 'success', props.onFileStatusChange)
+ updateFileStatus(curFile, 'success')
fileUploading.value.splice(getTargetFileIndex(curFile, fileUploading.value), 1)
}
diff --git a/packages/components/upload/src/composables/useUploadControl.ts b/packages/components/upload/src/composables/useUploadControl.ts
new file mode 100644
index 000000000..1b1bccdd1
--- /dev/null
+++ b/packages/components/upload/src/composables/useUploadControl.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import type { UploadRequest } from './useRequest'
+import type { UploadFile } from '../types'
+
+import { type ComputedRef, watch } from 'vue'
+
+import { getTargetFile } from '../utils/files'
+
+export function useUploadControl(fileList: ComputedRef, uploadRequest: UploadRequest): void {
+ const { startUpload, abort } = uploadRequest
+
+ watch(fileList, (currentFileList, preFileList) => {
+ currentFileList.forEach(file => {
+ const preFile = getTargetFile(file, preFileList)
+
+ if (!preFile && file.status === 'selected') {
+ startUpload(file)
+ }
+ })
+
+ preFileList.forEach(file => {
+ const currentFile = getTargetFile(file, currentFileList)
+
+ if (!currentFile && file.status === 'uploading') {
+ abort(file)
+ }
+ })
+ })
+}
diff --git a/packages/components/upload/src/token.ts b/packages/components/upload/src/token.ts
index f9f7f8697..ce9f69439 100644
--- a/packages/components/upload/src/token.ts
+++ b/packages/components/upload/src/token.ts
@@ -5,18 +5,19 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
+import type { UploadDrag } from './composables/useDrag'
+import type { FilesDataContext } from './composables/useFilesData'
import type { UploadRequest } from './composables/useRequest'
-import type { UploadFile, UploadProps } from './types'
+import type { UploadProps } from './types'
import type { Locale } from '@idux/components/locales'
-import type { ComputedRef, InjectionKey } from 'vue'
+import type { InjectionKey } from 'vue'
-export type UploadToken = {
+export interface UploadContext extends UploadRequest, UploadDrag, FilesDataContext {
props: UploadProps
locale: Locale
- files: ComputedRef
+ selectFiles: (files: File[]) => Promise
setViewerVisible: (visible: boolean, imageSrc?: string) => void
- onUpdateFiles: (file: UploadFile[]) => void
setSelectorVisible: (isShow: boolean) => void
-} & UploadRequest
+}
-export const uploadToken: InjectionKey = Symbol('UploadToken')
+export const uploadToken: InjectionKey = Symbol('UploadToken')
diff --git a/packages/components/upload/src/types.ts b/packages/components/upload/src/types.ts
index cdf7903ae..f7124e4d5 100644
--- a/packages/components/upload/src/types.ts
+++ b/packages/components/upload/src/types.ts
@@ -18,6 +18,12 @@ export type UploadRequestStatus = 'loadstart' | 'progress' | 'abort' | 'error' |
export type UploadFileStatus = 'selected' | 'cancel' | 'uploading' | 'error' | 'success' | 'abort'
export type UploadFilesType = 'text' | 'image' | 'imageCard'
export type UploadIconType = 'file' | 'preview' | 'download' | 'remove' | 'retry'
+
+export interface FilteredFile {
+ file: File
+ reason: 'acceptNotMatch' | 'maxCountExceeded'
+}
+
export interface UploadRequestHandler {
abort?: () => void
}
@@ -102,7 +108,10 @@ export const uploadProps = {
requestHeaders: Object as PropType,
requestMethod: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>,
'onUpdate:files': [Function, Array] as PropType[]) => void>>,
- onSelect: [Function, Array] as PropType boolean | File[] | Promise>>,
+ onSelect: [Function, Array] as PropType<
+ MaybeArray<(files: File[], filteredFiles: FilteredFile[]) => boolean | File[] | Promise | void>
+ >,
+ onMaxCountExceeded: [Function, Array] as PropType void>>,
onBeforeUpload: [Function, Array] as PropType<
MaybeArray<(file: UploadFile) => boolean | UploadFile | Promise>>
>,
diff --git a/packages/components/upload/src/util/fileHandle.ts b/packages/components/upload/src/utils/files.ts
similarity index 63%
rename from packages/components/upload/src/util/fileHandle.ts
rename to packages/components/upload/src/utils/files.ts
index 35c2580d6..4994f3a9b 100644
--- a/packages/components/upload/src/util/fileHandle.ts
+++ b/packages/components/upload/src/utils/files.ts
@@ -5,11 +5,11 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { UploadFile, UploadFileStatus } from '../types'
+import type { UploadFile } from '../types'
-import { callEmit, uniqueId } from '@idux/cdk/utils'
+import { uniqueId } from '@idux/cdk/utils'
-export function getFileInfo(file: File, options: Partial = {}): UploadFile {
+export function createUploadFile(file: File, options: Partial = {}): UploadFile {
const key = uniqueId()
return {
key,
@@ -34,18 +34,9 @@ export function isImage(file: File): boolean {
return !!file.type && file.type.startsWith('image/')
}
-export function setFileStatus(
- file: UploadFile,
- status: UploadFileStatus,
- onFileStatusChange?: ((file: UploadFile) => void) | ((file: UploadFile) => void)[],
-): void {
- file.status = status
- onFileStatusChange && callEmit(onFileStatusChange, file)
-}
-
-export function getFilesAcceptAllow(filesSelected: File[], accept?: string[]): File[] {
+export function filterFilesByAccept(filesSelected: File[], accept?: string[]): [File[], File[]] {
if (!accept || accept.length === 0) {
- return filesSelected
+ return [filesSelected, []]
}
const isMatch = (file: File, type: string) => {
const ext = `.${file.name.split('.').pop()}`.toLowerCase()
@@ -62,23 +53,42 @@ export function getFilesAcceptAllow(filesSelected: File[], accept?: string[]): F
}
return false
}
- return filesSelected.filter(file => accept.some(type => isMatch(file, type)))
+
+ const accepted: File[] = []
+ const filtered: File[] = []
+
+ filesSelected.forEach(file => {
+ if (accept.some(type => isMatch(file, type))) {
+ accepted.push(file)
+ } else {
+ filtered.push(file)
+ }
+ })
+
+ return [accepted, filtered]
}
-export function getFilesCountAllow(filesSelected: File[], curFilesCount: number, maxCount?: number): File[] {
+export function filterFilesByMaxCount(
+ filesSelected: File[],
+ curFilesCount: number,
+ maxCount?: number,
+): [File[], File[]] {
if (!maxCount) {
- return filesSelected
+ return [filesSelected, []]
}
// 当为 1 时,始终用最新上传的文件代替当前文件
if (maxCount === 1) {
- return filesSelected.slice(0, 1)
+ return [filesSelected.slice(0, 1), filesSelected.slice(1)]
}
+
const remainder = maxCount - curFilesCount
if (remainder <= 0) {
- return []
+ return [[], filesSelected]
}
+
if (remainder >= filesSelected.length) {
- return filesSelected
+ return [filesSelected, []]
}
- return filesSelected.slice(0, remainder)
+
+ return [filesSelected.slice(0, remainder), filesSelected.slice(remainder)]
}
diff --git a/packages/components/upload/src/util/icon.ts b/packages/components/upload/src/utils/icon.ts
similarity index 100%
rename from packages/components/upload/src/util/icon.ts
rename to packages/components/upload/src/utils/icon.ts
diff --git a/packages/components/upload/src/util/request.ts b/packages/components/upload/src/utils/request.ts
similarity index 100%
rename from packages/components/upload/src/util/request.ts
rename to packages/components/upload/src/utils/request.ts
diff --git a/packages/components/upload/src/util/visible.ts b/packages/components/upload/src/utils/visible.ts
similarity index 100%
rename from packages/components/upload/src/util/visible.ts
rename to packages/components/upload/src/utils/visible.ts