diff --git a/src/uploader/PreviewItem.js b/src/uploader/PreviewItem.tsx similarity index 84% rename from src/uploader/PreviewItem.js rename to src/uploader/PreviewItem.tsx index 2c30b0a3b..dd69d59de 100644 --- a/src/uploader/PreviewItem.js +++ b/src/uploader/PreviewItem.tsx @@ -1,23 +1,28 @@ -import { bem } from './shared'; -import { isImageFile } from './utils'; +import { defineComponent, PropType } from 'vue'; + +// Utils +import { bem, isImageFile, FileListItem } from './utils'; import { isDef, getSizeStyle } from '../utils'; -import { callInterceptor } from '../utils/interceptor'; +import { callInterceptor, Interceptor } from '../utils/interceptor'; // Components import Icon from '../icon'; -import Image from '../image'; +import Image, { ImageFit } from '../image'; import Loading from '../loading'; -export default { +export default defineComponent({ props: { - name: String, - item: Object, + name: [String, Number], index: Number, - imageFit: String, + imageFit: String as PropType, lazyLoad: Boolean, deletable: Boolean, previewSize: [Number, String], - beforeDelete: Function, + beforeDelete: Function as PropType, + item: { + type: Object as PropType, + required: true, + }, }, emits: ['delete', 'preview'], @@ -45,7 +50,7 @@ export default { } }; - const onDelete = (event) => { + const onDelete = (event: MouseEvent) => { const { name, item, index, beforeDelete } = props; event.stopPropagation(); callInterceptor({ @@ -120,4 +125,4 @@ export default { ); }, -}; +}); diff --git a/src/uploader/index.js b/src/uploader/index.tsx similarity index 71% rename from src/uploader/index.js rename to src/uploader/index.tsx index 552dfea8c..737482f6a 100644 --- a/src/uploader/index.js +++ b/src/uploader/index.tsx @@ -1,14 +1,17 @@ -import { ref, reactive } from 'vue'; +import { ref, reactive, PropType } from 'vue'; // Utils -import { bem, createComponent } from './shared'; -import { isPromise, getSizeStyle, pick } from '../utils'; +import { pick, isPromise, getSizeStyle, ComponentInstance } from '../utils'; import { + bem, toArray, isOversize, filterFiles, isImageFile, + FileListItem, readFileContent, + createComponent, + UploaderResultType, } from './utils'; // Composition @@ -18,7 +21,29 @@ import { useLinkField } from '../composables/use-link-field'; // Components import Icon from '../icon'; import PreviewItem from './PreviewItem'; -import ImagePreview from '../image-preview'; +import ImagePreview, { ImagePreviewOptions } from '../image-preview'; + +// Types +import type { ImageFit } from '../image'; +import type { Interceptor } from '../utils/interceptor'; + +type PromiseOrNot = T | Promise; + +export type UploaderBeforeRead = ( + file: File | File[], + detail: { + name: string | number; + index: number; + } +) => PromiseOrNot; + +export type UploaderAfterRead = ( + items: FileListItem | FileListItem[], + detail: { + name: string | number; + index: number; + } +) => void; export default createComponent({ props: { @@ -27,11 +52,11 @@ export default createComponent({ disabled: Boolean, lazyLoad: Boolean, uploadText: String, - afterRead: Function, - beforeRead: Function, - beforeDelete: Function, + afterRead: Function as PropType, + beforeRead: Function as PropType, + beforeDelete: Function as PropType, previewSize: [Number, String], - previewOptions: Object, + previewOptions: Object as PropType, name: { type: [Number, String], default: '', @@ -41,7 +66,7 @@ export default createComponent({ default: 'image/*', }, modelValue: { - type: Array, + type: Array as PropType, default: () => [], }, maxSize: { @@ -69,11 +94,11 @@ export default createComponent({ default: true, }, imageFit: { - type: String, + type: String as PropType, default: 'cover', }, resultType: { - type: String, + type: String as PropType, default: 'dataUrl', }, uploadIcon: { @@ -104,7 +129,7 @@ export default createComponent({ } }; - const onAfterRead = (items) => { + const onAfterRead = (items: FileListItem | FileListItem[]) => { resetInput(); if (isOversize(items, props.maxSize)) { @@ -129,11 +154,11 @@ export default createComponent({ } }; - const readFile = (files) => { + const readFile = (files: File | File[]) => { const { maxCount, modelValue, resultType } = props; if (Array.isArray(files)) { - const remainCount = maxCount - modelValue.length; + const remainCount = +maxCount - modelValue.length; if (files.length > remainCount) { files = files.slice(0, remainCount); @@ -142,11 +167,11 @@ export default createComponent({ Promise.all( files.map((file) => readFileContent(file, resultType)) ).then((contents) => { - const fileList = files.map((file, index) => { - const result = { file, status: '', message: '' }; + const fileList = (files as File[]).map((file, index) => { + const result: FileListItem = { file, status: '', message: '' }; if (contents[index]) { - result.content = contents[index]; + result.content = contents[index] as string; } return result; @@ -156,7 +181,11 @@ export default createComponent({ }); } else { readFileContent(files, resultType).then((content) => { - const result = { file: files, status: '', message: '' }; + const result: FileListItem = { + file: files as File, + status: '', + message: '', + }; if (content) { result.content = content; @@ -167,17 +196,18 @@ export default createComponent({ } }; - const onChange = (event) => { - let { files } = event.target; + const onChange = (event: Event) => { + const { files } = event.target as HTMLInputElement; - if (props.disabled || !files.length) { + if (props.disabled || !files || !files.length) { return; } - files = files.length === 1 ? files[0] : [].slice.call(files); + const file = + files.length === 1 ? files[0] : ([].slice.call(files) as File[]); if (props.beforeRead) { - const response = props.beforeRead(files, getDetail()); + const response = props.beforeRead(file, getDetail()); if (!response) { resetInput(); @@ -190,7 +220,7 @@ export default createComponent({ if (data) { readFile(data); } else { - readFile(files); + readFile(file); } }) .catch(resetInput); @@ -198,19 +228,21 @@ export default createComponent({ } } - readFile(files); + readFile(file); }; - let imagePreview; + let imagePreview: ComponentInstance | undefined; const onClosePreview = () => { emit('close-preview'); }; - const previewImage = (item) => { + const previewImage = (item: FileListItem) => { if (props.previewFullImage) { const imageFiles = props.modelValue.filter(isImageFile); - const images = imageFiles.map((item) => item.content || item.url); + const images = imageFiles + .map((item) => item.content || item.url) + .filter((item) => !!item) as string[]; imagePreview = ImagePreview({ images, @@ -227,7 +259,7 @@ export default createComponent({ } }; - const deleteFile = (item, index) => { + const deleteFile = (item: FileListItem, index: number) => { const fileList = props.modelValue.slice(0); fileList.splice(index, 1); @@ -235,22 +267,18 @@ export default createComponent({ emit('delete', item, getDetail(index)); }; - const renderPreviewItem = (item, index) => { + const renderPreviewItem = (item: FileListItem, index: number) => { const needPickData = [ 'imageFit', 'deletable', 'previewSize', 'beforeDelete', - ]; + ] as const; - const previewData = pick(props, needPickData); - const previewProp = pick(item, needPickData); - - Object.keys(previewProp).forEach((item) => { - if (previewProp[item] !== undefined) { - previewData[item] = previewProp[item]; - } - }); + const previewData = { + ...pick(props, needPickData), + ...pick(item, needPickData, true), + }; return ( (item: T | T[]): T[] { if (Array.isArray(item)) { @@ -8,8 +29,8 @@ export function toArray(item: T | T[]): T[] { return [item]; } -export function readFileContent(file: File, resultType: ResultType) { - return new Promise((resolve) => { +export function readFileContent(file: File, resultType: UploaderResultType) { + return new Promise((resolve) => { if (resultType === 'file') { resolve(); return; @@ -18,7 +39,7 @@ export function readFileContent(file: File, resultType: ResultType) { const reader = new FileReader(); reader.onload = (event) => { - resolve((event.target as FileReader).result); + resolve((event.target as FileReader).result as string); }; if (resultType === 'dataUrl') { @@ -29,15 +50,6 @@ export function readFileContent(file: File, resultType: ResultType) { }); } -export type FileListItem = { - url?: string; - file?: File; - content?: string; // dataUrl - isImage?: boolean; - status?: '' | 'uploading' | 'done' | 'failed'; - message?: string; -}; - export function isOversize( items: FileListItem | FileListItem[], maxSize: number | string @@ -81,7 +93,7 @@ export function isImageFile(item: FileListItem): boolean { return isImageUrl(item.url); } - if (item.content) { + if (typeof item.content === 'string') { return item.content.indexOf('data:image') === 0; } diff --git a/src/utils/base.ts b/src/utils/base.ts index e6306ff92..d03d66ba6 100644 --- a/src/utils/base.ts +++ b/src/utils/base.ts @@ -38,9 +38,15 @@ export function get(object: any, path: string): any { return result; } -export function pick(obj: T, keys: ReadonlyArray) { +export function pick( + obj: T, + keys: ReadonlyArray, + ignoreUndefined?: boolean +) { return keys.reduce((ret, key) => { - ret[key] = obj[key]; + if (!ignoreUndefined || obj[key] !== undefined) { + ret[key] = obj[key]; + } return ret; }, {} as Pick); } diff --git a/src/vue-tsx-shim.d.ts b/src/vue-tsx-shim.d.ts index 334a49c3a..018dbb541 100644 --- a/src/vue-tsx-shim.d.ts +++ b/src/vue-tsx-shim.d.ts @@ -22,12 +22,14 @@ declare module 'vue' { onCancel?: EventHandler; onClosed?: EventHandler; onChange?: EventHandler; + onDelete?: EventHandler; onOpened?: EventHandler; onScroll?: EventHandler; onSubmit?: EventHandler; onSelect?: EventHandler; onToggle?: EventHandler; onConfirm?: EventHandler; + onPreview?: EventHandler; onKeypress?: EventHandler; onTouchend?: EventHandler; onClickStep?: EventHandler;