"use client" import { uploadFile } from "@/api/file"; import { updateUser } from "@/api/user"; import { ImageCropper } from "@/components/common/image-cropper"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { useAuth } from "@/contexts/auth-context"; import { getFileUri } from "@/utils/client/file"; import { getGravatarFromUser } from "@/utils/common/gravatar"; import { getFallbackAvatarFromUsername } from "@/utils/common/username"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; interface UploadConstraints { allowedTypes: string[]; maxSize: number; } interface PictureInputChangeEvent { target: HTMLInputElement & { files?: FileList | null }; } export function UserProfilePage() { const t = useTranslations("Console.user_profile") const { user } = useAuth(); const [nickname, setNickname] = useState(user?.nickname || '') const [username, setUsername] = useState(user?.username || '') const [avatarFile, setAvatarFile] = useState(null) const [avatarFileUrl, setAvatarFileUrl] = useState(null) // 这部分交由useEffect控制,监听 avatarFile 变化 const [backgroundFile, setBackgroundFile] = useState(null) const [backgroundFileUrl, setBackgroundFileUrl] = useState(null) const [submitting, setSubmitting] = useState(false) const [gender, setGender] = useState(user?.gender || '') useEffect(() => { if (!user) return; if (!avatarFile) { setAvatarFileUrl(getGravatarFromUser({ user })); return; } const url = URL.createObjectURL(avatarFile); setAvatarFileUrl(url); return () => { URL.revokeObjectURL(url); setAvatarFileUrl(getGravatarFromUser({ user })); }; }, [avatarFile, user]); useEffect(() => { if (!user) return; if (!backgroundFile) { setBackgroundFileUrl(null); return; } const url = URL.createObjectURL(backgroundFile); setBackgroundFileUrl(url); return () => { URL.revokeObjectURL(url); setBackgroundFileUrl(null); }; }, [backgroundFile, user]); const handlePictureSelected = (e: PictureInputChangeEvent): void => { const file: File | null = e.target.files?.[0] ?? null; if (!file) { setAvatarFile(null); return; } const constraints: UploadConstraints = { allowedTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], maxSize: 5 * 1024 * 1024, // 5 MB }; if (!file.type || !file.type.startsWith('image/') || !constraints.allowedTypes.includes(file.type)) { setAvatarFile(null); toast.error(t("only_allow_picture")); return; } if (file.size > constraints.maxSize) { setAvatarFile(null); toast.error(t("picture_size_cannot_exceed", { "size": "5MiB" })); return; } setAvatarFile(file); } const handleBackgroundSelected = (e: PictureInputChangeEvent): void => { const file: File | null = e.target.files?.[0] ?? null; if (!file) { setBackgroundFile(null); return; } const constraints: UploadConstraints = { allowedTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], maxSize: 5 * 1024 * 1024, // 5 MB }; if (!file.type || !file.type.startsWith('image/') || !constraints.allowedTypes.includes(file.type)) { setBackgroundFile(null); toast.error(t("only_allow_picture")); return; } if (file.size > constraints.maxSize) { setBackgroundFile(null); toast.error(t("picture_size_cannot_exceed", { "size": "5MiB" })); return; } setBackgroundFile(file); } const handleSubmit = () => { if (!user) return; if ( nickname.trim() === '' || username.trim() === '' ) { toast.error(t("nickname_and_username_cannot_be_empty")) return } if ( (username.length < 1 || username.length > 20) || (nickname.length < 1 || nickname.length > 20) ) { toast.error(t("nickname_and_username_must_be_between", { "min": 1, "max": 20 })) return } if ( username === user.username && nickname === user.nickname && gender === user.gender && avatarFile === null && backgroundFile === null ) { toast.warning(t("no_changes_made")) return } let avatarUrl = user.avatarUrl; let backgroundUrl = user.backgroundUrl; setSubmitting(true); (async () => { if (avatarFile) { try { const resp = await uploadFile({ file: avatarFile }); avatarUrl = getFileUri(resp.data.id); } catch (error: unknown) { toast.error(`${t("failed_to_upload_avatar")}: ${error}`); return; } } if (backgroundFile) { try { const resp = await uploadFile({ file: backgroundFile }); backgroundUrl = getFileUri(resp.data.id); } catch (error: unknown) { toast.error(`${t("failed_to_upload_background")}: ${error}`); return; } } try { await updateUser({ nickname, username, avatarUrl, backgroundUrl, gender, id: user.id }); window.location.reload(); } catch (error: unknown) { toast.error(`${t("failed_to_update_profile")}: ${error}`); } finally { setSubmitting(false); } })(); } const handleCropped = (blob: Blob) => { const file = new File([blob], 'avatar.png', { type: blob.type }); setAvatarFile(file); } if (!user) return null return (

{t("public_profile")}

{avatarFileUrl ? : } {getFallbackAvatarFromUsername(nickname || username)}
setNickname(e.target.value)} />
setUsername(e.target.value)} />
setGender(e.target.value)} />
) }