Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web)
119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Card } from '@/components/common/Card';
|
|
import { Empty } from '@/components/common/Empty';
|
|
import { Badge } from '@/components/common/Badge';
|
|
import { PageHeader } from '@/components/layout/PageHeader';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { useNotificationStore } from '@/stores/notification.store';
|
|
import * as healthService from '@/services/health.service';
|
|
import { MEASUREMENT_TYPES, HEALTH_TIPS } from '@/utils/constants';
|
|
import { getBPRiskLevel } from '@/utils/format';
|
|
import type { HealthStats } from '@/types';
|
|
import styles from './HomePage.module.css';
|
|
|
|
const QUICK_ACTIONS = [
|
|
{ label: '测血压', icon: '💓', path: '/health/records?type=blood_pressure' },
|
|
{ label: '记用药', icon: '💊', path: '/health/medications' },
|
|
{ label: '在线问诊', icon: '👨⚕️', path: '/services/consultation' },
|
|
{ label: '报告解读', icon: '📋', path: '/services/reports' },
|
|
{ label: '健康日历', icon: '📅', path: '/health/calendar' },
|
|
{ label: '运动饮食', icon: '🏃', path: '/health/exercise-diet' },
|
|
];
|
|
|
|
export function HomePage() {
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const { unreadCount, fetchNotifications } = useNotificationStore();
|
|
const [stats, setStats] = useState<HealthStats[]>([]);
|
|
const [tipIndex, setTipIndex] = useState(0);
|
|
|
|
useEffect(() => {
|
|
healthService.getLatestStats().then(setStats);
|
|
fetchNotifications();
|
|
}, [fetchNotifications]);
|
|
|
|
const bpStats = stats.find((s) => s.type === 'blood_pressure');
|
|
const hrStats = stats.find((s) => s.type === 'heart_rate');
|
|
|
|
const bpValue = bpStats?.latest?.value;
|
|
const systolic = typeof bpValue === 'object' ? bpValue.systolic : null;
|
|
const diastolic = typeof bpValue === 'object' ? bpValue.diastolic : null;
|
|
const riskLevel = systolic && diastolic ? getBPRiskLevel(systolic, diastolic) : null;
|
|
|
|
return (
|
|
<div className="page">
|
|
<PageHeader
|
|
title={`你好,${user?.nickname || '用户'}`}
|
|
showBack={false}
|
|
rightAction={
|
|
<button className={styles.notifyBtn} onClick={() => navigate('/notifications')}>
|
|
🔔
|
|
{unreadCount > 0 && <Badge count={unreadCount} />}
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
{/* Health Overview */}
|
|
{bpStats?.latest && hrStats?.latest ? (
|
|
<Card className={styles.overviewCard}>
|
|
<div className={styles.overviewHeader}>
|
|
<span className={styles.overviewTitle}>健康概览</span>
|
|
<span className={styles.overviewTime}>最新记录</span>
|
|
</div>
|
|
<div className={styles.overviewData}>
|
|
<div className={styles.bpSection}>
|
|
<span className={styles.dataLabel}>血压</span>
|
|
<div className={styles.bpValues}>
|
|
<span className={`${styles.bpNum} ${styles[`risk_${riskLevel}`] || ''}`}>
|
|
{systolic}
|
|
</span>
|
|
<span className={styles.bpSep}>/</span>
|
|
<span className={`${styles.bpNum} ${styles[`risk_${riskLevel}`] || ''}`}>
|
|
{diastolic}
|
|
</span>
|
|
</div>
|
|
<span className={styles.unit}>mmHg</span>
|
|
</div>
|
|
<div className={styles.divider} />
|
|
<div className={styles.hrSection}>
|
|
<span className={styles.dataLabel}>心率</span>
|
|
<span className={styles.hrNum}>{Number(hrStats.latest.value)}</span>
|
|
<span className={styles.unit}>bpm</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<Empty icon="💓" message="暂无健康数据" />
|
|
)}
|
|
|
|
{/* Quick Actions */}
|
|
<div className={styles.quickActions}>
|
|
{QUICK_ACTIONS.map((action) => (
|
|
<button
|
|
key={action.label}
|
|
className={styles.quickAction}
|
|
onClick={() => navigate(action.path)}
|
|
>
|
|
<span className={styles.quickIcon}>{action.icon}</span>
|
|
<span className={styles.quickLabel}>{action.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Health Tip */}
|
|
<Card
|
|
className={styles.tipCard}
|
|
onClick={() => setTipIndex((prev) => (prev + 1) % HEALTH_TIPS.length)}
|
|
>
|
|
<div className={styles.tipHeader}>
|
|
<span>💡</span>
|
|
<span className={styles.tipTitle}>健康小贴士</span>
|
|
<span className={styles.tipHint}>点击换一条</span>
|
|
</div>
|
|
<p className={styles.tipContent}>{HEALTH_TIPS[tipIndex]}</p>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|