From 8faddc1388ddc8dae97814ece2ac3eef69c5c53f Mon Sep 17 00:00:00 2001 From: saller Date: Fri, 8 Sep 2023 16:11:06 +0800 Subject: [PATCH] feat(comp:upload): add `onMaxCountExceeded` (#1673) refactor upload component --- packages/components/upload/demo/MaxCount.vue | 11 ++ packages/components/upload/index.ts | 1 + packages/components/upload/src/Upload.tsx | 26 ++- .../upload/src/component/ImageCardList.tsx | 20 +-- .../upload/src/component/ImageList.tsx | 20 +-- .../upload/src/component/Selector.tsx | 159 +++--------------- .../upload/src/component/TextList.tsx | 20 +-- .../upload/src/composables/useDisplay.ts | 4 +- .../upload/src/composables/useDrag.ts | 23 ++- .../upload/src/composables/useFileSelect.ts | 82 +++++++++ .../upload/src/composables/useFilesData.ts | 61 +++++++ .../upload/src/composables/useOperation.ts | 29 ++-- .../upload/src/composables/useRequest.ts | 46 +++-- .../src/composables/useUploadControl.ts | 35 ++++ packages/components/upload/src/token.ts | 15 +- packages/components/upload/src/types.ts | 11 +- .../{util/fileHandle.ts => utils/files.ts} | 52 +++--- .../upload/src/{util => utils}/icon.ts | 0 .../upload/src/{util => utils}/request.ts | 0 .../upload/src/{util => utils}/visible.ts | 0 20 files changed, 356 insertions(+), 259 deletions(-) create mode 100644 packages/components/upload/src/composables/useFileSelect.ts create mode 100644 packages/components/upload/src/composables/useFilesData.ts create mode 100644 packages/components/upload/src/composables/useUploadControl.ts rename packages/components/upload/src/{util/fileHandle.ts => utils/files.ts} (63%) rename packages/components/upload/src/{util => utils}/icon.ts (100%) rename packages/components/upload/src/{util => utils}/request.ts (100%) rename packages/components/upload/src/{util => utils}/visible.ts (100%) 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 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 () => ( ) }, 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 && ( 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