feat: 支持 healthkit
This commit is contained in:
@@ -73,7 +73,6 @@ export async function ensureHealthPermissions(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
console.log('开始初始化HealthKit...');
|
||||
|
||||
resolve(true)
|
||||
AppleHealthKit.initHealthKit(PERMISSIONS, (error) => {
|
||||
if (error) {
|
||||
console.error('HealthKit初始化失败:', error);
|
||||
|
||||
147
utils/healthKit.ts
Normal file
147
utils/healthKit.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* HealthKit Native Module Interface
|
||||
* React Native TypeScript bindings for iOS HealthKit access
|
||||
*/
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
export interface HealthKitPermissions {
|
||||
[key: string]: 'notDetermined' | 'denied' | 'authorized' | 'unknown';
|
||||
}
|
||||
|
||||
export interface HealthKitAuthorizationResult {
|
||||
success: boolean;
|
||||
permissions: HealthKitPermissions;
|
||||
}
|
||||
|
||||
export interface SleepDataSource {
|
||||
name: string;
|
||||
bundleIdentifier: string;
|
||||
}
|
||||
|
||||
export interface SleepDataSample {
|
||||
id: string;
|
||||
startDate: string; // ISO8601 format
|
||||
endDate: string; // ISO8601 format
|
||||
value: number;
|
||||
categoryType: 'inBed' | 'asleep' | 'awake' | 'core' | 'deep' | 'rem' | 'unknown';
|
||||
duration: number; // Duration in seconds
|
||||
source: SleepDataSource;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SleepDataOptions {
|
||||
startDate?: string; // ISO8601 format, defaults to 7 days ago
|
||||
endDate?: string; // ISO8601 format, defaults to now
|
||||
limit?: number; // Maximum number of samples, defaults to 100
|
||||
}
|
||||
|
||||
export interface SleepDataResult {
|
||||
data: SleepDataSample[];
|
||||
count: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export interface HealthKitManagerInterface {
|
||||
/**
|
||||
* Request authorization to access HealthKit data
|
||||
* This will prompt the user for permission to read/write health data
|
||||
*/
|
||||
requestAuthorization(): Promise<HealthKitAuthorizationResult>;
|
||||
|
||||
/**
|
||||
* Get sleep analysis data from HealthKit
|
||||
* @param options Query options including date range and limit
|
||||
*/
|
||||
getSleepData(options?: SleepDataOptions): Promise<SleepDataResult>;
|
||||
}
|
||||
|
||||
console.log('NativeModules', NativeModules);
|
||||
|
||||
|
||||
// Native module interface
|
||||
const HealthKitManager: HealthKitManagerInterface = NativeModules.HealthKitManager;
|
||||
|
||||
export default HealthKitManager;
|
||||
|
||||
// Utility functions for working with sleep data
|
||||
export class HealthKitUtils {
|
||||
/**
|
||||
* Convert seconds to hours and minutes
|
||||
*/
|
||||
static formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total sleep duration from sleep samples for a specific date
|
||||
*/
|
||||
static getTotalSleepDuration(samples: SleepDataSample[], date: Date): number {
|
||||
const targetDate = date.toISOString().split('T')[0];
|
||||
|
||||
return samples
|
||||
.filter(sample => {
|
||||
const sampleDate = new Date(sample.startDate).toISOString().split('T')[0];
|
||||
return sampleDate === targetDate &&
|
||||
['asleep', 'core', 'deep', 'rem'].includes(sample.categoryType);
|
||||
})
|
||||
.reduce((total, sample) => total + sample.duration, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group sleep samples by date
|
||||
*/
|
||||
static groupSamplesByDate(samples: SleepDataSample[]): Record<string, SleepDataSample[]> {
|
||||
return samples.reduce((grouped, sample) => {
|
||||
const date = new Date(sample.startDate).toISOString().split('T')[0];
|
||||
if (!grouped[date]) {
|
||||
grouped[date] = [];
|
||||
}
|
||||
grouped[date].push(sample);
|
||||
return grouped;
|
||||
}, {} as Record<string, SleepDataSample[]>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sleep quality metrics from samples
|
||||
*/
|
||||
static getSleepQualityMetrics(samples: SleepDataSample[]) {
|
||||
const sleepSamples = samples.filter(s =>
|
||||
['asleep', 'core', 'deep', 'rem'].includes(s.categoryType)
|
||||
);
|
||||
|
||||
if (sleepSamples.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalDuration = sleepSamples.reduce((sum, s) => sum + s.duration, 0);
|
||||
const deepSleepDuration = sleepSamples
|
||||
.filter(s => s.categoryType === 'deep')
|
||||
.reduce((sum, s) => sum + s.duration, 0);
|
||||
const remSleepDuration = sleepSamples
|
||||
.filter(s => s.categoryType === 'rem')
|
||||
.reduce((sum, s) => sum + s.duration, 0);
|
||||
|
||||
return {
|
||||
totalDuration,
|
||||
deepSleepDuration,
|
||||
remSleepDuration,
|
||||
deepSleepPercentage: totalDuration > 0 ? (deepSleepDuration / totalDuration) * 100 : 0,
|
||||
remSleepPercentage: totalDuration > 0 ? (remSleepDuration / totalDuration) * 100 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if HealthKit is available (iOS only)
|
||||
*/
|
||||
static isAvailable(): boolean {
|
||||
return HealthKitManager != null;
|
||||
}
|
||||
}
|
||||
236
utils/healthKitExample.ts
Normal file
236
utils/healthKitExample.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* HealthKit Native Module Usage Example
|
||||
* 展示如何使用HealthKit native module的示例代码
|
||||
*/
|
||||
|
||||
import HealthKitManager, { HealthKitUtils, SleepDataSample } from './healthKit';
|
||||
|
||||
export class HealthKitService {
|
||||
|
||||
/**
|
||||
* 初始化HealthKit并请求权限
|
||||
*/
|
||||
static async initializeHealthKit(): Promise<boolean> {
|
||||
try {
|
||||
// 检查HealthKit是否可用
|
||||
if (!HealthKitUtils.isAvailable()) {
|
||||
console.log('HealthKit不可用,可能运行在Android设备或模拟器上');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 请求授权
|
||||
const result = await HealthKitManager.requestAuthorization();
|
||||
|
||||
if (result.success) {
|
||||
console.log('HealthKit授权成功');
|
||||
console.log('权限状态:', result.permissions);
|
||||
|
||||
// 检查睡眠数据权限
|
||||
const sleepPermission = result.permissions['HKCategoryTypeIdentifierSleepAnalysis'];
|
||||
if (sleepPermission === 'authorized') {
|
||||
console.log('睡眠数据访问权限已获得');
|
||||
return true;
|
||||
} else {
|
||||
console.log('睡眠数据访问权限未获得:', sleepPermission);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('HealthKit授权失败');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HealthKit初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的睡眠数据
|
||||
*/
|
||||
static async getRecentSleepData(days: number = 7): Promise<SleepDataSample[]> {
|
||||
try {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(endDate.getDate() - days);
|
||||
|
||||
const result = await HealthKitManager.getSleepData({
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
limit: 100
|
||||
});
|
||||
|
||||
console.log(`获取到 ${result.count} 条睡眠记录`);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('获取睡眠数据失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析睡眠质量
|
||||
*/
|
||||
static async analyzeSleepQuality(days: number = 7): Promise<any> {
|
||||
try {
|
||||
const sleepData = await this.getRecentSleepData(days);
|
||||
|
||||
if (sleepData.length === 0) {
|
||||
return {
|
||||
error: '没有找到睡眠数据',
|
||||
hasData: false
|
||||
};
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const groupedData = HealthKitUtils.groupSamplesByDate(sleepData);
|
||||
const dates = Object.keys(groupedData).sort().reverse(); // 最新的日期在前
|
||||
|
||||
const analysis = dates.slice(0, days).map(date => {
|
||||
const daySamples = groupedData[date];
|
||||
const totalSleepDuration = HealthKitUtils.getTotalSleepDuration(daySamples, new Date(date));
|
||||
const qualityMetrics = HealthKitUtils.getSleepQualityMetrics(daySamples);
|
||||
|
||||
return {
|
||||
date,
|
||||
totalSleepDuration,
|
||||
totalSleepFormatted: HealthKitUtils.formatDuration(totalSleepDuration),
|
||||
qualityMetrics,
|
||||
samplesCount: daySamples.length
|
||||
};
|
||||
});
|
||||
|
||||
// 计算平均值
|
||||
const validDays = analysis.filter(day => day.totalSleepDuration > 0);
|
||||
const averageSleepDuration = validDays.length > 0
|
||||
? validDays.reduce((sum, day) => sum + day.totalSleepDuration, 0) / validDays.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
days: analysis,
|
||||
summary: {
|
||||
averageSleepDuration,
|
||||
averageSleepFormatted: HealthKitUtils.formatDuration(averageSleepDuration),
|
||||
daysWithData: validDays.length,
|
||||
totalDaysAnalyzed: days
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('睡眠质量分析失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
hasData: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取昨晚的睡眠数据
|
||||
*/
|
||||
static async getLastNightSleep(): Promise<any> {
|
||||
try {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
// 设置查询范围:昨天下午6点到今天上午12点
|
||||
const startDate = new Date(yesterday);
|
||||
startDate.setHours(18, 0, 0, 0);
|
||||
|
||||
const endDate = new Date(today);
|
||||
endDate.setHours(12, 0, 0, 0);
|
||||
|
||||
const result = await HealthKitManager.getSleepData({
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
limit: 50
|
||||
});
|
||||
|
||||
if (result.data.length === 0) {
|
||||
return {
|
||||
hasData: false,
|
||||
message: '未找到昨晚的睡眠数据'
|
||||
};
|
||||
}
|
||||
|
||||
const sleepSamples = result.data.filter(sample =>
|
||||
['asleep', 'core', 'deep', 'rem'].includes(sample.categoryType)
|
||||
);
|
||||
|
||||
if (sleepSamples.length === 0) {
|
||||
return {
|
||||
hasData: false,
|
||||
message: '未找到有效的睡眠阶段数据'
|
||||
};
|
||||
}
|
||||
|
||||
// 找到睡眠的开始和结束时间
|
||||
const sleepStart = new Date(Math.min(...sleepSamples.map(s => new Date(s.startDate).getTime())));
|
||||
const sleepEnd = new Date(Math.max(...sleepSamples.map(s => new Date(s.endDate).getTime())));
|
||||
const totalDuration = sleepSamples.reduce((sum, s) => sum + s.duration, 0);
|
||||
|
||||
const qualityMetrics = HealthKitUtils.getSleepQualityMetrics(sleepSamples);
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
sleepStart: sleepStart.toISOString(),
|
||||
sleepEnd: sleepEnd.toISOString(),
|
||||
totalDuration,
|
||||
totalDurationFormatted: HealthKitUtils.formatDuration(totalDuration),
|
||||
qualityMetrics,
|
||||
samples: sleepSamples,
|
||||
bedTime: sleepStart.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
wakeTime: sleepEnd.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取昨晚睡眠数据失败:', error);
|
||||
return {
|
||||
hasData: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
export const useHealthKitExample = async () => {
|
||||
console.log('=== HealthKit 使用示例 ===');
|
||||
|
||||
// 1. 初始化和授权
|
||||
const initialized = await HealthKitService.initializeHealthKit();
|
||||
if (!initialized) {
|
||||
console.log('HealthKit初始化失败,无法继续');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取昨晚的睡眠数据
|
||||
console.log('\n--- 昨晚睡眠数据 ---');
|
||||
const lastNightSleep = await HealthKitService.getLastNightSleep();
|
||||
if (lastNightSleep.hasData) {
|
||||
console.log(`睡眠时间: ${lastNightSleep.bedTime} - ${lastNightSleep.wakeTime}`);
|
||||
console.log(`睡眠时长: ${lastNightSleep.totalDurationFormatted}`);
|
||||
if (lastNightSleep.qualityMetrics) {
|
||||
console.log(`深睡眠: ${lastNightSleep.qualityMetrics.deepSleepPercentage.toFixed(1)}%`);
|
||||
console.log(`REM睡眠: ${lastNightSleep.qualityMetrics.remSleepPercentage.toFixed(1)}%`);
|
||||
}
|
||||
} else {
|
||||
console.log(lastNightSleep.message || '未找到睡眠数据');
|
||||
}
|
||||
|
||||
// 3. 分析最近一周的睡眠质量
|
||||
console.log('\n--- 最近一周睡眠分析 ---');
|
||||
const weeklyAnalysis = await HealthKitService.analyzeSleepQuality(7);
|
||||
if (weeklyAnalysis.hasData) {
|
||||
console.log(`平均睡眠时长: ${weeklyAnalysis.summary.averageSleepFormatted}`);
|
||||
console.log(`有数据的天数: ${weeklyAnalysis.summary.daysWithData}/${weeklyAnalysis.summary.totalDaysAnalyzed}`);
|
||||
|
||||
console.log('\n每日睡眠详情:');
|
||||
weeklyAnalysis.days.forEach((day: any) => {
|
||||
if (day.totalSleepDuration > 0) {
|
||||
console.log(`${day.date}: ${day.totalSleepFormatted}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(weeklyAnalysis.error || '睡眠分析失败');
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import dayjs from 'dayjs';
|
||||
import HealthKit from 'react-native-health';
|
||||
import { ensureHealthPermissions } from './health';
|
||||
import HealthKitManager, { HealthKitUtils } from './healthKit';
|
||||
|
||||
// 睡眠阶段枚举
|
||||
export enum SleepStage {
|
||||
@@ -116,54 +115,54 @@ export const fetchSleepSamples = async (date: Date): Promise<SleepSample[]> => {
|
||||
endDate: dateRange.endDate.toISOString(),
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
HealthKit.getSleepSamples(options, (error: string, results: any[]) => {
|
||||
if (error) {
|
||||
console.error('[Sleep] 获取睡眠数据失败:', error);
|
||||
resolve([]); // 返回空数组而非拒绝,以便于处理
|
||||
return;
|
||||
}
|
||||
// return new Promise((resolve) => {
|
||||
// HealthKitManager.(options, (error: string, results: any[]) => {
|
||||
// if (error) {
|
||||
// console.error('[Sleep] 获取睡眠数据失败:', error);
|
||||
// resolve([]); // 返回空数组而非拒绝,以便于处理
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
console.warn('[Sleep] 未找到睡眠数据');
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
// if (!results || results.length === 0) {
|
||||
// console.warn('[Sleep] 未找到睡眠数据');
|
||||
// resolve([]);
|
||||
// return;
|
||||
// }
|
||||
|
||||
console.log('[Sleep] 获取到原始睡眠样本:', results.length, '条');
|
||||
// console.log('[Sleep] 获取到原始睡眠样本:', results.length, '条');
|
||||
|
||||
// 过滤并转换数据格式
|
||||
const sleepSamples: SleepSample[] = results
|
||||
.filter(sample => {
|
||||
const sampleStart = new Date(sample.startDate).getTime();
|
||||
const sampleEnd = new Date(sample.endDate).getTime();
|
||||
const rangeStart = dateRange.startDate.getTime();
|
||||
const rangeEnd = dateRange.endDate.getTime();
|
||||
// // 过滤并转换数据格式
|
||||
// const sleepSamples: SleepSample[] = results
|
||||
// .filter(sample => {
|
||||
// const sampleStart = new Date(sample.startDate).getTime();
|
||||
// const sampleEnd = new Date(sample.endDate).getTime();
|
||||
// const rangeStart = dateRange.startDate.getTime();
|
||||
// const rangeEnd = dateRange.endDate.getTime();
|
||||
|
||||
return (sampleStart >= rangeStart && sampleStart < rangeEnd) ||
|
||||
(sampleStart < rangeEnd && sampleEnd > rangeStart);
|
||||
})
|
||||
.map(sample => {
|
||||
console.log('[Sleep] 原始睡眠样本:', {
|
||||
startDate: sample.startDate,
|
||||
endDate: sample.endDate,
|
||||
value: sample.value,
|
||||
sourceName: sample.sourceName
|
||||
});
|
||||
// return (sampleStart >= rangeStart && sampleStart < rangeEnd) ||
|
||||
// (sampleStart < rangeEnd && sampleEnd > rangeStart);
|
||||
// })
|
||||
// .map(sample => {
|
||||
// console.log('[Sleep] 原始睡眠样本:', {
|
||||
// startDate: sample.startDate,
|
||||
// endDate: sample.endDate,
|
||||
// value: sample.value,
|
||||
// sourceName: sample.sourceName
|
||||
// });
|
||||
|
||||
return {
|
||||
startDate: sample.startDate,
|
||||
endDate: sample.endDate,
|
||||
value: mapHealthKitSleepValue(sample.value),
|
||||
sourceName: sample.sourceName,
|
||||
sourceId: sample.sourceId || sample.uuid
|
||||
};
|
||||
});
|
||||
// return {
|
||||
// startDate: sample.startDate,
|
||||
// endDate: sample.endDate,
|
||||
// value: mapHealthKitSleepValue(sample.value),
|
||||
// sourceName: sample.sourceName,
|
||||
// sourceId: sample.sourceId || sample.uuid
|
||||
// };
|
||||
// });
|
||||
|
||||
console.log('[Sleep] 过滤后的睡眠样本:', sleepSamples.length, '条');
|
||||
resolve(sleepSamples);
|
||||
});
|
||||
});
|
||||
// console.log('[Sleep] 过滤后的睡眠样本:', sleepSamples.length, '条');
|
||||
// resolve(sleepSamples);
|
||||
// });
|
||||
// });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Sleep] 获取睡眠样本失败:', error);
|
||||
@@ -181,37 +180,37 @@ export const fetchSleepHeartRateData = async (bedtime: string, wakeupTime: strin
|
||||
endDate: wakeupTime,
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
HealthKit.getHeartRateSamples(options, (error: string, results: any[]) => {
|
||||
if (error) {
|
||||
console.error('[Sleep] 获取心率数据失败:', error);
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
// return new Promise((resolve) => {
|
||||
// HealthKit.getHeartRateSamples(options, (error: string, results: any[]) => {
|
||||
// if (error) {
|
||||
// console.error('[Sleep] 获取心率数据失败:', error);
|
||||
// resolve([]);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
console.log('[Sleep] 未找到心率数据');
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
// if (!results || results.length === 0) {
|
||||
// console.log('[Sleep] 未找到心率数据');
|
||||
// resolve([]);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const heartRateData: HeartRateData[] = results
|
||||
.filter(sample => {
|
||||
const sampleTime = new Date(sample.startDate).getTime();
|
||||
const bedtimeMs = new Date(bedtime).getTime();
|
||||
const wakeupTimeMs = new Date(wakeupTime).getTime();
|
||||
return sampleTime >= bedtimeMs && sampleTime <= wakeupTimeMs;
|
||||
})
|
||||
.map(sample => ({
|
||||
timestamp: sample.startDate,
|
||||
value: Math.round(sample.value)
|
||||
}))
|
||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
// const heartRateData: HeartRateData[] = results
|
||||
// .filter(sample => {
|
||||
// const sampleTime = new Date(sample.startDate).getTime();
|
||||
// const bedtimeMs = new Date(bedtime).getTime();
|
||||
// const wakeupTimeMs = new Date(wakeupTime).getTime();
|
||||
// return sampleTime >= bedtimeMs && sampleTime <= wakeupTimeMs;
|
||||
// })
|
||||
// .map(sample => ({
|
||||
// timestamp: sample.startDate,
|
||||
// value: Math.round(sample.value)
|
||||
// }))
|
||||
// .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
|
||||
console.log('[Sleep] 获取到睡眠心率数据:', heartRateData.length, '个样本');
|
||||
resolve(heartRateData);
|
||||
});
|
||||
});
|
||||
// console.log('[Sleep] 获取到睡眠心率数据:', heartRateData.length, '个样本');
|
||||
// resolve(heartRateData);
|
||||
// });
|
||||
// });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Sleep] 获取睡眠心率数据失败:', error);
|
||||
@@ -369,8 +368,14 @@ export const getSleepQualityInfo = (sleepScore: number): { description: string;
|
||||
export const fetchCompleteSleepData = async (date: Date): Promise<CompleteSleepData | null> => {
|
||||
try {
|
||||
console.log('[Sleep] 开始获取完整睡眠数据...', dayjs(date).format('YYYY-MM-DD'));
|
||||
// 检查HealthKit是否可用
|
||||
if (!HealthKitUtils.isAvailable()) {
|
||||
console.log('HealthKit不可用,可能运行在Android设备或模拟器上');
|
||||
return null;
|
||||
}
|
||||
|
||||
await ensureHealthPermissions()
|
||||
await HealthKitManager.requestAuthorization()
|
||||
// await ensureHealthPermissions()
|
||||
// 获取睡眠样本
|
||||
const sleepSamples = await fetchSleepSamples(date);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user