import { useEffect, useState, useMemo } from 'react'; import { useParams, Link } from 'react-router-dom'; import { api } from '../../services/api-client'; import { MultiLineChart, type SeriesData } from '../../components/charts/MultiLineChart'; interface PatientDetail { id: string; name: string; phone: string; gender: string; birthday: string; heightCm: number; weightKg: number; medicalHistory: string[]; stentDate: string; stentType: string; } interface HealthRecord { id: string; type: string; value: string; unit: string; recordedAt: string; } interface ExerciseEntry { type: string; duration: number; intensity: string; caloriesBurned: number; date: string; } interface DietEntry { foods: { name: string; amount?: string; calories?: number }[]; mealType: string; totalCalories: number; date: string; } const typeLabels: Record = { blood_pressure: '血压', heart_rate: '心率', blood_sugar: '血糖', spo2: '血氧', }; const typeColors: Record = { blood_pressure: '#EF4444', heart_rate: '#F59E0B', blood_sugar: '#4F6EF7', spo2: '#20C997', }; const typeBgs: Record = { blood_pressure: '#FEE9E9', heart_rate: '#FFF8E6', blood_sugar: '#EDF0FD', spo2: '#E6F9F2', }; const exerciseIcons: Record = { '散步': '🚶', '慢跑': '🏃', '太极拳': '🤸', '游泳': '🏊', '骑自行车': '🚴', '八段锦': '🧘', }; const mealLabels: Record = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐', }; const mealIcons: Record = { breakfast: '🌅', lunch: '☀️', dinner: '🌙', snack: '🍎', }; export function PatientDetailPage() { const { id } = useParams<{ id: string }>(); const [patient, setPatient] = useState(null); const [records, setRecords] = useState([]); const [exercises, setExercises] = useState([]); const [diets, setDiets] = useState([]); useEffect(() => { if (!id) return; api.get(`/api/patients/${id}`).then((r) => { if (r.data) setPatient(r.data); }).catch(() => {}); api.get(`/api/health-records?patientId=${id}&days=30`).then((r) => { const all = r.data || []; setRecords(all.filter((x) => ['blood_pressure', 'heart_rate', 'blood_sugar', 'spo2'].includes(x.type))); // Parse exercise records const exList: ExerciseEntry[] = []; const dietList: DietEntry[] = []; all.filter((x) => x.type === 'exercise' || x.type === 'diet').forEach((r) => { try { const v = JSON.parse(r.value); const date = r.recordedAt?.split('T')[0] || ''; if (r.type === 'exercise') { exList.push({ type: v.type, duration: v.duration, intensity: v.intensity, caloriesBurned: v.caloriesBurned || v.calories, date }); } else { dietList.push({ mealType: v.mealType || v.meal, foods: v.foods || [], totalCalories: v.totalCalories, date }); } } catch { /* skip */ } }); exList.sort((a, b) => b.date.localeCompare(a.date)); dietList.sort((a, b) => b.date.localeCompare(a.date)); setExercises(exList.slice(0, 10)); setDiets(dietList.slice(0, 10)); }).catch(() => {}); }, [id]); if (!patient) return
加载中...
; const latestByType: Record = {}; records.forEach((r) => { if (!latestByType[r.type] || r.recordedAt > latestByType[r.type].recordedAt) { latestByType[r.type] = r; } }); const parseValueDisplay = (r: HealthRecord) => { try { const v = JSON.parse(r.value); if (typeof v === 'object') return `${v.systolic ?? v.value ?? '-'}/${v.diastolic ?? '-'}`; return String(v.value ?? v); } catch { return r.value; } }; return (
← 返回患者列表 {/* Patient info card */}
{patient.name?.charAt(0) || '?'}

{patient.name}

{patient.phone}

{/* Health vitals */}

生命体征

{Object.entries(latestByType).map(([type, record]) => (
{typeLabels[type] || type}
{parseValueDisplay(record)} {record.unit}
{record.recordedAt?.split('T')[0]}
))}
{/* Trend chart */} {/* Exercise + Diet side by side */}
{/* Exercise */}

🏃 运动记录 近7天 · {exercises.reduce((s, e) => s + (e.duration || 0), 0)}分钟

{exercises.length === 0 ? (
暂无运动记录
) : ( exercises.slice(0, 7).map((e, i) => (
{exerciseIcons[e.type] || '💪'}
{e.type}
{e.duration}分钟 · {e.caloriesBurned}kcal
{e.date?.slice(5)}
)) )}
{/* Diet */}

🥗 饮食记录 近7天 · {diets.reduce((s, d) => s + (d.totalCalories || 0), 0)}kcal

{diets.length === 0 ? (
暂无饮食记录
) : ( diets.slice(0, 7).map((d, i) => (
{mealIcons[d.mealType] || '🍽️'}
{d.foods?.map(f => f.name).join('、') || '-'}
{mealLabels[d.mealType] || d.mealType} · {d.totalCalories}kcal
{d.date?.slice(5)}
)) )}
); } const CHART_INDICATORS = [ { type: 'bp_sys', label: '收缩压', color: '#DC4A4A', unit: 'mmHg', source: 'blood_pressure', field: 'systolic' as const }, { type: 'bp_dia', label: '舒张压', color: '#E0558A', unit: 'mmHg', source: 'blood_pressure', field: 'diastolic' as const }, { type: 'heart_rate', label: '心率', color: '#D68B20', unit: 'bpm' }, { type: 'blood_sugar', label: '血糖', color: '#7C5CE7', unit: 'mmol/L' }, { type: 'spo2', label: '血氧', color: '#3B8ED4', unit: '%' }, { type: 'weight', label: '体重', color: '#3DAF86', unit: 'kg' }, ]; function ChartSection({ records }: { records: HealthRecord[] }) { const [visible, setVisible] = useState>(new Set(CHART_INDICATORS.map((i) => i.type))); const series: SeriesData[] = useMemo(() => { return CHART_INDICATORS .filter((ind) => visible.has(ind.type)) .map((ind) => { const source = (ind as Record).source || ind.type; const field = (ind as Record).field; const raw = records .filter((r) => r.type === source) .sort((a, b) => a.recordedAt.localeCompare(b.recordedAt)); const data = raw.map((r) => { try { const v = JSON.parse(r.value); const val = field ? (v[field] ?? 0) : (v.value ?? v); return { date: r.recordedAt.split('T')[0], value: Number(val) || 0 }; } catch { return { date: r.recordedAt.split('T')[0], value: 0 }; } }); return { name: ind.label, color: ind.color, data, unit: ind.unit }; }); }, [records, visible]); const toggle = (type: string) => { const next = new Set(visible); if (next.has(type)) next.delete(type); else next.add(type); setVisible(next); }; const hasData = series.some((s) => s.data.length > 0); if (!hasData) return null; return (

📈 健康趋势

{CHART_INDICATORS.map((ind) => ( ))}
); } function InfoRow({ label, value }: { label: string; value: string }) { return (
{label} {value}
); }