feat: 补齐 Garmin 未同步的数据,并各自配上界面
审计 57 个接口后,把账号里真有数据却从未入库的部分补上。全部走同步模块, 界面只读本地库。 新增数据 - 体重与身体成分(体脂率/肌肉量/体水分/骨量/内脏脂肪/代谢年龄) - 血压(接口通,账号暂无记录) - 跑步成绩预测(5 公里 / 10 公里 / 半马 / 全马) - 爬坡分、饮水量、出汗量 → health_data 新增七列 - 全天曲线:心率 / 压力 / 身体电量 / 呼吸 / 血氧 - 挑战赛(徽章挑战与好友挑战,与一次性的徽章不同,有周期和进度) - 已配对设备 新增界面 - /body/ 身体成分:体重大数字 + BMI 分级 + 体脂肌肉曲线 + 血压表格 - /race/ 成绩预测:四个距离的预测成绩与配速,以及预测随时间的变化 - /challenges/ 挑战赛:按类型筛选,有目标的显示进度条 - /devices/ 已配对设备 - 每日页新增「全天曲线」,这是存日内采样的主要目的 - 健康页新增「身体成分」分组与「更多」入口,运动页加挑战赛与成绩预测入口 同步开销 - 日内曲线每天五个请求,14 天以内的同步顺带拉,更长的历史交给后台 「补齐详细数据」,否则一年的同步会多出约 1800 个请求 - 原来的「补齐运动详情」扩展为统一的补齐任务,分阶段上报进度 日内采样抽稀到每天 240 点:手机图表分辨不出更多,只会把行撑大。 全量 446 项测试通过。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,13 @@ export interface HealthDay {
|
||||
sleepStressAvg: number | null;
|
||||
trainingReadiness: number | null;
|
||||
vo2max: number | null;
|
||||
hillScore: number | null;
|
||||
hydrationMl: number | null;
|
||||
hydrationGoalMl: number | null;
|
||||
sweatLossMl: number | null;
|
||||
weightKg: number | null;
|
||||
bodyFatPct: number | null;
|
||||
bmi: number | null;
|
||||
enduranceScore: number | null;
|
||||
sleep: SleepDetail | null;
|
||||
}
|
||||
@@ -211,11 +218,65 @@ export interface FitnessAge {
|
||||
|
||||
export interface DetailSyncStatus {
|
||||
running: boolean;
|
||||
/** Which part of the backfill is running: 运动详情 / 每日曲线. */
|
||||
stage?: string | null;
|
||||
done: number;
|
||||
total: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface BodyCompositionDay {
|
||||
date: string;
|
||||
weightKg: number | null;
|
||||
bmi: number | null;
|
||||
bodyFatPct: number | null;
|
||||
bodyWaterPct: number | null;
|
||||
boneMassKg: number | null;
|
||||
muscleMassKg: number | null;
|
||||
physiqueRating: number | null;
|
||||
visceralFat: number | null;
|
||||
metabolicAge: number | null;
|
||||
}
|
||||
|
||||
export interface BloodPressureReading {
|
||||
measuredAt: string;
|
||||
systolic: number | null;
|
||||
diastolic: number | null;
|
||||
pulse: number | null;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
/** Predicted finishing times, in seconds. */
|
||||
export interface RacePrediction {
|
||||
date: string;
|
||||
time5k: number | null;
|
||||
time10k: number | null;
|
||||
timeHalf: number | null;
|
||||
timeMarathon: number | null;
|
||||
}
|
||||
|
||||
/** [timestamp, value] pairs, thinned to at most 240 points per day. */
|
||||
export type DailySeries = Record<string, Array<[string, number]>>;
|
||||
|
||||
export interface Challenge {
|
||||
uuid: string;
|
||||
kind: string;
|
||||
name: string | null;
|
||||
status: string | null;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
deviceId: string;
|
||||
name: string | null;
|
||||
model: string | null;
|
||||
serial: string | null;
|
||||
softwareVersion: string | null;
|
||||
lastUsedAt: string | null;
|
||||
}
|
||||
|
||||
export interface AutoSyncStatus {
|
||||
enabled: boolean;
|
||||
intervalSeconds: number;
|
||||
@@ -511,6 +572,44 @@ class ApiClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getBodyComposition(startDate?: string, endDate?: string) {
|
||||
const { data } = await this.client.get<BodyCompositionDay[]>(
|
||||
'/health/body-composition', this.range(startDate, endDate)
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getBloodPressure() {
|
||||
const { data } = await this.client.get<BloodPressureReading[]>(
|
||||
'/health/blood-pressure'
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getRacePredictions() {
|
||||
const { data } = await this.client.get<RacePrediction[]>(
|
||||
'/health/race-predictions'
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDailySeries(date: string) {
|
||||
const { data } = await this.client.get<DailySeries>(
|
||||
'/health/series', { params: { date } }
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getChallenges() {
|
||||
const { data } = await this.client.get<Challenge[]>('/health/challenges');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDevices() {
|
||||
const { data } = await this.client.get<Device[]>('/health/devices');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getBadges() {
|
||||
const { data } = await this.client.get<Badge[]>('/health/badges');
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user