feat: 添加用户资料中的邮箱和地址字段,并更新相关接口和页面

This commit is contained in:
2026-05-29 01:59:05 +08:00
parent 201f80811a
commit a8409ad76b
6 changed files with 111 additions and 42 deletions

View File

@@ -21,6 +21,8 @@ class User(Document):
name: str = ""
phone: Optional[str] = None
age: Optional[int] = None
email: str = ""
address: str = ""
height: Optional[float] = None # cm
pre_pregnancy_weight: Optional[float] = None # kg
current_weight: Optional[float] = None # kg

View File

@@ -18,6 +18,8 @@ def _user_to_response(user: User) -> UserProfileResponse:
id=str(user.id),
name=user.name,
phone=user.phone,
email=user.email,
address=user.address,
age=user.age,
height=user.height,
pre_pregnancy_weight=user.pre_pregnancy_weight,

View File

@@ -14,6 +14,8 @@ class UserProfileUpdate(BaseModel):
name: Optional[str] = None
age: Optional[int] = None
phone: Optional[str] = None
email: Optional[str] = None
address: Optional[str] = None
height: Optional[float] = None
pre_pregnancy_weight: Optional[float] = None
current_weight: Optional[float] = None
@@ -52,6 +54,8 @@ class UserProfileResponse(BaseModel):
id: str
name: str
phone: Optional[str]
email: str = ""
address: str = ""
age: Optional[int]
height: Optional[float]
pre_pregnancy_weight: Optional[float]

View File

@@ -12,6 +12,8 @@ export interface UserProfile {
name: string;
age: number | null;
phone: string;
email: string;
address: string;
avatar_url: string | null;
lastPeriodDate: string; // YYYY-MM-DD
dueDate: string;
@@ -36,6 +38,8 @@ const DEFAULT_PROFILE: UserProfile = {
name: "",
age: null,
phone: "",
email: "",
address: "",
avatar_url: null,
lastPeriodDate: "",
dueDate: "",
@@ -156,6 +160,8 @@ function mapDtoToProfile(dto: UserProfileDTO): UserProfile {
name: dto.name || "",
age: dto.age,
phone: dto.phone || "",
email: dto.email || "",
address: dto.address || "",
avatar_url: dto.avatar_url || null,
lastPeriodDate: lmpDate,
dueDate,
@@ -180,7 +186,7 @@ interface UserContextValue {
updateUserRemote: (data: Partial<UserProfile>) => Promise<void>;
setToken: (token: string) => void;
setTokenAndUser: (token: string, info: BasicUserInfo) => void;
fetchProfile: () => Promise<void>;
fetchProfile: () => Promise<UserProfileDTO | null>;
logout: () => void;
}
@@ -220,6 +226,8 @@ export function UserProvider({ children }: { children: ReactNode }) {
if (data.name !== undefined) payload.name = data.name;
if (data.age !== undefined) payload.age = data.age;
if (data.phone !== undefined) payload.phone = data.phone;
if (data.email !== undefined) payload.email = data.email;
if (data.address !== undefined) payload.address = data.address;
if (data.height !== undefined) payload.height = data.height;
if (data.prePregnancyWeight !== undefined) payload.pre_pregnancy_weight = data.prePregnancyWeight;
if (data.currentWeight !== undefined) payload.current_weight = data.currentWeight;

View File

@@ -58,8 +58,8 @@ export function ProfilePage() {
name: user.name || "未设置",
age: user.age || 0,
phone: user.phone || "未设置",
email: "",
address: "",
email: user.email || "",
address: user.address || "",
avatar_url: user.avatar_url || null,
lastPeriod: user.lastPeriodDate || "",
dueDate: user.dueDate || "",
@@ -88,24 +88,17 @@ export function ProfilePage() {
};
const handleSave = (field: string) => {
const updated = { ...userInfo, ...(editValues as any) };
const newVal = (editValues as any)[field];
if (newVal === undefined) return;
const updated = { ...userInfo, [field]: newVal };
setUserInfo(updated);
setIsEditing(null);
setEditValues({});
// Sync back to global context + backend
updateUserRemote({
name: updated.name,
age: updated.age,
phone: updated.phone,
lastPeriodDate: updated.lastPeriod,
height: updated.height,
prePregnancyWeight: updated.prePregnancyWeight,
currentWeight: updated.currentWeight,
bloodType: updated.bloodType,
allergies: updated.allergies,
medicalHistory: updated.medicalHistory,
emergencyContact: updated.emergencyContact,
});
// 字段名映射:userInfo key → UserProfile key
const keyMap: Record<string, string> = { lastPeriod: 'lastPeriodDate' };
const profileKey = keyMap[field] || field;
// 只提交修改的属性
updateUserRemote({ [profileKey]: newVal } as any);
};
const handleCancel = () => {
@@ -114,6 +107,29 @@ export function ProfilePage() {
setEmergencySubField(null);
};
// 列表字段编辑(过敏史/既往病史)
const [addingField, setAddingField] = useState<string | null>(null);
const [newItemText, setNewItemText] = useState("");
const handleRemoveItem = (field: 'allergies' | 'medicalHistory', index: number) => {
const list = userInfo[field].filter((_, i) => i !== index);
const placeholder = field === 'allergies' ? ["无"] : ["无重大病史"];
const updated = { ...userInfo, [field]: list.length ? list : placeholder };
setUserInfo(updated);
updateUserRemote({ [field]: list } as any);
};
const handleAddItem = (field: 'allergies' | 'medicalHistory') => {
if (!newItemText.trim()) return;
const cleaned = (userInfo[field][0] === "无" || userInfo[field][0] === "无重大病史")
? [] : userInfo[field];
const updated = { ...userInfo, [field]: [...cleaned, newItemText.trim()] };
setUserInfo(updated);
setNewItemText("");
setAddingField(null);
updateUserRemote({ [field]: updated[field] } as any);
};
// 紧急联系人子字段编辑
const handleEmergencyEdit = (subField: string) => {
setEmergencySubField(subField);
@@ -127,10 +143,11 @@ export function ProfilePage() {
};
// 紧急联系人单行输入组件
function EmergencyInput({ value, onSave, onCancel }: { value: string; onSave: (v: string) => void; onCancel: () => void }) {
function EmergencyInput({ value, onSave, onCancel, inputId }: { value: string; onSave: (v: string) => void; onCancel: () => void; inputId?: string }) {
const [v, setV] = useState(value);
return (
<input
id={inputId}
value={v}
onChange={(e) => setV(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') onSave(v); if (e.key === 'Escape') onCancel(); }}
@@ -443,43 +460,74 @@ export function ProfilePage() {
医疗信息
</h2>
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0">
<div className="space-y-4">
<div>
<div className="text-sm text-gray-500 mb-2">血型</div>
<Badge className="bg-red-100 text-red-700 hover:bg-red-100">
{userInfo.bloodType}
</Badge>
</div>
<InfoItem
label="血型"
value={userInfo.bloodType}
field="bloodType"
/>
<div className="pt-4 space-y-4">
<div>
<div className="text-sm text-gray-500 mb-2">药物过敏史</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 items-center">
{userInfo.allergies.map((allergy, index) => (
<Badge
key={index}
className="bg-morandi-rose-100 text-morandi-rose-400 hover:bg-morandi-rose-100"
>
<Badge key={index} className="bg-morandi-rose-100 text-morandi-rose-400 hover:bg-morandi-rose-100 flex items-center gap-1">
{allergy}
{allergy !== "无" && (
<button onClick={() => handleRemoveItem('allergies', index)} className="w-4 h-4 rounded-full flex items-center justify-center hover:bg-morandi-rose-200">
<X className="w-3 h-3" />
</button>
)}
</Badge>
))}
<button className="px-3 py-1 border border-dashed border-gray-300 rounded-full text-sm text-gray-500 hover:border-morandi-200 hover:text-morandi-500">
+ 添加
</button>
{addingField === 'allergies' ? (
<div className="flex items-center gap-2">
<input
value={newItemText}
onChange={(e) => setNewItemText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleAddItem('allergies'); if (e.key === 'Escape') { setAddingField(null); setNewItemText(""); }}}
placeholder="输入..."
className="px-3 py-1 border border-morandi-200 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-morandi-500"
autoFocus
/>
<button onClick={() => { setAddingField(null); setNewItemText(""); }} className="text-xs text-gray-400 hover:text-gray-600">取消</button>
</div>
) : (
<button onClick={() => setAddingField('allergies')} className="px-3 py-1 border border-dashed border-gray-300 rounded-full text-sm text-gray-500 hover:border-morandi-200 hover:text-morandi-500">
+ 添加
</button>
)}
</div>
</div>
<div>
<div className="text-sm text-gray-500 mb-2">既往病史</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 items-center">
{userInfo.medicalHistory.map((history, index) => (
<Badge
key={index}
className="bg-morandi-50 text-morandi-500 hover:bg-morandi-50"
>
<Badge key={index} className="bg-morandi-50 text-morandi-500 hover:bg-morandi-50 flex items-center gap-1">
{history}
{history !== "无重大病史" && (
<button onClick={() => handleRemoveItem('medicalHistory', index)} className="w-4 h-4 rounded-full flex items-center justify-center hover:bg-morandi-200">
<X className="w-3 h-3" />
</button>
)}
</Badge>
))}
<button className="px-3 py-1 border border-dashed border-gray-300 rounded-full text-sm text-gray-500 hover:border-morandi-200 hover:text-morandi-500">
+ 添加
</button>
{addingField === 'medicalHistory' ? (
<div className="flex items-center gap-2">
<input
value={newItemText}
onChange={(e) => setNewItemText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleAddItem('medicalHistory'); if (e.key === 'Escape') { setAddingField(null); setNewItemText(""); }}}
placeholder="输入..."
className="px-3 py-1 border border-morandi-200 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-morandi-500"
autoFocus
/>
<button onClick={() => { setAddingField(null); setNewItemText(""); }} className="text-xs text-gray-400 hover:text-gray-600">取消</button>
</div>
) : (
<button onClick={() => setAddingField('medicalHistory')} className="px-3 py-1 border border-dashed border-gray-300 rounded-full text-sm text-gray-500 hover:border-morandi-200 hover:text-morandi-500">
+ 添加
</button>
)}
</div>
</div>
</div>
@@ -507,6 +555,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">姓名</div>
{emergencySubField === 'name' ? (
<EmergencyInput
inputId="emg-name"
value={userInfo.emergencyContact.name}
onSave={(v) => handleEmergencySave('name', v)}
onCancel={() => setEmergencySubField(null)}
@@ -535,6 +584,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">关系</div>
{emergencySubField === 'relation' ? (
<EmergencyInput
inputId="emg-relation"
value={userInfo.emergencyContact.relation}
onSave={(v) => handleEmergencySave('relation', v)}
onCancel={() => setEmergencySubField(null)}
@@ -565,6 +615,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">联系电话</div>
{emergencySubField === 'phone' ? (
<EmergencyInput
inputId="emg-phone"
value={userInfo.emergencyContact.phone}
onSave={(v) => handleEmergencySave('phone', v)}
onCancel={() => setEmergencySubField(null)}

View File

@@ -96,6 +96,8 @@ export interface UserProfileDTO {
id: string;
name: string;
phone: string | null;
email: string;
address: string;
age: number | null;
height: number | null;
pre_pregnancy_weight: number | null;