feat: 支持步数卡片; 优化数据分析各类卡片样式
This commit is contained in:
@@ -27,6 +27,11 @@ const PERMISSIONS: HealthKitPermissions = {
|
||||
},
|
||||
};
|
||||
|
||||
export type HourlyStepData = {
|
||||
hour: number; // 0-23
|
||||
steps: number;
|
||||
};
|
||||
|
||||
export type TodayHealthData = {
|
||||
steps: number;
|
||||
activeEnergyBurned: number; // kilocalories
|
||||
@@ -43,6 +48,8 @@ export type TodayHealthData = {
|
||||
// 新增血氧饱和度和心率数据
|
||||
oxygenSaturation: number | null;
|
||||
heartRate: number | null;
|
||||
// 每小时步数数据
|
||||
hourlySteps: HourlyStepData[];
|
||||
};
|
||||
|
||||
export async function ensureHealthPermissions(): Promise<boolean> {
|
||||
@@ -155,6 +162,88 @@ async function fetchStepCount(date: Date): Promise<number> {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取指定日期每小时步数数据
|
||||
async function fetchHourlyStepCount(date: Date): Promise<HourlyStepData[]> {
|
||||
return new Promise((resolve) => {
|
||||
const startOfDay = dayjs(date).startOf('day');
|
||||
const endOfDay = dayjs(date).endOf('day');
|
||||
|
||||
AppleHealthKit.getStepCount({
|
||||
startDate: startOfDay.toDate().toISOString(),
|
||||
endDate: endOfDay.toDate().toISOString(),
|
||||
includeManuallyAdded: false,
|
||||
}, (err, res) => {
|
||||
if (err) {
|
||||
logError('每小时步数', err);
|
||||
return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
|
||||
}
|
||||
|
||||
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||
logWarning('每小时步数', '为空');
|
||||
return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
|
||||
}
|
||||
|
||||
logSuccess('每小时步数', res);
|
||||
|
||||
// 初始化24小时数据
|
||||
const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
hour: i,
|
||||
steps: 0
|
||||
}));
|
||||
|
||||
// 如果返回的是累计数据,我们需要获取样本数据
|
||||
resolve(hourlyData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 使用样本数据获取每小时步数
|
||||
async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData[]> {
|
||||
return new Promise((resolve) => {
|
||||
const startOfDay = dayjs(date).startOf('day');
|
||||
const endOfDay = dayjs(date).endOf('day');
|
||||
|
||||
AppleHealthKit.getSamples(
|
||||
{
|
||||
startDate: startOfDay.toDate().toISOString(),
|
||||
endDate: endOfDay.toDate().toISOString(),
|
||||
type: 'StepCount',
|
||||
},
|
||||
(err: any, res: any[]) => {
|
||||
if (err) {
|
||||
logError('每小时步数样本', err);
|
||||
return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
|
||||
}
|
||||
|
||||
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||
logWarning('每小时步数样本', '为空');
|
||||
return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
|
||||
}
|
||||
|
||||
logSuccess('每小时步数样本', res);
|
||||
|
||||
// 初始化24小时数据
|
||||
const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
hour: i,
|
||||
steps: 0
|
||||
}));
|
||||
|
||||
// 将样本数据按小时分组并累加
|
||||
res.forEach((sample: any) => {
|
||||
if (sample && sample.startDate && sample.value) {
|
||||
const hour = dayjs(sample.startDate).hour();
|
||||
if (hour >= 0 && hour < 24) {
|
||||
hourlyData[hour].steps += Math.round(sample.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resolve(hourlyData);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
|
||||
@@ -304,7 +393,8 @@ function getDefaultHealthData(): TodayHealthData {
|
||||
standHours: 0,
|
||||
standHoursGoal: 12,
|
||||
oxygenSaturation: null,
|
||||
heartRate: null
|
||||
heartRate: null,
|
||||
hourlySteps: Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -318,6 +408,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
// 并行获取所有健康数据
|
||||
const [
|
||||
steps,
|
||||
hourlySteps,
|
||||
activeEnergyBurned,
|
||||
basalEnergyBurned,
|
||||
sleepDuration,
|
||||
@@ -327,6 +418,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
heartRate
|
||||
] = await Promise.all([
|
||||
fetchStepCount(date),
|
||||
fetchHourlyStepSamples(date),
|
||||
fetchActiveEnergyBurned(options),
|
||||
fetchBasalEnergyBurned(options),
|
||||
fetchSleepDuration(options),
|
||||
@@ -338,6 +430,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
|
||||
console.log('指定日期健康数据获取完成:', {
|
||||
steps,
|
||||
hourlySteps,
|
||||
activeEnergyBurned,
|
||||
basalEnergyBurned,
|
||||
sleepDuration,
|
||||
@@ -349,6 +442,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
|
||||
return {
|
||||
steps,
|
||||
hourlySteps,
|
||||
activeEnergyBurned,
|
||||
basalEnergyBurned,
|
||||
sleepDuration,
|
||||
|
||||
136
utils/mockHealthData.ts
Normal file
136
utils/mockHealthData.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { HourlyStepData, TodayHealthData } from './health';
|
||||
|
||||
// Mock的每小时步数数据,模拟真实的一天活动模式
|
||||
export const mockHourlySteps: HourlyStepData[] = [
|
||||
{ hour: 0, steps: 0 }, // 午夜
|
||||
{ hour: 1, steps: 0 }, // 凌晨
|
||||
{ hour: 2, steps: 0 },
|
||||
{ hour: 3, steps: 0 },
|
||||
{ hour: 4, steps: 0 },
|
||||
{ hour: 5, steps: 0 },
|
||||
{ hour: 6, steps: 120 }, // 早晨起床
|
||||
{ hour: 7, steps: 450 }, // 晨练/上班准备
|
||||
{ hour: 8, steps: 680 }, // 上班通勤
|
||||
{ hour: 9, steps: 320 }, // 工作时间
|
||||
{ hour: 10, steps: 180 }, // 办公室内活动
|
||||
{ hour: 11, steps: 280 }, // 会议/活动
|
||||
{ hour: 12, steps: 520 }, // 午餐时间
|
||||
{ hour: 13, steps: 150 }, // 午休
|
||||
{ hour: 14, steps: 240 }, // 下午工作
|
||||
{ hour: 15, steps: 300 }, // 工作活动
|
||||
{ hour: 16, steps: 380 }, // 会议/外出
|
||||
{ hour: 17, steps: 480 }, // 下班通勤
|
||||
{ hour: 18, steps: 620 }, // 晚餐/活动
|
||||
{ hour: 19, steps: 350 }, // 晚间活动
|
||||
{ hour: 20, steps: 280 }, // 散步
|
||||
{ hour: 21, steps: 150 }, // 休闲时间
|
||||
{ hour: 22, steps: 80 }, // 准备睡觉
|
||||
{ hour: 23, steps: 30 }, // 睡前
|
||||
];
|
||||
|
||||
// Mock的完整健康数据
|
||||
export const mockHealthData: TodayHealthData = {
|
||||
steps: 6140, // 总步数
|
||||
hourlySteps: mockHourlySteps,
|
||||
activeEnergyBurned: 420,
|
||||
basalEnergyBurned: 1680,
|
||||
sleepDuration: 480, // 8小时
|
||||
hrv: 45,
|
||||
activeCalories: 420,
|
||||
activeCaloriesGoal: 350,
|
||||
exerciseMinutes: 32,
|
||||
exerciseMinutesGoal: 30,
|
||||
standHours: 8,
|
||||
standHoursGoal: 12,
|
||||
oxygenSaturation: 98.2,
|
||||
heartRate: 72,
|
||||
};
|
||||
|
||||
// 生成随机的每小时步数数据(用于测试不同的数据模式)
|
||||
export const generateRandomHourlySteps = (): HourlyStepData[] => {
|
||||
return Array.from({ length: 24 }, (_, hour) => {
|
||||
let steps = 0;
|
||||
|
||||
// 模拟真实的活动模式
|
||||
if (hour >= 6 && hour <= 22) {
|
||||
if (hour >= 7 && hour <= 9) {
|
||||
// 早晨高峰期
|
||||
steps = Math.floor(Math.random() * 600) + 200;
|
||||
} else if (hour >= 12 && hour <= 13) {
|
||||
// 午餐时间
|
||||
steps = Math.floor(Math.random() * 400) + 300;
|
||||
} else if (hour >= 17 && hour <= 19) {
|
||||
// 晚间活跃期
|
||||
steps = Math.floor(Math.random() * 500) + 250;
|
||||
} else if (hour >= 6 && hour <= 22) {
|
||||
// 白天正常活动
|
||||
steps = Math.floor(Math.random() * 300) + 50;
|
||||
}
|
||||
} else {
|
||||
// 夜间很少活动
|
||||
steps = Math.floor(Math.random() * 50);
|
||||
}
|
||||
|
||||
return { hour, steps };
|
||||
});
|
||||
};
|
||||
|
||||
// 不同活动模式的预设数据
|
||||
export const activityPatterns = {
|
||||
// 久坐办公族
|
||||
sedentary: Array.from({ length: 24 }, (_, hour) => ({
|
||||
hour,
|
||||
steps: hour >= 7 && hour <= 18 ? Math.floor(Math.random() * 200) + 50 :
|
||||
hour >= 19 && hour <= 21 ? Math.floor(Math.random() * 300) + 100 :
|
||||
Math.floor(Math.random() * 20)
|
||||
})),
|
||||
|
||||
// 活跃用户
|
||||
active: Array.from({ length: 24 }, (_, hour) => ({
|
||||
hour,
|
||||
steps: hour >= 6 && hour <= 8 ? Math.floor(Math.random() * 800) + 400 :
|
||||
hour >= 12 && hour <= 13 ? Math.floor(Math.random() * 600) + 300 :
|
||||
hour >= 17 && hour <= 20 ? Math.floor(Math.random() * 900) + 500 :
|
||||
hour >= 9 && hour <= 16 ? Math.floor(Math.random() * 400) + 100 :
|
||||
Math.floor(Math.random() * 50)
|
||||
})),
|
||||
|
||||
// 健身爱好者
|
||||
fitness: Array.from({ length: 24 }, (_, hour) => ({
|
||||
hour,
|
||||
steps: hour === 6 ? Math.floor(Math.random() * 1200) + 800 : // 晨跑
|
||||
hour === 18 ? Math.floor(Math.random() * 1000) + 600 : // 晚间锻炼
|
||||
hour >= 7 && hour <= 17 ? Math.floor(Math.random() * 300) + 100 :
|
||||
Math.floor(Math.random() * 50)
|
||||
})),
|
||||
};
|
||||
|
||||
// 用于快速切换测试数据的函数
|
||||
export const getTestHealthData = (pattern: 'mock' | 'random' | 'sedentary' | 'active' | 'fitness' = 'mock'): TodayHealthData => {
|
||||
let hourlySteps: HourlyStepData[];
|
||||
|
||||
switch (pattern) {
|
||||
case 'random':
|
||||
hourlySteps = generateRandomHourlySteps();
|
||||
break;
|
||||
case 'sedentary':
|
||||
hourlySteps = activityPatterns.sedentary;
|
||||
break;
|
||||
case 'active':
|
||||
hourlySteps = activityPatterns.active;
|
||||
break;
|
||||
case 'fitness':
|
||||
hourlySteps = activityPatterns.fitness;
|
||||
break;
|
||||
default:
|
||||
hourlySteps = mockHourlySteps;
|
||||
}
|
||||
|
||||
const totalSteps = hourlySteps.reduce((sum, data) => sum + data.steps, 0);
|
||||
|
||||
return {
|
||||
...mockHealthData,
|
||||
steps: totalSteps,
|
||||
hourlySteps,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user