159 lines
4.3 KiB
TypeScript
Executable File
159 lines
4.3 KiB
TypeScript
Executable File
import { env } from "@web/src/env";
|
||
import { message, Progress, Spin, theme } from "antd";
|
||
import React, { useState, useEffect, useRef } from "react";
|
||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||
import { Avatar } from "antd/lib";
|
||
import toast from "react-hot-toast";
|
||
|
||
export interface AvatarUploaderProps {
|
||
value?: string;
|
||
placeholder?: string;
|
||
className?: string;
|
||
onChange?: (value: string) => void;
|
||
compressed?: boolean;
|
||
style?: React.CSSProperties; // 添加style属性
|
||
}
|
||
|
||
interface UploadingFile {
|
||
name: string;
|
||
progress: number;
|
||
status: "uploading" | "done" | "error";
|
||
fileId?: string;
|
||
url?: string;
|
||
compressedUrl?: string;
|
||
fileKey?: string;
|
||
}
|
||
|
||
const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||
value,
|
||
onChange,
|
||
compressed = false,
|
||
className,
|
||
placeholder = "点击上传",
|
||
style, // 解构style属性
|
||
}) => {
|
||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||
const [file, setFile] = useState<UploadingFile | null>(null);
|
||
const avatarRef = useRef<HTMLImageElement>(null);
|
||
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
||
|
||
const [compressedUrl, setCompressedUrl] = useState<string>(value || "");
|
||
const [url, setUrl] = useState<string>(value || "");
|
||
const [uploading, setUploading] = useState(false);
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
// 在组件中定义 key 状态
|
||
const [avatarKey, setAvatarKey] = useState(0);
|
||
const { token } = theme.useToken();
|
||
useEffect(() => {
|
||
setPreviewUrl(value || "");
|
||
}, [value]);
|
||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const selectedFile = event.target.files?.[0];
|
||
if (!selectedFile) return;
|
||
// Create an object URL for the selected file
|
||
const objectUrl = URL.createObjectURL(selectedFile);
|
||
setPreviewUrl(objectUrl);
|
||
setFile({
|
||
name: selectedFile.name,
|
||
progress: 0,
|
||
status: "uploading",
|
||
fileKey: `${selectedFile.name}-${Date.now()}`,
|
||
});
|
||
setUploading(true);
|
||
|
||
try {
|
||
const uploadedUrl = await new Promise<string>((resolve, reject) => {
|
||
handleFileUpload(
|
||
selectedFile,
|
||
(result) => {
|
||
setFile((prev) => ({
|
||
...prev!,
|
||
progress: 100,
|
||
status: "done",
|
||
fileId: result.fileId,
|
||
url: result.url,
|
||
compressedUrl: result.compressedUrl,
|
||
}));
|
||
|
||
setUrl(result.url);
|
||
setCompressedUrl(result.compressedUrl);
|
||
// 直接使用 result 中的最新值
|
||
resolve(compressed ? result.compressedUrl : result.url);
|
||
},
|
||
(error) => {
|
||
reject(error);
|
||
},
|
||
file?.fileKey
|
||
);
|
||
});
|
||
// await new Promise((resolve) => setTimeout(resolve,4999)); // 方法1:使用 await 暂停执行
|
||
// 使用 resolved 的最新值调用 onChange
|
||
// 强制刷新 Avatar 组件
|
||
setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载
|
||
onChange?.(uploadedUrl);
|
||
console.log(uploadedUrl);
|
||
toast.success("头像上传成功");
|
||
} catch (error) {
|
||
console.error("上传错误:", error);
|
||
toast.error("头像上传失败");
|
||
setFile((prev) => ({ ...prev!, status: "error" }));
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
};
|
||
|
||
const triggerUpload = () => {
|
||
inputRef.current?.click();
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`relative w-24 h-24 overflow-hidden cursor-pointer ${className}`}
|
||
onClick={triggerUpload}
|
||
style={{
|
||
border: `1px solid ${token.colorBorder}`,
|
||
background: token.colorBgContainer,
|
||
...style, // 应用外部传入的样式
|
||
}}>
|
||
<input
|
||
type="file"
|
||
ref={inputRef}
|
||
onChange={handleChange}
|
||
accept="image/*"
|
||
style={{ display: "none" }}
|
||
/>
|
||
{previewUrl ? (
|
||
<Avatar
|
||
key={avatarKey}
|
||
ref={avatarRef}
|
||
src={previewUrl}
|
||
shape="square"
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="flex items-center justify-center w-full h-full text-sm text-gray-500">
|
||
{placeholder}
|
||
</div>
|
||
)}
|
||
{uploading && (
|
||
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||
<Spin />
|
||
</div>
|
||
)}
|
||
{file && file.status === "uploading" && (
|
||
<div className="absolute bottom-0 left-0 right-0 bg-white bg-opacity-75">
|
||
<Progress
|
||
percent={Math.round(
|
||
uploadProgress?.[file.fileKey!] || 0
|
||
)}
|
||
showInfo={false}
|
||
strokeColor={token.colorPrimary}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AvatarUploader;
|