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属性
|
2025-02-26 08:50:18 +08:00
|
|
|
|
successText?: string;
|
|
|
|
|
showCover?: boolean;
|
2025-02-06 16:32:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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属性
|
2025-02-26 08:50:18 +08:00
|
|
|
|
successText = "上传成功",
|
|
|
|
|
showCover = true,
|
2025-02-06 16:32:52 +08:00
|
|
|
|
}) => {
|
|
|
|
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
|
|
|
|
const [file, setFile] = useState<UploadingFile | null>(null);
|
|
|
|
|
const avatarRef = useRef<HTMLImageElement>(null);
|
|
|
|
|
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
2025-02-24 20:53:22 +08:00
|
|
|
|
const [imageSrc, setImageSrc] = useState(value);
|
2025-02-06 16:32:52 +08:00
|
|
|
|
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(() => {
|
2025-02-24 20:53:22 +08:00
|
|
|
|
if (!previewUrl || previewUrl?.length < 1) {
|
|
|
|
|
setPreviewUrl(value || "");
|
|
|
|
|
}
|
2025-02-24 10:16:36 +08:00
|
|
|
|
}, [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 强制重新挂载
|
|
|
|
|
console.log(uploadedUrl);
|
2025-02-26 08:50:18 +08:00
|
|
|
|
onChange?.(uploadedUrl);
|
|
|
|
|
toast.success(successText);
|
2025-02-06 16:32:52 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("上传错误:", error);
|
2025-02-26 08:50:18 +08:00
|
|
|
|
toast.error("上传失败");
|
2025-02-06 16:32:52 +08:00
|
|
|
|
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" }}
|
|
|
|
|
/>
|
2025-02-26 08:50:18 +08:00
|
|
|
|
{(previewUrl && showCover) ? (
|
2025-02-06 16:32:52 +08:00
|
|
|
|
<Avatar
|
|
|
|
|
key={avatarKey}
|
|
|
|
|
ref={avatarRef}
|
|
|
|
|
src={previewUrl}
|
|
|
|
|
shape="square"
|
2025-02-24 20:53:22 +08:00
|
|
|
|
onError={() => {
|
|
|
|
|
if (value && previewUrl && imageSrc === value) {
|
|
|
|
|
// 当原始图片(value)加载失败时,切换到 previewUrl
|
|
|
|
|
setImageSrc(previewUrl);
|
|
|
|
|
return true; // 阻止默认的 fallback 行为,让它尝试新设置的 src
|
|
|
|
|
}
|
|
|
|
|
return false; // 如果 previewUrl 也失败了,显示默认头像
|
|
|
|
|
}}
|
2025-02-06 16:32:52 +08:00
|
|
|
|
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;
|