"use client" import { useState, useRef, useEffect } from "react" import { Button } from "@/components/ui/button" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Upload, X, Loader2, Crop } from "lucide-react" import ReactCrop, { type Crop as CropType } from "react-image-crop" import "react-image-crop/dist/ReactCrop.css" import imageCompression from "browser-image-compression" import { useDialog } from "@/components/ui/alert-dialog-custom" interface ImageUploadProps { value?: string // The image URL onChange: (imageUrl: string | undefined) => void className?: string } export function ImageUpload({ value, onChange, className }: ImageUploadProps) { const [previewUrl, setPreviewUrl] = useState(undefined) const [isLoading, setIsLoading] = useState(false) const [showCropDialog, setShowCropDialog] = useState(false) const [imageToCrop, setImageToCrop] = useState(null) const [crop, setCrop] = useState() const [completedCrop, setCompletedCrop] = useState() const imgRef = useRef(null) const fileInputRef = useRef(null) const { showAlert } = useDialog() // Load preview if value (URL) exists useEffect(() => { if (value) { setPreviewUrl(value) } else { setPreviewUrl(undefined) } }, [value]) const handleFileSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return try { // Basic validation if (!file.type.startsWith("image/")) { await showAlert("请选择图片文件", "提示") return } // 压缩图片 const options = { maxSizeMB: 1, maxWidthOrHeight: 1024, useWebWorker: true, } const compressedFile = await imageCompression(file, options) // 显示裁剪对话框 const reader = new FileReader() reader.onload = () => { setImageToCrop(reader.result as string) setShowCropDialog(true) } reader.readAsDataURL(compressedFile) } catch (error) { console.error("Failed to process image:", error) await showAlert("图片处理失败", "错误") } finally { // Reset input if (fileInputRef.current) fileInputRef.current.value = "" } } const handleCropComplete = async () => { if (!completedCrop || !imgRef.current) { // 如果没有裁剪,直接保存原图 await saveImage(imageToCrop!) return } try { setIsLoading(true) const canvas = document.createElement('canvas') const scaleX = imgRef.current.naturalWidth / imgRef.current.width const scaleY = imgRef.current.naturalHeight / imgRef.current.height canvas.width = completedCrop.width canvas.height = completedCrop.height const ctx = canvas.getContext('2d') if (ctx) { ctx.drawImage( imgRef.current, completedCrop.x * scaleX, completedCrop.y * scaleY, completedCrop.width * scaleX, completedCrop.height * scaleY, 0, 0, completedCrop.width, completedCrop.height ) } canvas.toBlob(async (blob) => { if (blob) { await saveImage(URL.createObjectURL(blob), blob) } }, 'image/jpeg', 0.9) } catch (error) { console.error("Failed to crop image:", error) await showAlert("图片裁剪失败", "错误") } finally { setIsLoading(false) setShowCropDialog(false) setImageToCrop(null) } } const saveImage = async (dataUrl: string, blob?: Blob) => { try { setIsLoading(true) let imageBlob = blob if (!imageBlob) { const response = await fetch(dataUrl) imageBlob = await response.blob() } // 上传到服务器 const formData = new FormData() formData.append('file', imageBlob, 'avatar.jpg') const response = await fetch('/api/upload', { method: 'POST', body: formData, }) if (!response.ok) { const error = await response.json() throw new Error(error.error || '上传失败') } const { url } = await response.json() onChange(url) setShowCropDialog(false) setImageToCrop(null) } catch (error) { console.error("Failed to save image:", error) await showAlert("图片保存失败: " + (error instanceof Error ? error.message : '未知错误'), "错误") } finally { setIsLoading(false) } } const handleRemove = () => { onChange(undefined) } return ( <>
{isLoading ? : }
{value && ( )}
{/* 裁剪对话框 */} 裁剪图片
{imageToCrop && ( setCrop(c)} onComplete={(c) => setCompletedCrop(c)} aspect={1} circularCrop > 待裁剪图片 )}
) }