import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react' import classNames from 'classnames' import { CommonComponentProps } from '../../utils/types' import { readFileContent, FileReaderResultType } from '../../utils' import { Icon } from '../icon/Icon' import './Upload.scss' import { ImagePreview } from '../image-preview/ImagePreview' import { isImageFile } from './utils' import { UploadPreview, UploadPreviewProps, UploadFileItem, } from './UploadPreview' import { useEvent } from '../../use' export * from './UploadPreview' interface ChainNode { (data: any, next: (...args: any[]) => void): void } export interface UploadBaseProps extends CommonComponentProps { className?: string style?: CSSProperties children?: ReactNode accept?: string capture?: boolean | 'user' | 'environment' previewList?: UploadPreviewProps[] defaultPreviewList?: UploadPreviewProps[] maxCount?: number maxSize?: number | ((file: File) => boolean) select?: ReactNode resultType?: FileReaderResultType disabled?: boolean readOnly?: boolean beforeRead?: (file: File) => boolean | Promise onChange?: (previewPropsList: UploadPreviewProps[]) => void removable?: boolean remove?: ReactNode beforeRemove?: (...args: any[]) => boolean | Promise onRemove?: (index: number) => void } export interface UploadSingleProps extends UploadBaseProps { multiple?: false afterRead?: (previewProps: UploadFileItem) => void overSize?: (previewProps: UploadFileItem) => void } export interface UploadMutipleProps extends UploadBaseProps { multiple: true afterRead?: (previewPropsList: UploadFileItem[]) => void overSize?: (previewPropsList: UploadFileItem[]) => void } export type UploadProps = UploadSingleProps | UploadMutipleProps export function Upload(props: UploadProps) { const { className, children, multiple, accept, capture, previewList, defaultPreviewList, maxCount = Infinity, maxSize = Infinity, overSize, select, resultType = 'dataUrl', disabled, readOnly, beforeRead, afterRead, onChange, removable = true, remove, beforeRemove, onRemove, ...restProps } = props const inputRef = useRef(null) const [innerPreviewList, setInnerPreviewList] = useState( previewList ?? defaultPreviewList ?? [] ) // 受控 useEffect(() => { if (previewList != null) { setInnerPreviewList(previewList) } }, [previewList]) const resetInput = () => { if (inputRef.current) { inputRef.current.value = '' } } const limitCountNode: ChainNode = (files: File[], next) => { const remainCount = maxCount - innerPreviewList.length if (files.length > remainCount) { files = files.slice(0, remainCount) } next(files) } const beforeReadNode: ChainNode = (files: File[], next) => { Promise.allSettled( files.map( (file) => new Promise((resolve, reject) => { if (beforeRead) { const ret = beforeRead(file) if (!ret) { resetInput() reject() return } if (ret instanceof Promise) { ret .then((mayNewFile) => { resolve(mayNewFile instanceof File ? mayNewFile : file) }) .catch(() => { resetInput() reject() }) return } } resolve(file) }) ) ).then((results) => { const fileList = results .filter((result) => result.status === 'fulfilled') .map((result) => (result as PromiseFulfilledResult).value) next(fileList) }) } const readFileContentNode: ChainNode = (files: File[], next) => { resetInput() Promise.all(files.map((file) => readFileContent(file, resultType))).then( (contents) => { const fileList = files.map((file, index) => { const item: UploadPreviewProps = { file, } if (contents[index]) { item.content = contents[index] as string } return item }) next(fileList) } ) } const limitSizeNode: ChainNode = (fileList: UploadFileItem[], next) => { const valid: UploadFileItem[] = [] const invalid: UploadFileItem[] = [] fileList.forEach((item) => { const file = item.file as File if ( (typeof maxSize === 'function' && maxSize(file)) || file.size > maxSize ) { invalid.push(item) } else { valid.push(item) } }) if (invalid.length) { if (multiple) { overSize?.(invalid) } else { overSize?.(invalid[0]) } } if (valid.length) { const list = [...innerPreviewList, ...valid] // 非受控 if (previewList == null) { setInnerPreviewList(list) } onChange?.(list) next(valid) } } const afterReadNode: ChainNode = (fileList: UploadFileItem[]) => { if (multiple) { afterRead?.(fileList) } else { afterRead?.(fileList[0]) } } const handleChange = () => { const files = inputRef.current?.files if (disabled || !files || files.length === 0) { return } const fileList = [...files] const chain = [ limitCountNode, beforeReadNode, readFileContentNode, limitSizeNode, afterReadNode, ].reduceRight<(...args: any[]) => void>( (next, node) => (data: any) => { node(data, next) }, () => {} ) chain(fileList) } const handleRemove = useEvent((index: number, item: UploadPreviewProps) => { const list = innerPreviewList.filter((_, i) => i !== index) // 非受控 if (previewList == null) { setInnerPreviewList(list) } onChange?.(list) item.onRemove?.(index) onRemove?.(index) }) const handleClick = useEvent((index: number) => { const currentItem = innerPreviewList[index] if (!isImageFile(currentItem)) { return } const previewList = innerPreviewList.filter((item) => isImageFile(item)) const previewIndex = previewList.findIndex((item) => item === currentItem) const imageList = previewList.map( (item) => (item.url || item.content) as string ) ImagePreview.show({ images: imageList, defaultIndex: previewIndex, }) }) const uploadClass = classNames( 's-upload', { 's-upload-disabled': disabled, 's-upload-readonly': readOnly, }, className ) const renderInput = () => { return ( !disabled && ( ) ) } return (
{children && !readOnly && innerPreviewList.length < maxCount && (
{children} {renderInput()}
)}
{innerPreviewList.map((item, index) => { return ( handleRemove(index, item)} onClick={handleClick} > ) })} {!children && !readOnly && innerPreviewList.length < maxCount && (
{select ?? } {renderInput()}
)}
) } Upload.Preview = UploadPreview export default Upload