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 = "" name: str = ""
phone: Optional[str] = None phone: Optional[str] = None
age: Optional[int] = None age: Optional[int] = None
email: str = ""
address: str = ""
height: Optional[float] = None # cm height: Optional[float] = None # cm
pre_pregnancy_weight: Optional[float] = None # kg pre_pregnancy_weight: Optional[float] = None # kg
current_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), id=str(user.id),
name=user.name, name=user.name,
phone=user.phone, phone=user.phone,
email=user.email,
address=user.address,
age=user.age, age=user.age,
height=user.height, height=user.height,
pre_pregnancy_weight=user.pre_pregnancy_weight, pre_pregnancy_weight=user.pre_pregnancy_weight,

View File

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

View File

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

View File

@@ -58,8 +58,8 @@ export function ProfilePage() {
name: user.name || "未设置", name: user.name || "未设置",
age: user.age || 0, age: user.age || 0,
phone: user.phone || "未设置", phone: user.phone || "未设置",
email: "", email: user.email || "",
address: "", address: user.address || "",
avatar_url: user.avatar_url || null, avatar_url: user.avatar_url || null,
lastPeriod: user.lastPeriodDate || "", lastPeriod: user.lastPeriodDate || "",
dueDate: user.dueDate || "", dueDate: user.dueDate || "",
@@ -88,24 +88,17 @@ export function ProfilePage() {
}; };
const handleSave = (field: string) => { 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); setUserInfo(updated);
setIsEditing(null); setIsEditing(null);
setEditValues({}); setEditValues({});
// Sync back to global context + backend // 字段名映射:userInfo key → UserProfile key
updateUserRemote({ const keyMap: Record<string, string> = { lastPeriod: 'lastPeriodDate' };
name: updated.name, const profileKey = keyMap[field] || field;
age: updated.age, // 只提交修改的属性
phone: updated.phone, updateUserRemote({ [profileKey]: newVal } as any);
lastPeriodDate: updated.lastPeriod,
height: updated.height,
prePregnancyWeight: updated.prePregnancyWeight,
currentWeight: updated.currentWeight,
bloodType: updated.bloodType,
allergies: updated.allergies,
medicalHistory: updated.medicalHistory,
emergencyContact: updated.emergencyContact,
});
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -114,6 +107,29 @@ export function ProfilePage() {
setEmergencySubField(null); 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) => { const handleEmergencyEdit = (subField: string) => {
setEmergencySubField(subField); 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); const [v, setV] = useState(value);
return ( return (
<input <input
id={inputId}
value={v} value={v}
onChange={(e) => setV(e.target.value)} onChange={(e) => setV(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') onSave(v); if (e.key === 'Escape') onCancel(); }} onKeyDown={(e) => { if (e.key === 'Enter') onSave(v); if (e.key === 'Escape') onCancel(); }}
@@ -443,43 +460,74 @@ export function ProfilePage() {
医疗信息 医疗信息
</h2> </h2>
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0"> <Card className="bg-white rounded-2xl p-5 shadow-sm border-0">
<div className="space-y-4"> <InfoItem
<div> label="血型"
<div className="text-sm text-gray-500 mb-2">血型</div> value={userInfo.bloodType}
<Badge className="bg-red-100 text-red-700 hover:bg-red-100"> field="bloodType"
{userInfo.bloodType} />
</Badge> <div className="pt-4 space-y-4">
</div>
<div> <div>
<div className="text-sm text-gray-500 mb-2">药物过敏史</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) => ( {userInfo.allergies.map((allergy, index) => (
<Badge <Badge key={index} className="bg-morandi-rose-100 text-morandi-rose-400 hover:bg-morandi-rose-100 flex items-center gap-1">
key={index}
className="bg-morandi-rose-100 text-morandi-rose-400 hover:bg-morandi-rose-100"
>
{allergy} {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> </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"> {addingField === 'allergies' ? (
+ 添加 <div className="flex items-center gap-2">
</button> <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>
<div> <div>
<div className="text-sm text-gray-500 mb-2">既往病史</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) => ( {userInfo.medicalHistory.map((history, index) => (
<Badge <Badge key={index} className="bg-morandi-50 text-morandi-500 hover:bg-morandi-50 flex items-center gap-1">
key={index}
className="bg-morandi-50 text-morandi-500 hover:bg-morandi-50"
>
{history} {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> </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"> {addingField === 'medicalHistory' ? (
+ 添加 <div className="flex items-center gap-2">
</button> <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> </div>
</div> </div>
@@ -507,6 +555,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">姓名</div> <div className="text-sm text-gray-500">姓名</div>
{emergencySubField === 'name' ? ( {emergencySubField === 'name' ? (
<EmergencyInput <EmergencyInput
inputId="emg-name"
value={userInfo.emergencyContact.name} value={userInfo.emergencyContact.name}
onSave={(v) => handleEmergencySave('name', v)} onSave={(v) => handleEmergencySave('name', v)}
onCancel={() => setEmergencySubField(null)} onCancel={() => setEmergencySubField(null)}
@@ -535,6 +584,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">关系</div> <div className="text-sm text-gray-500">关系</div>
{emergencySubField === 'relation' ? ( {emergencySubField === 'relation' ? (
<EmergencyInput <EmergencyInput
inputId="emg-relation"
value={userInfo.emergencyContact.relation} value={userInfo.emergencyContact.relation}
onSave={(v) => handleEmergencySave('relation', v)} onSave={(v) => handleEmergencySave('relation', v)}
onCancel={() => setEmergencySubField(null)} onCancel={() => setEmergencySubField(null)}
@@ -565,6 +615,7 @@ export function ProfilePage() {
<div className="text-sm text-gray-500">联系电话</div> <div className="text-sm text-gray-500">联系电话</div>
{emergencySubField === 'phone' ? ( {emergencySubField === 'phone' ? (
<EmergencyInput <EmergencyInput
inputId="emg-phone"
value={userInfo.emergencyContact.phone} value={userInfo.emergencyContact.phone}
onSave={(v) => handleEmergencySave('phone', v)} onSave={(v) => handleEmergencySave('phone', v)}
onCancel={() => setEmergencySubField(null)} onCancel={() => setEmergencySubField(null)}

View File

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