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:
81
frontend-doctor/src/pages/patients/PatientDetailPage.tsx
Normal file
81
frontend-doctor/src/pages/patients/PatientDetailPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user