feat: extract secrets to .env, remove hardcoded credentials
- Backend: .env file for DB/JWT/Redis/MinIO config, appsettings.json cleared - Backend: Program.cs loads .env at startup (no extra NuGet packages) - Frontend: .env files for VITE_API_URL, api-clients use import.meta.env - Added vite-env.d.ts type declarations for both frontends - All hardcoded localhost:5000 replaced with env variable - Added .env.example template for onboarding
This commit is contained in:
@@ -12,6 +12,18 @@ interface HealthRecord {
|
||||
id: string; type: string; value: string; unit: string; recordedAt: string;
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
blood_pressure: '血压', heart_rate: '心率', blood_sugar: '血糖', spo2: '血氧',
|
||||
};
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
blood_pressure: '#EF4444', heart_rate: '#F59E0B', blood_sugar: '#4F6EF7', spo2: '#20C997',
|
||||
};
|
||||
|
||||
const typeBgs: Record<string, string> = {
|
||||
blood_pressure: '#FEE9E9', heart_rate: '#FFF8E6', blood_sugar: '#EDF0FD', spo2: '#E6F9F2',
|
||||
};
|
||||
|
||||
export function PatientDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [patient, setPatient] = useState<PatientDetail | null>(null);
|
||||
@@ -19,14 +31,13 @@ export function PatientDetailPage() {
|
||||
|
||||
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>;
|
||||
if (!patient) return <div style={{ padding: 28, color: '#9BA0B4' }}>加载中...</div>;
|
||||
|
||||
const latestByType: Record<string, HealthRecord> = {};
|
||||
records.forEach((r) => {
|
||||
@@ -44,34 +55,79 @@ export function PatientDetailPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Link to="/patients" style={{ fontSize: 13, color: '#1976d2' }}>← 返回患者列表</Link>
|
||||
<div style={{ padding: 28 }}>
|
||||
<Link to="/patients" style={{ fontSize: 13, color: '#4F6EF7', fontWeight: 500 }}>← 返回患者列表</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 style={{ background: '#fff', marginTop: 16, padding: 28, borderRadius: 16, boxShadow: '0 2px 12px rgba(0,0,0,0.04)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 20 }}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, #4F6EF7, #6C8AFF)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, fontWeight: 700, color: '#fff',
|
||||
}}>
|
||||
{patient.name?.charAt(0) || '?'}
|
||||
</div>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{patient.name}</h2>
|
||||
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#9BA0B4' }}>{patient.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 32px', fontSize: 13 }}>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>手机号</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.phone}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>性别</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.gender || '-'}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>出生日期</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.birthday || '-'}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>身高/体重</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.heightCm}cm / {patient.weightKg}kg</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>病史</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{(patient.medicalHistory || []).join('、') || '-'}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>支架日期</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.stentDate || '-'}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0', borderBottom: '1px solid #F5F6F9' }}>
|
||||
<span style={{ color: '#9BA0B4' }}>支架类型</span>
|
||||
<span style={{ marginLeft: 16, color: '#1A1D28' }}>{patient.stentType || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style={{ marginTop: 24, marginBottom: 12 }}>最近健康数据</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
|
||||
<h3 style={{ marginTop: 28, marginBottom: 14, fontSize: 17, fontWeight: 700, color: '#1A1D28' }}>最近健康数据</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 14 }}>
|
||||
{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 key={type} style={{
|
||||
background: '#fff', padding: 20, borderRadius: 16,
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.04)', position: 'relative',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: 4, height: '100%', background: typeColors[type] || '#4F6EF7', borderRadius: '4px 0 0 4px' }} />
|
||||
<div style={{ paddingLeft: 8 }}>
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 600, color: typeColors[type] || '#4F6EF7',
|
||||
background: typeBgs[type] || '#EDF0FD', display: 'inline-block',
|
||||
padding: '3px 10px', borderRadius: 6, marginBottom: 10,
|
||||
}}>
|
||||
{typeLabels[type] || type}
|
||||
</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#1A1D28' }}>
|
||||
{parseValueDisplay(record)} <span style={{ fontSize: 13, fontWeight: 500, color: '#9BA0B4' }}>{record.unit}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#C0C5D2', marginTop: 6 }}>
|
||||
{record.recordedAt?.split('T')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user