Initial commit: HealthManager full-stack health management platform

Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR
Frontend patient: React 19 + TypeScript + Vite (mobile H5)
Frontend doctor: React 19 + TypeScript + Vite (desktop web)
This commit is contained in:
MingNian
2026-05-20 16:18:56 +08:00
commit 435af55c4a
215 changed files with 18595 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { api } from '../../services/api-client';
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;
}
export function PatientDetailPage() {
const { id } = useParams<{ id: string }>();
const [patient, setPatient] = useState<PatientDetail | null>(null);
const [records, setRecords] = useState<HealthRecord[]>([]);
useEffect(() => {
if (!id) return;
// Fetch patient detail directly by ID + health records
api.get<PatientDetail>(`/api/patients/${id}`).then((r) => {
if (r.data) setPatient(r.data);
}).catch(() => {});
api.get<HealthRecord[]>(`/api/health-records?patientId=${id}&days=30`).then((r) => setRecords(r.data));
}, [id]);
if (!patient) return <div style={{ padding: 24 }}>...</div>;
const latestByType: Record<string, HealthRecord> = {};
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 (
<div style={{ padding: 24 }}>
<Link to="/patients" style={{ fontSize: 13, color: '#1976d2' }}> </Link>
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<h2>{patient.name}</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 24px', marginTop: 12, fontSize: 14 }}>
<div>{patient.phone}</div>
<div>{patient.gender || '-'}</div>
<div>{patient.birthday || '-'}</div>
<div>{patient.heightCm}cm / {patient.weightKg}kg</div>
<div>{(patient.medicalHistory || []).join('、') || '-'}</div>
<div>{patient.stentDate || '-'}</div>
<div>{patient.stentType || '-'}</div>
</div>
</div>
<h3 style={{ marginTop: 24, marginBottom: 12 }}></h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
{Object.entries(latestByType).map(([type, record]) => (
<div key={type} style={{ background: '#fff', padding: 16, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<div style={{ fontSize: 12, color: '#888' }}>
{type === 'blood_pressure' ? '血压' : type === 'heart_rate' ? '心率' : type}
</div>
<div style={{ fontSize: 20, fontWeight: 600, marginTop: 4 }}>
{parseValueDisplay(record)} {record.unit}
</div>
<div style={{ fontSize: 11, color: '#bbb', marginTop: 4 }}>
{record.recordedAt?.split('T')[0]}
</div>
</div>
))}
</div>
</div>
);
}