training_data/apps/web/src/components/common/uploader/AvatarUploader.tsx

159 lines
4.3 KiB
TypeScript
Raw Normal View History

2025-02-06 16:32:52 +08:00
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();
2025-02-24 10:16:36 +08:00
useEffect(() => {
setPreviewUrl(value || "");
}, [value]);
2025-02-06 16:32:52 +08:00
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;