2025-12-15 11:31:18 +00:00
|
|
|
|
'use client';
|
2025-11-13 08:38:25 +00:00
|
|
|
|
|
2025-12-15 11:31:18 +00:00
|
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
|
|
import { IconButton, Button } from '@/components/ui/button';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Form,
|
|
|
|
|
|
FormControl,
|
|
|
|
|
|
FormField,
|
|
|
|
|
|
FormItem,
|
|
|
|
|
|
FormLabel,
|
|
|
|
|
|
FormMessage,
|
|
|
|
|
|
} from '@/components/ui/form';
|
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
|
import GenderInput from '@/components/features/genderInput';
|
|
|
|
|
|
import { Gender } from '@/types/user';
|
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
import { useCheckNickname, useCurrentUser, useUpdateUser } from '@/hooks/auth';
|
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
|
import * as z from 'zod';
|
|
|
|
|
|
import { calculateAge } from '@/lib/utils';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2025-11-28 06:31:36 +00:00
|
|
|
|
|
2025-12-15 11:31:18 +00:00
|
|
|
|
const schema = z
|
|
|
|
|
|
.object({
|
|
|
|
|
|
nickname: z
|
|
|
|
|
|
.string()
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.min(1, 'Nickname is required')
|
|
|
|
|
|
.min(2, 'Nickname must be between 2 and 20 characters'),
|
|
|
|
|
|
gender: z.enum(Gender, { message: 'Please select a gender' }),
|
|
|
|
|
|
year: z.string().min(1, 'Please select birth year'),
|
|
|
|
|
|
month: z.string().min(1, 'Please select birth month'),
|
|
|
|
|
|
day: z.string().min(1, 'Please select birth day'),
|
|
|
|
|
|
})
|
|
|
|
|
|
.refine(
|
|
|
|
|
|
(data) => {
|
|
|
|
|
|
const age = calculateAge(data.year, data.month, data.day);
|
|
|
|
|
|
return age >= 18;
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
message: 'Character age must be at least 18 years old',
|
|
|
|
|
|
path: ['year'],
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
type EditProfileFormData = z.infer<typeof schema>;
|
|
|
|
|
|
|
|
|
|
|
|
const EditPage = () => {
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { data: user, isLoading } = useCurrentUser();
|
|
|
|
|
|
const { mutateAsync: updateUser } = useUpdateUser();
|
|
|
|
|
|
const { mutateAsync: checkNickname } = useCheckNickname({
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
form.setError('nickname', {
|
|
|
|
|
|
message: error.errorMsg,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户生日,如果是时间戳则转换为Date对象
|
|
|
|
|
|
const getUserBirthday = () => {
|
|
|
|
|
|
if (!user?.birthday) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return new Date(user.birthday);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const userBirthday = getUserBirthday();
|
|
|
|
|
|
|
|
|
|
|
|
const form = useForm<EditProfileFormData>({
|
|
|
|
|
|
resolver: zodResolver(schema),
|
|
|
|
|
|
defaultValues: {
|
|
|
|
|
|
nickname: user?.nickname || '',
|
|
|
|
|
|
gender: user?.sex,
|
|
|
|
|
|
year: userBirthday ? userBirthday.getFullYear().toString() : '',
|
|
|
|
|
|
month: userBirthday ? (userBirthday.getMonth() + 1).toString().padStart(2, '0') : '',
|
|
|
|
|
|
day: userBirthday ? userBirthday.getDate().toString().padStart(2, '0') : '',
|
|
|
|
|
|
},
|
|
|
|
|
|
mode: 'onChange',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
formState: { isValid, isDirty },
|
|
|
|
|
|
} = form;
|
|
|
|
|
|
|
|
|
|
|
|
// 当用户数据加载完成后,更新表单值
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (user && !isLoading) {
|
|
|
|
|
|
const userBirthday = getUserBirthday();
|
|
|
|
|
|
|
|
|
|
|
|
form.reset({
|
|
|
|
|
|
nickname: user.nickname || '',
|
|
|
|
|
|
gender: user.sex,
|
|
|
|
|
|
year: userBirthday ? userBirthday.getFullYear().toString() : '',
|
|
|
|
|
|
month: userBirthday ? (userBirthday.getMonth() + 1).toString().padStart(2, '0') : '',
|
|
|
|
|
|
day: userBirthday ? userBirthday.getDate().toString().padStart(2, '0') : '',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [user, isLoading, form]);
|
|
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: EditProfileFormData) => {
|
|
|
|
|
|
if (!user?.userId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isExist = await checkNickname({
|
|
|
|
|
|
nickname: data.nickname.trim(),
|
|
|
|
|
|
exUserId: user.userId,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (isExist) {
|
|
|
|
|
|
form.setError('nickname', {
|
|
|
|
|
|
message: 'This nickname is already taken',
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 将日期字符串转换为时间戳
|
|
|
|
|
|
const birthdayDate = new Date(`${data.year}-${data.month}-${data.day}`);
|
|
|
|
|
|
const birthdayTimestamp = birthdayDate.getTime();
|
|
|
|
|
|
|
|
|
|
|
|
await updateUser({
|
|
|
|
|
|
nickname: data.nickname,
|
|
|
|
|
|
birthday: birthdayTimestamp,
|
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
|
});
|
|
|
|
|
|
router.push('/profile');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 生成年份选项 (当前年份往前100年)
|
|
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
|
|
const years = Array.from({ length: 100 }, (_, i) => currentYear - i).map((year) => ({
|
|
|
|
|
|
value: year.toString(),
|
|
|
|
|
|
label: year.toString(),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'));
|
|
|
|
|
|
// 生成月份选项
|
|
|
|
|
|
const months = Array.from({ length: 12 }, (_, i) => ({
|
|
|
|
|
|
value: (i + 1).toString().padStart(2, '0'),
|
|
|
|
|
|
label: monthTexts[i],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 根据年份和月份计算该月的天数
|
|
|
|
|
|
const getDaysInMonth = (year: string, month: string): number => {
|
|
|
|
|
|
if (!year || !month) return 31; // 默认返回31天
|
|
|
|
|
|
|
|
|
|
|
|
const yearNum = parseInt(year);
|
|
|
|
|
|
const monthNum = parseInt(month);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Date 对象计算该月的实际天数
|
|
|
|
|
|
// 传入下个月的第0天,会返回上个月的最后一天
|
|
|
|
|
|
return new Date(yearNum, monthNum, 0).getDate();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听年份和月份变化,动态生成日期选项
|
|
|
|
|
|
const selectedYear = form.watch('year');
|
|
|
|
|
|
const selectedMonth = form.watch('month');
|
|
|
|
|
|
const selectedDay = form.watch('day');
|
|
|
|
|
|
|
|
|
|
|
|
const daysInMonth = getDaysInMonth(selectedYear, selectedMonth);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成日期选项,根据选中的年月动态调整
|
|
|
|
|
|
const days = Array.from({ length: daysInMonth }, (_, i) => ({
|
|
|
|
|
|
value: (i + 1).toString().padStart(2, '0'),
|
|
|
|
|
|
label: (i + 1).toString(),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 当年份或月份变化时,检查并调整日期
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedYear && selectedMonth && selectedDay) {
|
|
|
|
|
|
const currentDay = parseInt(selectedDay);
|
|
|
|
|
|
const maxDay = getDaysInMonth(selectedYear, selectedMonth);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前选中的日期超出了该月的最大天数,自动调整为该月最后一天
|
|
|
|
|
|
if (currentDay > maxDay) {
|
|
|
|
|
|
form.setValue('day', maxDay.toString().padStart(2, '0'), { shouldValidate: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedYear, selectedMonth, selectedDay, form]);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mx-auto px-4 max-w-[752px] pt-6 pb-6">
|
|
|
|
|
|
{/* 标题栏 */}
|
|
|
|
|
|
<div className="mb-6 flex items-center gap-2">
|
|
|
|
|
|
<IconButton variant="ghost" size="large" onClick={handleBack} className="p-2">
|
|
|
|
|
|
<i className="iconfont icon-arrow-left !text-[16px]" />
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<h1 className="txt-title-l text-txt-primary-normal">Edit Profile</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表单容器 */}
|
|
|
|
|
|
<div className="bg-surface-base-normal rounded-2xl p-6">
|
|
|
|
|
|
<Form {...form}>
|
|
|
|
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
|
|
|
|
{/* 昵称字段 */}
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="nickname"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormItem>
|
|
|
|
|
|
<FormLabel className="txt-label-m text-txt-primary-normal">Nickname</FormLabel>
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="Enter nickname"
|
|
|
|
|
|
maxLength={20}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
error={!!form.formState.errors.nickname}
|
|
|
|
|
|
{...field}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
<FormMessage />
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 性别字段 */}
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="gender"
|
|
|
|
|
|
disabled={true}
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormItem>
|
|
|
|
|
|
<FormLabel className="txt-label-m text-txt-primary-normal">Gender</FormLabel>
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<GenderInput
|
|
|
|
|
|
value={field.value as Gender}
|
|
|
|
|
|
onChange={field.onChange}
|
|
|
|
|
|
disabled={true}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
<p className="txt-body-s text-txt-secondary-normal">
|
|
|
|
|
|
Please note: gender cannot be changed after setting
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<FormMessage />
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 年龄字段 */}
|
|
|
|
|
|
<FormItem>
|
|
|
|
|
|
<FormLabel className="txt-label-m text-txt-primary-normal">Age</FormLabel>
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-2">
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="year"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
|
|
|
|
<SelectTrigger block error={!!form.formState.errors.year}>
|
|
|
|
|
|
<SelectValue placeholder="Year" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{years.map((year) => (
|
|
|
|
|
|
<SelectItem key={`${year.value}`} value={`${year.value}`}>
|
|
|
|
|
|
{year.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="month"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
|
|
|
|
<SelectTrigger block error={!!form.formState.errors.year}>
|
|
|
|
|
|
<SelectValue placeholder="Month" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{months.map((month) => (
|
|
|
|
|
|
<SelectItem key={month.value} value={month.value}>
|
|
|
|
|
|
{month.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
|
control={form.control}
|
|
|
|
|
|
name="day"
|
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
|
<FormControl>
|
|
|
|
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
|
|
|
|
<SelectTrigger block error={!!form.formState.errors.year}>
|
|
|
|
|
|
<SelectValue placeholder="Day" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{days.map((day) => (
|
|
|
|
|
|
<SelectItem key={day.value} value={day.value}>
|
|
|
|
|
|
{day.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</FormControl>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<FormMessage>
|
|
|
|
|
|
{form.formState.errors.year?.message ||
|
|
|
|
|
|
form.formState.errors.month?.message ||
|
|
|
|
|
|
form.formState.errors.day?.message}
|
|
|
|
|
|
</FormMessage>
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 保存按钮 */}
|
|
|
|
|
|
<div className="flex justify-end">
|
|
|
|
|
|
<Button type="submit" size="large" disabled={!isValid || !isDirty} loading={loading}>
|
|
|
|
|
|
Save
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default EditPage;
|