Files
ai-showcase-platform/components/avatar-upload.tsx
2025-09-21 22:11:20 +08:00

232 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =====================================================
// 頭像上傳組件
// =====================================================
'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Upload, X, Check, AlertCircle } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { isValidImageType, isValidImageSize } from '@/lib/image-utils';
interface AvatarUploadProps {
userId: string;
currentAvatar?: string;
userName?: string;
onUploadSuccess?: (imageUrl: string) => void;
onUploadError?: (error: string) => void;
}
export function AvatarUpload({
userId,
currentAvatar,
userName,
onUploadSuccess,
onUploadError
}: AvatarUploadProps) {
const [isUploading, setIsUploading] = useState(false);
const [uploadStatus, setUploadStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// 監聽 currentAvatar 變化,清除預覽
useEffect(() => {
if (currentAvatar) {
setPreviewUrl(null);
}
}, [currentAvatar]);
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// 驗證文件類型
if (!isValidImageType(file)) {
setErrorMessage('只支援 JPEG、PNG、WebP 格式的圖片');
setUploadStatus('error');
return;
}
// 驗證文件大小(限制 5MB
if (!isValidImageSize(file, 5)) {
setErrorMessage('圖片大小不能超過 5MB');
setUploadStatus('error');
return;
}
// 創建預覽 URL
const url = URL.createObjectURL(file);
setPreviewUrl(url);
setUploadStatus('idle');
setErrorMessage('');
};
const handleUpload = async () => {
const file = fileInputRef.current?.files?.[0];
if (!file) return;
setIsUploading(true);
setUploadStatus('idle');
setErrorMessage('');
try {
const formData = new FormData();
formData.append('avatar', file);
formData.append('userId', userId);
const response = await fetch('/api/upload/avatar', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (result.success) {
setUploadStatus('success');
setPreviewUrl(null); // 清除預覽,讓組件顯示實際頭像
onUploadSuccess?.(result.data.imageUrl);
console.log('頭像上傳成功:', result.data);
} else {
setUploadStatus('error');
setErrorMessage(result.error || '上傳失敗');
onUploadError?.(result.error || '上傳失敗');
}
} catch (error) {
setUploadStatus('error');
const errorMsg = error instanceof Error ? error.message : '上傳失敗';
setErrorMessage(errorMsg);
onUploadError?.(errorMsg);
} finally {
setIsUploading(false);
}
};
const handleRemoveFile = () => {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
setPreviewUrl(null);
setUploadStatus('idle');
setErrorMessage('');
};
const getDisplayAvatar = () => {
if (previewUrl) return previewUrl;
if (currentAvatar && currentAvatar !== '') return currentAvatar;
return null;
};
const getInitials = () => {
if (userName) {
return userName.split(' ').map(n => n[0]).join('').toUpperCase();
}
return 'U';
};
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
</CardTitle>
<CardDescription>
JPEGPNGWebP 5MB
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 頭像預覽 */}
<div className="flex justify-center">
<Avatar className="h-24 w-24">
<AvatarImage src={getDisplayAvatar() || ''} alt="頭像" />
<AvatarFallback className="text-lg">
{getInitials()}
</AvatarFallback>
</Avatar>
</div>
{/* 文件選擇 */}
<div className="space-y-2">
<Label htmlFor="avatar"></Label>
<Input
id="avatar"
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp"
onChange={handleFileSelect}
ref={fileInputRef}
disabled={isUploading}
/>
</div>
{/* 預覽和操作按鈕 */}
{previewUrl && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<Button
variant="ghost"
size="sm"
onClick={handleRemoveFile}
disabled={isUploading}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="flex justify-center">
<img
src={previewUrl}
alt="預覽"
className="h-16 w-16 rounded-full object-cover border"
/>
</div>
</div>
)}
{/* 上傳按鈕 */}
<Button
onClick={handleUpload}
disabled={!fileInputRef.current?.files?.[0] || isUploading}
className="w-full"
>
{isUploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
...
</>
) : (
<>
<Upload className="h-4 w-4 mr-2" />
</>
)}
</Button>
{/* 狀態提示 */}
{uploadStatus === 'success' && (
<Alert>
<Check className="h-4 w-4" />
<AlertDescription>
</AlertDescription>
</Alert>
)}
{uploadStatus === 'error' && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{errorMessage}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
);
}