[阶段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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user