|
|
@@ -0,0 +1,134 @@
|
|
|
+import React, { useState, useRef, ChangeEvent } from 'react';
|
|
|
+import { CameraIcon, PlusIcon, CheckIcon } from '@heroicons/react/24/outline';
|
|
|
+
|
|
|
+interface AvatarUploaderProps {
|
|
|
+ initialAvatar?: string;
|
|
|
+ onUpload?: (file: File) => void;
|
|
|
+}
|
|
|
+
|
|
|
+export const AvatarUploader: React.FC<AvatarUploaderProps> = ({ initialAvatar }) => {
|
|
|
+ const [isUploading, setIsUploading] = useState<boolean>(false);
|
|
|
+ const [preview, setPreview] = useState<string | null>(initialAvatar || null);
|
|
|
+ const [showControls, setShowControls] = useState<boolean>(false);
|
|
|
+ const [uploadSuccess, setUploadSuccess] = useState<boolean>(false);
|
|
|
+ const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
+
|
|
|
+ // 处理文件选择
|
|
|
+ const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
|
+ const file = e.target.files?.[0];
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ // 检查文件类型
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ alert('请选择图片文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小 (限制2MB)
|
|
|
+ if (file.size > 2 * 1024 * 1024) {
|
|
|
+ alert('图片大小不能超过2MB');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示预览
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (event) => {
|
|
|
+ setPreview(event.target?.result as string);
|
|
|
+ // 模拟上传过程
|
|
|
+ simulateUpload(file);
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+
|
|
|
+ // 重置input值,以便可以重复选择同一文件
|
|
|
+ if (e.target) e.target.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 模拟上传过程
|
|
|
+ const simulateUpload = (file: File) => {
|
|
|
+ setIsUploading(true);
|
|
|
+ setUploadSuccess(false);
|
|
|
+
|
|
|
+ // 模拟1.5秒上传时间
|
|
|
+ setTimeout(() => {
|
|
|
+ setIsUploading(false);
|
|
|
+ setUploadSuccess(true);
|
|
|
+
|
|
|
+ // 3秒后隐藏成功提示
|
|
|
+ setTimeout(() => {
|
|
|
+ setUploadSuccess(false);
|
|
|
+ }, 3000);
|
|
|
+ }, 1500);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 触发文件选择对话框
|
|
|
+ const triggerFileSelect = () => {
|
|
|
+ fileInputRef.current?.click();
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className="relative group"
|
|
|
+ onMouseEnter={() => setShowControls(true)}
|
|
|
+ onMouseLeave={() => setShowControls(false)}
|
|
|
+ >
|
|
|
+ {/* 头像预览区域 */}
|
|
|
+ <div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
|
|
|
+ {preview ? (
|
|
|
+ <img
|
|
|
+ src={preview}
|
|
|
+ alt="用户头像"
|
|
|
+ className="h-full w-full object-cover rounded-full transition-transform duration-300 group-hover:scale-105"
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div className="text-center">
|
|
|
+ <CameraIcon className="h-12 w-12 text-gray-400 mx-auto" />
|
|
|
+ <span className="text-xs text-gray-500 mt-1 block">上传头像</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 上传状态指示器 */}
|
|
|
+ {isUploading && (
|
|
|
+ <div className="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
|
|
+ <div className="h-8 w-8 border-4 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 上传成功指示器 */}
|
|
|
+ {uploadSuccess && (
|
|
|
+ <div className="absolute inset-0 bg-green-500 bg-opacity-70 rounded-full flex items-center justify-center">
|
|
|
+ <CheckIcon className="h-10 w-10 text-white" />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 上传控制按钮 - 悬停时显示 */}
|
|
|
+ {(showControls && !isUploading && !uploadSuccess) && (
|
|
|
+ <div className="absolute -bottom-2 -right-2 bg-blue-600 rounded-full p-2 shadow-md cursor-pointer hover:bg-blue-700 transition-colors">
|
|
|
+ <PlusIcon
|
|
|
+ className="h-5 w-5 text-white"
|
|
|
+ onClick={triggerFileSelect}
|
|
|
+ title="更换头像"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 隐藏的文件输入 */}
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ ref={fileInputRef}
|
|
|
+ accept="image/*"
|
|
|
+ className="hidden"
|
|
|
+ onChange={handleFileChange}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 裁剪提示 */}
|
|
|
+ {(preview && !isUploading && !uploadSuccess) && (
|
|
|
+ <p className="text-xs text-gray-500 mt-2 text-center">
|
|
|
+ 点击上传新头像 (建议尺寸: 200x200px)
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default AvatarUploader;
|