Files
GarminHealthLab/client/src/pages/Dashboard.tsx
ericwyuan 0177758e1f [阶段2.3-2.4] 实现前端数据同步和仪表板页面
前端实现:
- 创建 DataSync 页面
  - 显示同步状态(就绪/正在同步/失败)
  - 手动触发同步按钮
  - 显示同步日志和错误信息
  - 显示同步帮助信息

- 创建 Dashboard 页面
  - 今日概览(步数、心率、睡眠、卡路里)
  - 30 日统计(平均值、总计)
  - 交互式图表展示(步数、心率、睡眠、卡路里)

- 创建 Chart 组件
  - 支持折线图和柱状图
  - 使用 Recharts 库
  - 响应式布局

前端样式:
- DataSync.css
- Dashboard.css
- 完整的响应式设计

验收标准已满足:
- 数据同步页面功能完整
- 仪表板显示健康数据
- 图表正确展示数据趋势
- 移动端响应式

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-23 12:27:43 +08:00

219 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import { apiClient } from '../services/api';
import Chart from '../components/Chart';
import './Dashboard.css';
interface HealthData {
date: string;
steps?: number;
heartRate?: number;
sleep?: {
duration: number;
quality: number;
};
caloriesBurned?: number;
}
function Dashboard() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [summary, setSummary] = useState<HealthData[]>([]);
const [today, setToday] = useState<HealthData | null>(null);
const [stats, setStats] = useState({
avgSteps: 0,
avgHeartRate: 0,
avgSleep: 0,
totalCalories: 0,
});
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
setError('');
// Load 30-day summary
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const startDate = thirtyDaysAgo.toISOString().split('T')[0];
const endDate = now.toISOString().split('T')[0];
const response = await apiClient.getHealthSummary(startDate, endDate);
const data = response.data.data || [];
setSummary(data);
// Get today's data
const todayData = data.find((d: HealthData) => d.date === endDate);
setToday(todayData || null);
// Calculate stats
const validData = data.filter((d: HealthData) => d.steps || d.heartRate);
if (validData.length > 0) {
const avgSteps =
Math.round(
validData.reduce((sum: number, d: HealthData) => sum + (d.steps || 0), 0) /
validData.length
) || 0;
const heartRates = validData
.map((d: HealthData) => d.heartRate)
.filter((hr) => hr);
const avgHeartRate =
heartRates.length > 0
? Math.round(heartRates.reduce((a, b) => a + b, 0) / heartRates.length)
: 0;
const sleeps = validData
.map((d: HealthData) => d.sleep?.duration)
.filter((s) => s);
const avgSleep =
sleeps.length > 0
? Math.round((sleeps.reduce((a, b) => a + b, 0) / sleeps.length) * 10) / 10
: 0;
const totalCalories = Math.round(
validData.reduce((sum: number, d: HealthData) => sum + (d.caloriesBurned || 0), 0)
);
setStats({ avgSteps, avgHeartRate, avgSleep, totalCalories });
}
setLoading(false);
} catch (err: any) {
const errorMsg = err.response?.data?.error?.message || 'Failed to load data';
setError(errorMsg);
setLoading(false);
}
};
if (loading) {
return <div className="page-loading"> ...</div>;
}
return (
<div className="page">
<h2>📊 </h2>
{error && <div className="error-message">{error}</div>}
{/* Today's Summary */}
{today ? (
<div className="today-summary">
<h3></h3>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">🚶</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{today.steps || '-'}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{today.heartRate || '-'} bpm</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">😴</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{today.sleep?.duration || '-'} </div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">🔥</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{today.caloriesBurned || '-'} kcal</div>
</div>
</div>
</div>
</div>
) : (
<div className="placeholder"></div>
)}
{/* 30-Day Statistics */}
<div className="statistics">
<h3>30 </h3>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">📈</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{stats.avgSteps.toLocaleString()}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{stats.avgHeartRate} bpm</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">😴</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{stats.avgSleep}h</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">🔥</div>
<div className="stat-content">
<div className="stat-label"></div>
<div className="stat-value">{stats.totalCalories.toLocaleString()}</div>
</div>
</div>
</div>
</div>
{/* Charts */}
{summary.length > 0 && (
<div className="charts-section">
<h3></h3>
<div className="charts-grid">
<Chart title="步数" data={summary} type="bar" dataKey="steps" fill="#667eea" />
<Chart
title="心率"
data={summary.filter((d) => d.heartRate)}
type="line"
dataKey="heartRate"
stroke="#ff6b9d"
/>
<Chart
title="睡眠时长"
data={summary.filter((d) => d.sleep)}
type="line"
dataKey={(d: any) => d.sleep?.duration}
stroke="#ffd93d"
/>
<Chart
title="卡路里消耗"
data={summary}
type="bar"
dataKey="caloriesBurned"
fill="#6bcf7f"
/>
</div>
</div>
)}
</div>
);
}
export default Dashboard;