feat: 添加用户资料中的邮箱和地址字段,并更新相关接口和页面
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user