[阶段2.3-2.4] 实现前端数据同步和仪表板页面

前端实现:
- 创建 DataSync 页面
  - 显示同步状态(就绪/正在同步/失败)
  - 手动触发同步按钮
  - 显示同步日志和错误信息
  - 显示同步帮助信息

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

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

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

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

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-08-23 12:27:43 +08:00
parent 637347082a
commit 0177758e1f
5 changed files with 726 additions and 23 deletions

View File

@@ -1,18 +1,161 @@
import React from 'react';
import './Pages.css';
import React, { useEffect, useState } from 'react';
import { apiClient } from '../services/api';
import './DataSync.css';
interface SyncStatus {
status: 'idle' | 'syncing' | 'error';
lastSyncTime: string | null;
recordsSynced: number;
lastError?: string;
}
function DataSync() {
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>('');
const [message, setMessage] = useState<string>('');
// Load sync status on component mount
useEffect(() => {
loadSyncStatus();
}, []);
const loadSyncStatus = async () => {
try {
const response = await apiClient.getGarminSyncStatus();
setSyncStatus(response.data.data);
} catch (err: any) {
console.error('Failed to load sync status:', err);
}
};
const handleSync = async () => {
// TODO: Implement data sync
setLoading(true);
setError('');
setMessage('');
try {
const response = await apiClient.syncGarminData();
if (response.data.success) {
setMessage(`✅ 同步完成!新增/更新 ${response.data.data.recordsSynced} 天数据`);
// Reload sync status
loadSyncStatus();
} else {
setError(`❌ 同步失败:${response.data.data.message}`);
}
} catch (err: any) {
const errorMsg = err.response?.data?.error?.message || 'Sync failed';
setError(`❌ 同步失败:${errorMsg}`);
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'idle':
return '#999';
case 'syncing':
return '#667eea';
case 'error':
return '#c33';
default:
return '#999';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'idle':
return '就绪';
case 'syncing':
return '正在同步...';
case 'error':
return '同步失败';
default:
return '未知';
}
};
return (
<div className="page">
<h2></h2>
<p className="placeholder">...</p>
<button onClick={handleSync} className="btn btn-primary">
Garmin
</button>
<h2>📱 </h2>
<p className="subtitle"> Garmin Connect </p>
<div className="sync-container">
{/* Status Card */}
<div className="status-card">
<h3></h3>
{syncStatus ? (
<div className="status-info">
<div className="status-item">
<span className="label"></span>
<span
className="value"
style={{ color: getStatusColor(syncStatus.status) }}
>
{getStatusText(syncStatus.status)}
</span>
</div>
{syncStatus.lastSyncTime && (
<div className="status-item">
<span className="label"></span>
<span className="value">
{new Date(syncStatus.lastSyncTime).toLocaleString('zh-CN')}
</span>
</div>
)}
<div className="status-item">
<span className="label"></span>
<span className="value">{syncStatus.recordsSynced} </span>
</div>
{syncStatus.lastError && (
<div className="status-item error">
<span className="label"></span>
<span className="value">{syncStatus.lastError}</span>
</div>
)}
</div>
) : (
<p className="placeholder">...</p>
)}
</div>
{/* Sync Button */}
<div className="sync-actions">
<button
onClick={handleSync}
disabled={loading || (syncStatus?.status === 'syncing')}
className="btn btn-primary btn-large"
>
{loading || syncStatus?.status === 'syncing' ? '✋ 正在同步...' : '🔄 立即同步'}
</button>
<p className="help-text">
7 Garmin
</p>
</div>
{/* Messages */}
{error && <div className="error-message">{error}</div>}
{message && <div className="success-message">{message}</div>}
{/* Info Box */}
<div className="info-box">
<h4>💡 </h4>
<ul>
<li> 7 </li>
<li></li>
<li></li>
<li> Garmin API </li>
</ul>
</div>
</div>
</div>
);
}