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