|
|
@ -6,6 +6,8 @@ import { toast } from 'sonner' |
|
|
|
import { Button } from '@/components/ui/button' |
|
|
|
import { userService } from '@/services/user.service' |
|
|
|
import { useAuthStore } from '@/store/auth' |
|
|
|
import { useTranslation } from 'react-i18next' |
|
|
|
import { handleApiError } from '@/lib/error-handler' |
|
|
|
|
|
|
|
const profileSchema = z.object({ |
|
|
|
name: z.string().min(2, '姓名至少需要2个字符'), |
|
|
@ -26,7 +28,8 @@ type ProfileFormData = z.infer<typeof profileSchema> |
|
|
|
type PasswordFormData = z.infer<typeof passwordSchema> |
|
|
|
|
|
|
|
export function Profile() { |
|
|
|
const { user, login } = useAuthStore() |
|
|
|
const { t } = useTranslation() |
|
|
|
const { user, login, updateUser } = useAuthStore() |
|
|
|
const [isLoading, setIsLoading] = useState(true) |
|
|
|
const [avatar, setAvatar] = useState<string>() |
|
|
|
|
|
|
@ -36,6 +39,11 @@ export function Profile() { |
|
|
|
formState: { errors: profileErrors, isSubmitting: isProfileSubmitting }, |
|
|
|
} = useForm<ProfileFormData>({ |
|
|
|
resolver: zodResolver(profileSchema), |
|
|
|
defaultValues: { |
|
|
|
name: user?.name, |
|
|
|
phone: user?.phone, |
|
|
|
address: user?.address, |
|
|
|
}, |
|
|
|
}) |
|
|
|
|
|
|
|
const { |
|
|
@ -71,10 +79,10 @@ export function Profile() { |
|
|
|
const handleProfileUpdate = async (data: ProfileFormData) => { |
|
|
|
try { |
|
|
|
const updatedProfile = await userService.updateProfile(data) |
|
|
|
login(updatedProfile, useAuthStore.getState().token!) |
|
|
|
toast.success('个人资料更新成功') |
|
|
|
updateUser(updatedProfile) |
|
|
|
toast.success(t('user.profileUpdateSuccess')) |
|
|
|
} catch (error) { |
|
|
|
toast.error('更新个人资料失败') |
|
|
|
handleApiError(error) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -110,88 +118,88 @@ export function Profile() { |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className="container max-w-2xl py-8"> |
|
|
|
<h1 className="mb-8 text-2xl font-bold">个人资料</h1> |
|
|
|
|
|
|
|
<div className="container mx-auto max-w-2xl py-8"> |
|
|
|
<div className="space-y-8"> |
|
|
|
{/* 头像上传 */} |
|
|
|
<div className="flex items-center space-x-4"> |
|
|
|
<div className="relative h-24 w-24 overflow-hidden rounded-full"> |
|
|
|
<img |
|
|
|
src={avatar || `https://ui-avatars.com/api/?name=${user?.name}`} |
|
|
|
alt="头像" |
|
|
|
className="h-full w-full object-cover" |
|
|
|
<div> |
|
|
|
<h1 className="text-2xl font-bold">{t('user.profile')}</h1> |
|
|
|
<p className="text-muted-foreground"> |
|
|
|
{t('user.profileDescription')} |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
<form onSubmit={handleProfileSubmit(handleProfileUpdate)} className="space-y-6"> |
|
|
|
<div className="space-y-2"> |
|
|
|
<label |
|
|
|
htmlFor="name" |
|
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
|
|
|
> |
|
|
|
{t('user.name')} |
|
|
|
</label> |
|
|
|
<input |
|
|
|
id="name" |
|
|
|
type="text" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('name')} |
|
|
|
/> |
|
|
|
{profileErrors.name && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.name.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
<div> |
|
|
|
<div className="space-y-2"> |
|
|
|
<label |
|
|
|
htmlFor="email" |
|
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
|
|
|
> |
|
|
|
{t('user.email')} |
|
|
|
</label> |
|
|
|
<input |
|
|
|
type="file" |
|
|
|
accept="image/*" |
|
|
|
onChange={handleAvatarChange} |
|
|
|
className="hidden" |
|
|
|
id="avatar-upload" |
|
|
|
id="email" |
|
|
|
type="email" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('email')} |
|
|
|
/> |
|
|
|
<label htmlFor="avatar-upload"> |
|
|
|
<Button variant="outline" asChild> |
|
|
|
<span>更换头像</span> |
|
|
|
</Button> |
|
|
|
{profileErrors.email && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.email.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
<div className="space-y-2"> |
|
|
|
<label |
|
|
|
htmlFor="phone" |
|
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
|
|
|
> |
|
|
|
{t('user.phone')} |
|
|
|
</label> |
|
|
|
<input |
|
|
|
id="phone" |
|
|
|
type="tel" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('phone')} |
|
|
|
/> |
|
|
|
{profileErrors.phone && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.phone.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 基本信息表单 */} |
|
|
|
<form onSubmit={handleProfileSubmit(handleProfileUpdate)} className="space-y-6"> |
|
|
|
<h2 className="text-xl font-semibold">基本信息</h2> |
|
|
|
<div className="space-y-4"> |
|
|
|
<div className="space-y-2"> |
|
|
|
<label className="text-sm font-medium">姓名</label> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('name')} |
|
|
|
/> |
|
|
|
{profileErrors.name && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.name.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="space-y-2"> |
|
|
|
<label className="text-sm font-medium">邮箱</label> |
|
|
|
<input |
|
|
|
type="email" |
|
|
|
value={user?.email} |
|
|
|
disabled |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-muted px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="space-y-2"> |
|
|
|
<label className="text-sm font-medium">手机号码</label> |
|
|
|
<input |
|
|
|
type="tel" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('phone')} |
|
|
|
/> |
|
|
|
{profileErrors.phone && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.phone.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="space-y-2"> |
|
|
|
<label className="text-sm font-medium">地址</label> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('address')} |
|
|
|
/> |
|
|
|
{profileErrors.address && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.address.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
<div className="space-y-2"> |
|
|
|
<label |
|
|
|
htmlFor="address" |
|
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" |
|
|
|
> |
|
|
|
{t('user.address')} |
|
|
|
</label> |
|
|
|
<input |
|
|
|
id="address" |
|
|
|
type="text" |
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
{...registerProfile('address')} |
|
|
|
/> |
|
|
|
{profileErrors.address && ( |
|
|
|
<p className="text-sm text-destructive">{profileErrors.address.message}</p> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<Button type="submit" disabled={isProfileSubmitting}> |
|
|
|
{isProfileSubmitting ? '保存中...' : '保存修改'} |
|
|
|
<Button |
|
|
|
type="submit" |
|
|
|
disabled={isProfileSubmitting} |
|
|
|
> |
|
|
|
{isProfileSubmitting ? t('common.loading') : t('user.saveChanges')} |
|
|
|
</Button> |
|
|
|
</form> |
|
|
|
|
|
|
@ -243,7 +251,7 @@ export function Profile() { |
|
|
|
</div> |
|
|
|
|
|
|
|
<Button type="submit" disabled={isPasswordSubmitting}> |
|
|
|
{isPasswordSubmitting ? '修改中...' : '修改密码'} |
|
|
|
{isPasswordSubmitting ? t('common.loading') : t('user.changePassword')} |
|
|
|
</Button> |
|
|
|
</form> |
|
|
|
</div> |