132 lines
3.2 KiB
TypeScript
132 lines
3.2 KiB
TypeScript
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";
|
|
|
|
export interface AvatarUploaderProps {
|
|
value?: string;
|
|
placeholder?: string;
|
|
className?: string;
|
|
onChange?: (value: string) => void;
|
|
style?: React.CSSProperties; // 添加style属性
|
|
}
|
|
|
|
interface UploadingFile {
|
|
name: string;
|
|
progress: number;
|
|
status: "uploading" | "done" | "error";
|
|
fileId?: string;
|
|
fileKey?: string;
|
|
}
|
|
|
|
const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
|
value,
|
|
onChange,
|
|
className,
|
|
placeholder = "点击上传",
|
|
style, // 解构style属性
|
|
}) => {
|
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
|
const [file, setFile] = useState<UploadingFile | null>(null);
|
|
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
|
const [uploading, setUploading] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const { token } = theme.useToken();
|
|
|
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFile = event.target.files?.[0];
|
|
if (!selectedFile) return;
|
|
|
|
setFile({
|
|
name: selectedFile.name,
|
|
progress: 0,
|
|
status: "uploading",
|
|
fileKey: `${selectedFile.name}-${Date.now()}`,
|
|
});
|
|
setUploading(true);
|
|
|
|
try {
|
|
const fileId = await new Promise<string>((resolve, reject) => {
|
|
handleFileUpload(
|
|
selectedFile,
|
|
(result) => {
|
|
setFile((prev) => ({
|
|
...prev!,
|
|
progress: 100,
|
|
status: "done",
|
|
fileId: result.fileId,
|
|
}));
|
|
resolve(result.fileId);
|
|
},
|
|
(error) => {
|
|
reject(error);
|
|
},
|
|
file?.fileKey
|
|
);
|
|
});
|
|
setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`);
|
|
onChange?.(fileId);
|
|
message.success("头像上传成功");
|
|
} catch (error) {
|
|
console.error("上传错误:", error);
|
|
message.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 ? (
|
|
<img
|
|
src={previewUrl}
|
|
alt="Avatar"
|
|
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;
|