我有一个可以控制和控制的 Uploader
组件。 instantUpload
prop 允许在选择文件后进行imididate 更新。 不幸的是,由于某种原因, files
变量在 onUploadRegent
中未适当更新, 并且 on Success
回调( 即 < code> file 是空数组, 而不是选择的 file
。 为何这样解释?
use client ;
import { ChangeEvent, RefObject, createContext, useRef, useState } from react ;
import { useSaveFile } from @/api/file/hooks ;
import { useToast } from @/components/ui/use-toast ;
import { DefaultUploadContainer } from @/components/uploader/default-upload-container ;
import { DefaultUploadTrigger } from @/components/uploader/default-upload-trigger ;
import { formatFileSize, getFileExtension } from @/lib/utils ;
import { UploadedFile } from @/types ;
import { DefaultExtensionType } from react-file-icon ;
export type UploaderFile = {
file?: globalThis.File;
src: string;
extension: DefaultExtensionType;
progress?: number;
uploading?: boolean;
name: string;
size: number;
id?: string;
uploadedFile?: UploadedFile;
};
interface UploaderContext {
files: UploaderFile[];
onChange: (files: UploaderFile[]) => void;
handleUpload: (file: UploaderFile) => void;
handleRemoveFile: (file: UploaderFile) => void;
fileInputRef: RefObject<HTMLInputElement>;
}
export const UploaderContext = createContext<UploaderContext>({} as any);
export interface UploaderProps {
allowedExtensions?: DefaultExtensionType[];
multiple?: boolean;
instantUpload?: boolean;
files?: UploaderFile[];
defaultFiles?: UploaderFile[];
maxSize?: number;
onChange?: (files: UploaderFile[]) => void;
onClick?: (file: UploaderFile) => void;
triggerRenderer?: () => JSX.Element;
containerRenderer?: () => JSX.Element;
Trigger?: React.ComponentType;
Container?: React.ComponentType;
}
export function Uploader({
multiple = true,
allowedExtensions,
instantUpload = true,
maxSize,
files: valueFromProps,
defaultFiles: defaultValue,
Trigger,
Container,
onChange: onChangeFromProps,
}: UploaderProps) {
// A component can be considered controlled when its value prop is
// not undefined.
const isControlled = typeof valueFromProps != undefined ;
// When a component is not controlled, it can have a defaultValue.
const hasDefaultValue = typeof defaultValue != undefined ;
// If a defaultValue is specified, we will use it as our initial
// state. Otherwise, we will simply use an empty array.
const [internalValue, setInternalValue] = useState<UploaderFile[]>(
hasDefaultValue ? defaultValue : []
);
// Internally, we need to deal with some value. Depending on whether
// the component is controlled or not, that value comes from its
// props or from its internal state.
let files: UploaderFile[];
if (isControlled) {
files = valueFromProps;
} else files = internalValue;
const onChange = (value: UploaderFile[]) => {
// If exists, we always call onChange callback passed in props
// We do this even if there is no props.value (and the component
// is uncontrolled.)
if (onChangeFromProps) {
onChangeFromProps(value);
}
// If the component is uncontrolled, we need to update our
// internal value here.
if (!isControlled) {
setInternalValue(value);
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const mutationUploadFile = useSaveFile({});
const { toast } = useToast();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
let inputFiles: UploaderFile[] = Array.from(event.target.files || []).map(
(file) => ({
file: file,
name: file.name,
size: file.size,
src: URL.createObjectURL(file),
extension: getFileExtension(file.name),
})
);
if (!inputFiles.length) return;
if (!multiple && files.length !== 0) {
toast({
title: Zbyt wiele plików! ,
description: <div>Możesz przesłać maksymalnie jeden plik!</div>,
variant: destructive ,
});
return;
}
for (let file of inputFiles) {
if (!file.file) continue;
if (allowedExtensions && !allowedExtensions.includes(file.extension)) {
toast({
title: Nieprawidłowy format pliku! ,
description: (
<div className= w-full max-w-full >
<p>
Dozwolone są jedynie pliki w nastepujących formatach:{ }
<span className= font-bold >
{allowedExtensions.map((x) => x.toUpperCase()).join( , )}.
</span>
</p>
<p className= pt-2 >
Pominięto plik: <span className= italic >{file.file.name}</span>
</p>
</div>
),
variant: destructive ,
});
inputFiles = inputFiles.filter((item) => item.name !== file.name);
}
if (maxSize && file.file.size > maxSize) {
toast({
title: Zbyt duży rozmiar pliku! ,
description: (
<div>
Rozmiar pliku <span className= italic >{file.file.name}</span> (
{formatFileSize(file.file.size)}) przekracza maksymalny dozwolony
rozmiar{ }
<span className= font-bold >({formatFileSize(maxSize)})</span>
</div>
),
variant: destructive ,
});
inputFiles = inputFiles.filter((x) => x !== file);
}
if (files.some((x) => x.file?.name === file.file?.name)) {
inputFiles = inputFiles.filter((x) => x !== file);
toast({
title: Plik już istnieje! ,
description: (
<div>
Plik <span className= italic >{file.file.name}</span> (
{formatFileSize(file.file.size)}) został już dodany.
</div>
),
variant: destructive ,
});
}
}
onChange([...files, ...inputFiles]);
if (instantUpload) {
for (let file of inputFiles) {
handleUpload(file);
}
}
fileInputRef.current!.value = ;
};
const handleUpload = (file: UploaderFile) => {
mutationUploadFile.mutate(
{
file: file.file!,
onUploadProgress(progress) {
onChange(
files.map((item) => {
if (item.name === file.name)
return {
...file,
uploading: true,
progress: Math.floor(
(progress.loaded / progress.total!) * 100
),
};
return item;
})
);
},
},
{
onSuccess(res) {
onChange(
files.map((item) => {
if (item.name === file.name)
return {
...item,
id: res.id,
src: res.url,
uploadedFile: res,
uploading: false,
};
return item;
})
);
},
onError(error, variables, context) {},
}
);
};
const handleRemoveFile = (file: UploaderFile) => {
if (file.uploadedFile) {
onChange(files.filter((item) => item.name !== file.name));
} else {
onChange(files.filter((item) => item.name !== file.name));
}
};
return (
<UploaderContext.Provider
value={{
files,
onChange,
handleUpload,
handleRemoveFile,
fileInputRef,
}}
>
<div className= w-full >
<input
type= file
name= file
hidden
multiple={multiple}
ref={fileInputRef}
onChange={handleChange}
/>
{Trigger ? <Trigger /> : <DefaultUploadTrigger />}
{Container ? <Container /> : <DefaultUploadContainer />}
</div>
</UploaderContext.Provider>
);
}