'use client'; import * as React from 'react'; import Link from 'next/link'; import { motion, useAnimation } from 'motion/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { useUser } from '@/contexts/user-context'; import { ArrowRight, BookOpen, Camera, Edit, Globe, Info, Link2, Loader2, Lock, Mail, MapPin, Phone, Shield, Unlink, User as UserIcon, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import type { ChangePasswordRequest, UpdateProfileRequest, } from '@/lib/services/auth'; import { AuthService } from '@/lib/services/auth'; import services from '@/lib/services'; import { ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset, } from '@/components/ui/image-crop'; import { toast } from 'sonner'; import { useTranslations } from 'next-intl'; export function ProfileMain() { const { user, loading, refetch } = useUser(); const controls = useAnimation(); const t = useTranslations('settings.profile'); const isAnimatingRef = React.useRef(false); const queryClient = useQueryClient(); // 修改密码 State const [oldPassword, setOldPassword] = React.useState(''); const [newPassword, setNewPassword] = React.useState(''); const [confirmPassword, setConfirmPassword] = React.useState(''); // 编辑个人资料 State const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false); const [nickname, setNickname] = React.useState(''); const [email, setEmail] = React.useState(''); const [bio, setBio] = React.useState(''); const [phone, setPhone] = React.useState(''); const [gender, setGender] = React.useState('secret'); const [website, setWebsite] = React.useState(''); const [location, setLocation] = React.useState(''); const [avatarUrl, setAvatarUrl] = React.useState(''); // 头像裁剪 State const [cropFile, setCropFile] = React.useState(null); const [isCropDialogOpen, setIsCropDialogOpen] = React.useState(false); const fileInputRef = React.useRef(null); // 初始化编辑表单数据 React.useEffect(() => { if (user) { setNickname(user.nickname || ''); setEmail(user.email || ''); setBio(user.bio || ''); setPhone(user.phone || ''); setGender(user.gender || 'secret'); setWebsite(user.website || ''); setLocation(user.location || ''); setAvatarUrl(user.avatar_url || ''); } }, [user, isEditDialogOpen]); const changePasswordMutation = useMutation({ mutationFn: (req: ChangePasswordRequest) => AuthService.changePassword(req), onSuccess: () => { toast.success(t('passwordChangeSuccess')); setOldPassword(''); setNewPassword(''); setConfirmPassword(''); void refetch(); }, onError: (error: Error) => { toast.error(error.message || t('passwordChangeFailed')); }, }); const handlePasswordChange = (e: React.FormEvent) => { e.preventDefault(); if (newPassword !== confirmPassword) { toast.error(t('passwordMismatch')); return; } if (newPassword.length < 8) { toast.error(t('passwordTooShort')); return; } changePasswordMutation.mutate({ old_password: oldPassword, new_password: newPassword, }); }; const updateProfileMutation = useMutation({ mutationFn: (req: UpdateProfileRequest) => AuthService.updateProfile(req), onSuccess: () => { toast.success(t('profileUpdateSuccess')); setIsEditDialogOpen(false); void refetch(); }, onError: (error: Error) => { toast.error(error.message || t('profileUpdateFailed')); }, }); const handleProfileSubmit = (e: React.FormEvent) => { e.preventDefault(); updateProfileMutation.mutate({ nickname, email, avatar_url: avatarUrl, bio, phone, gender, website, location, }); }; const externalAccountBindingsQuery = useQuery({ queryKey: ['auth', 'external-accounts'], queryFn: () => AuthService.getExternalAccountBindings(), }); const publicAuthSourcesQuery = useQuery({ queryKey: ['auth', 'public-sources'], queryFn: () => AuthService.getAuthSources(), }); const bindSourceMutation = useMutation({ mutationFn: async (sourceName: string) => { const { authorize_url } = await AuthService.getAuthorizeUrl( sourceName, 'bind', ); sessionStorage.setItem( 'redirect_after_login', `${window.location.pathname}${window.location.search}`, ); window.location.href = authorize_url; }, onError: (error: Error) => { toast.error(error.message || t('bindSourceFailed')); }, }); const handleAvatarClick = () => { if (isAnimatingRef.current) return; isAnimatingRef.current = true; controls.start({ rotate: [0, -20, 20, -20, 20, 0], transition: { duration: 0.5, ease: 'easeInOut' }, }); setTimeout(() => { isAnimatingRef.current = false; }, 650); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setCropFile(file); setIsCropDialogOpen(true); } }; const handleCroppedImage = async (croppedBase64: string) => { try { const res = await services.upload.uploadBase64Image( croppedBase64, 'avatar', 'avatar.png', ); setAvatarUrl(`/f/${res.id}`); setIsCropDialogOpen(false); setCropFile(null); toast.success(t('avatarUploadSuccess')); } catch (err) { toast.error((err as Error).message || t('avatarUploadFailed')); } }; const getGenderLabel = (g?: string) => { switch (g) { case 'male': return t('genderMale'); case 'female': return t('genderFemale'); case 'secret': return t('genderSecret'); default: return g || t('genderSecret'); } }; if (loading) { return (

{t('title')}

); } if (!user) { return (
{t('userNotFound')}
); } return (
{t('breadcrumbSettings')} {t('breadcrumbProfile')}
{/* 基本资料面板 */}

{t('basicInfo')}

{t('basicInfoDesc')}

{(user.nickname || user.username).slice(0, 2).toUpperCase()}
{t('account')}
@{user.username}
{t('nickname')}
{user.nickname || t('notSet')}
{t('uid')}
{user.id}
{t('email')}
{user.email || t('notBound')}
{t('phone')}
{user.phone || t('notSet')}
{t('gender')}
{getGenderLabel(user.gender)}
{t('website')}
{user.website ? ( {user.website} ) : ( t('notSet') )}
{t('location')}
{user.location || t('notSet')}
{t('isAdmin')}
{user.is_admin ? ( {t('yes')} ) : ( {t('no')} )}
{t('bio')}
{user.bio || t('bioDefault')}
{/* 下方左右并排布局容器 */}
{/* 修改密码面板 */}

{t('changePassword')}

{t('changePasswordDesc')}

{user.need_change_password && (

{t('passwordRiskWarning')}

{t('mustChangePassword')}

)}
setOldPassword(e.target.value)} className='h-8 text-xs rounded-lg' required />
setNewPassword(e.target.value)} className='h-8 text-xs rounded-lg' required />
setConfirmPassword(e.target.value)} className='h-8 text-xs rounded-lg' required />
{/* 账号绑定面板 */}

{t('thirdPartyBinding')}

{t('thirdPartyBindingDesc')}

{/* 已绑定账号列表 */}

{t('boundAccounts')}

{externalAccountBindingsQuery.isPending ? (
) : (externalAccountBindingsQuery.data ?? []).length > 0 ? (
{(externalAccountBindingsQuery.data ?? []).map((binding) => (
{binding.auth_source_label} {binding.external_username || binding.email || t('noAccountIdentifier')}
))}
) : (
{t('noBoundAccounts')}
)}
{/* 绑定新账号列表 */}

{t('bindNewAccount')}

{publicAuthSourcesQuery.isPending ? (
) : (publicAuthSourcesQuery.data ?? []).length > 0 ? (
{(publicAuthSourcesQuery.data ?? []).map((source) => ( ))}
) : (
{t('noAvailableSources')}
)}
{/* 编辑资料 Dialog */} {t('editProfileTitle')} {t('editProfileDesc')}
{/* 上传头像域 */}
fileInputRef.current?.click()} > {(nickname || 'U').slice(0, 2).toUpperCase()}
{t('uploadAvatar')}
setNickname(e.target.value)} className='h-8 text-xs rounded-lg' placeholder={t('nicknamePlaceholder')} required />
setEmail(e.target.value)} className='h-8 text-xs rounded-lg' placeholder={t('emailPlaceholder')} />
setPhone(e.target.value)} className='h-8 text-xs rounded-lg' placeholder={t('phonePlaceholder')} />
setWebsite(e.target.value)} className='h-8 text-xs rounded-lg' placeholder='https://...' />
setLocation(e.target.value)} className='h-8 text-xs rounded-lg' placeholder={t('locationPlaceholder')} />