import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react"; import { getToken, setToken as saveToken, clearToken, getUserProfile, updateUserProfile, type UserProfileDTO, } from "../services/api"; 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; currentWeek: number; currentDay: number; isFirstPregnancy: boolean; height: number | null; prePregnancyWeight: number | null; currentWeight: number | null; bloodType: string; allergies: string[]; medicalHistory: string[]; emergencyContact: { name: string; relation: string; phone: string; }; isOnboarded: boolean; } const DEFAULT_PROFILE: UserProfile = { name: "", age: null, phone: "", email: "", address: "", avatar_url: null, lastPeriodDate: "", dueDate: "", currentWeek: 0, currentDay: 0, isFirstPregnancy: true, height: null, prePregnancyWeight: null, currentWeight: null, bloodType: "", allergies: [], medicalHistory: [], emergencyContact: { name: "", relation: "", phone: "" }, isOnboarded: false, }; const STORAGE_KEY = "pregnancy_user_profile"; const USER_BASIC_KEY = "user_basic"; export interface BasicUserInfo { id: string; openid: string; phone: string; } function loadProfile(): UserProfile { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const saved = JSON.parse(raw) as UserProfile; if (saved.lastPeriodDate) { const { weeks, days } = calcPregnancyWeek(saved.lastPeriodDate); saved.currentWeek = weeks; saved.currentDay = days; } return saved; } } catch {} return DEFAULT_PROFILE; } function saveProfile(profile: UserProfile) { localStorage.setItem(STORAGE_KEY, JSON.stringify(profile)); } export function calcPregnancyWeek(lastPeriodDate: string) { const lmp = new Date(lastPeriodDate); const now = new Date(); const diffDays = Math.floor((now.getTime() - lmp.getTime()) / (24 * 60 * 60 * 1000)); return { weeks: Math.max(0, Math.floor(diffDays / 7)), days: Math.max(0, diffDays % 7) }; } export function calcDueDate(lastPeriodDate: string): string { const lmp = new Date(lastPeriodDate); const due = new Date(lmp.getTime() + 280 * 24 * 60 * 60 * 1000); return due.toISOString().split("T")[0]; } export function getPregnancyStage(week: number): string { if (week <= 12) return "孕早期"; if (week <= 28) return "孕中期"; return "孕晚期"; } // 基于WHO标准,4-19周为头臀长,20周起为全身长 export function getBabySize(week: number): string { const sizes: Record = { 4: "约0.1厘米,像一粒罂粟种子", 5: "约0.2厘米,像一粒芝麻", 6: "约0.6厘米,像一粒小扁豆", 7: "约1.3厘米,像一颗蓝莓", 8: "约1.6厘米,像一颗覆盆子", 9: "约2.3厘米,像一颗葡萄", 10: "约3.1厘米,像一颗金橘", 11: "约4.1厘米,像一颗无花果", 12: "约5.4厘米,像一颗青柠", 13: "约7.4厘米,像一颗柠檬", 14: "约8.7厘米,像一颗桃子", 16: "约11.6厘米,像一个牛油果", 18: "约14.2厘米,像一个甜椒", 20: "约25.6厘米,像一根香蕉", 22: "约27.8厘米,像一个木瓜", 24: "约30厘米,像一穗玉米", 26: "约35.6厘米,像一颗菜花", 28: "约37.6厘米,像一颗茄子", 30: "约39.9厘米,像一颗卷心菜", 32: "约42.4厘米,像一个椰子", 34: "约45厘米,像一颗哈密瓜", 36: "约47.4厘米,像一颗蜜瓜", 38: "约49.8厘米,像一颗南瓜", 40: "约51.2厘米,像一颗小西瓜", }; const weeks = Object.keys(sizes).map(Number).sort((a, b) => a - b); let closest = weeks[0]; for (const w of weeks) { if (w <= week) closest = w; else break; } return sizes[closest] || `约${week}周`; } /** 将后端 DTO 映射为前端 UserProfile */ function mapDtoToProfile(dto: UserProfileDTO): UserProfile { const lmpDate = dto.last_period_date ? dto.last_period_date.split("T")[0] : ""; let currentWeek = dto.pregnancy_week || 0; let currentDay = dto.pregnancy_day || 0; let dueDate = dto.due_date ? dto.due_date.split("T")[0] : ""; // 如果有 LMP 日期,实时计算孕周和预产期 if (lmpDate) { const calc = calcPregnancyWeek(lmpDate); currentWeek = calc.weeks; currentDay = calc.days; dueDate = calcDueDate(lmpDate); } return { name: dto.name || "", age: dto.age, phone: dto.phone || "", email: dto.email || "", address: dto.address || "", avatar_url: dto.avatar_url || null, lastPeriodDate: lmpDate, dueDate, currentWeek, currentDay, isFirstPregnancy: true, height: dto.height, prePregnancyWeight: dto.pre_pregnancy_weight, currentWeight: dto.current_weight, bloodType: dto.blood_type || "", allergies: dto.allergies || [], medicalHistory: dto.medical_history || [], emergencyContact: dto.emergency_contact || { name: "", relation: "", phone: "" }, isOnboarded: dto.is_onboarded, }; } interface UserContextValue { user: UserProfile; token: string | null; updateUser: (data: Partial) => void; updateUserRemote: (data: Partial) => Promise; setToken: (token: string) => void; setTokenAndUser: (token: string, info: BasicUserInfo) => void; fetchProfile: () => Promise; logout: () => void; } const UserContext = createContext(null); export function UserProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(loadProfile); const [token, setTokenState] = useState(getToken); const setToken = useCallback((newToken: string) => { // 清理前一个用户的本地数据,防止用户间数据残留 localStorage.removeItem(STORAGE_KEY); setUser(DEFAULT_PROFILE); saveToken(newToken); setTokenState(newToken); }, []); const updateUser = useCallback((data: Partial) => { setUser((prev) => { const updated = { ...prev, ...data }; if (data.lastPeriodDate) { const { weeks, days } = calcPregnancyWeek(data.lastPeriodDate); updated.currentWeek = weeks; updated.currentDay = days; updated.dueDate = calcDueDate(data.lastPeriodDate); } saveProfile(updated); return updated; }); }, []); const updateUserRemote = useCallback(async (data: Partial) => { // 乐观更新本地 updateUser(data); // 同步到后端(camelCase → snake_case) const payload: Record = {}; 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; if (data.bloodType !== undefined) payload.blood_type = data.bloodType; if (data.allergies !== undefined) payload.allergies = data.allergies; if (data.medicalHistory !== undefined) payload.medical_history = data.medicalHistory; if (data.lastPeriodDate !== undefined) payload.last_period_date = data.lastPeriodDate || null; if (data.emergencyContact !== undefined) payload.emergency_contact = data.emergencyContact; try { await updateUserProfile(payload); } catch (e) { console.error("同步用户数据到后端失败:", e); } }, [updateUser]); const fetchProfile = useCallback(async (): Promise => { try { const dto = await getUserProfile(); const profile = mapDtoToProfile(dto); setUser(profile); saveProfile(profile); return dto; } catch { // Token 失效会被 api.ts 自动处理(跳转登录页) return null; } }, []); const setTokenAndUser = useCallback((newToken: string, info: BasicUserInfo) => { // 清理前一个用户的本地数据 localStorage.removeItem(STORAGE_KEY); setUser(DEFAULT_PROFILE); saveToken(newToken); localStorage.setItem(USER_BASIC_KEY, JSON.stringify(info)); setTokenState(newToken); }, []); const logout = useCallback(() => { clearToken(); localStorage.removeItem(STORAGE_KEY); setTokenState(null); setUser(DEFAULT_PROFILE); }, []); // 启动时如果有 token,自动拉取后端数据 useEffect(() => { if (token) { fetchProfile(); } }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( {children} ); } export function useUser() { const ctx = useContext(UserContext); if (!ctx) throw new Error("useUser must be used within UserProvider"); return ctx; }