diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx
index ec9d66f..96b6237 100644
--- a/app/(tabs)/personal.tsx
+++ b/app/(tabs)/personal.tsx
@@ -221,9 +221,7 @@ export default function PersonalScreen() {
: pushIfAuthedElseLogin('/profile/edit')}>
- 编辑
+ {isLoggedIn ? '编辑' : '登录'}
}
diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index 544169b..413a464 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -95,28 +95,6 @@ export default function ExploreScreen() {
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
- const fitnessRingsData = useMockData ? {
- activeCalories: mockData?.activeCalories ?? 0,
- activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
- exerciseMinutes: mockData?.exerciseMinutes ?? 0,
- exerciseMinutesGoal: mockData?.exerciseMinutesGoal ?? 30,
- standHours: mockData?.standHours ?? 0,
- standHoursGoal: mockData?.standHoursGoal ?? 12,
- } : (healthData ? {
- activeCalories: healthData.activeEnergyBurned,
- activeCaloriesGoal: healthData.activeCaloriesGoal,
- exerciseMinutes: healthData.exerciseMinutes,
- exerciseMinutesGoal: healthData.exerciseMinutesGoal,
- standHours: healthData.standHours,
- standHoursGoal: healthData.standHoursGoal,
- } : {
- activeCalories: 0,
- activeCaloriesGoal: 350,
- exerciseMinutes: 0,
- exerciseMinutesGoal: 30,
- standHours: 0,
- standHoursGoal: 12,
- });
// 用于触发动画重置的 token(当日期或数据变化时更新)
const [animToken, setAnimToken] = useState(0);
@@ -564,7 +542,6 @@ export default function ExploreScreen() {
pushIfAuthedElseLogin(`/sleep-detail?date=${dayjs(currentSelectedDate).format('YYYY-MM-DD')}`)}
/>
@@ -573,12 +550,7 @@ export default function ExploreScreen() {
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 09bd40f..0456e8c 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -16,7 +16,7 @@ import { WaterRecordSource } from '@/services/waterRecords';
import { store } from '@/store';
import { rehydrateUserSync, setPrivacyAgreed } from '@/store/userSlice';
import { createWaterRecordAction } from '@/store/waterSlice';
-import { ensureHealthPermissions } from '@/utils/health';
+import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React from 'react';
@@ -44,13 +44,24 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
};
const initHealthPermissions = async () => {
- // 初始化 HealthKit 权限
+ // 初始化 HealthKit 权限管理系统
try {
- console.log('开始请求 HealthKit 权限...');
- await ensureHealthPermissions();
- console.log('HealthKit 权限初始化完成');
+ console.log('初始化 HealthKit 权限管理系统...');
+ initializeHealthPermissions();
+
+ // 延迟请求权限,避免应用启动时弹窗
+ setTimeout(async () => {
+ try {
+ await ensureHealthPermissions();
+ console.log('HealthKit 权限请求完成');
+ } catch (error) {
+ console.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error);
+ }
+ }, 2000);
+
+ console.log('HealthKit 权限管理初始化完成');
} catch (error) {
- console.warn('HealthKit 权限初始化失败,可能在模拟器上运行:', error);
+ console.warn('HealthKit 权限管理初始化失败:', error);
}
}
diff --git a/components/FitnessRingsCard.tsx b/components/FitnessRingsCard.tsx
index 36a2275..3c4bc05 100644
--- a/components/FitnessRingsCard.tsx
+++ b/components/FitnessRingsCard.tsx
@@ -1,20 +1,14 @@
-import React from 'react';
+import React, { useState, useCallback, useRef } from 'react';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import { router } from 'expo-router';
+import { useFocusEffect } from '@react-navigation/native';
import { CircularRing } from './CircularRing';
import { ROUTES } from '@/constants/Routes';
+import { fetchActivityRingsForDate, ActivityRingsData } from '@/utils/health';
type FitnessRingsCardProps = {
style?: any;
- // 活动卡路里数据
- activeCalories?: number;
- activeCaloriesGoal?: number;
- // 锻炼分钟数据
- exerciseMinutes?: number;
- exerciseMinutesGoal?: number;
- // 站立小时数据
- standHours?: number;
- standHoursGoal?: number;
+ selectedDate?: Date;
// 动画重置令牌
resetToken?: unknown;
};
@@ -24,14 +18,48 @@ type FitnessRingsCardProps = {
*/
export function FitnessRingsCard({
style,
- activeCalories = 25,
- activeCaloriesGoal = 350,
- exerciseMinutes = 1,
- exerciseMinutesGoal = 5,
- standHours = 2,
- standHoursGoal = 13,
+ selectedDate,
resetToken,
}: FitnessRingsCardProps) {
+ const [activityData, setActivityData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const loadingRef = useRef(false);
+
+ // 获取健身圆环数据 - 在页面聚焦、日期变化、从后台切换到前台时触发
+ useFocusEffect(
+ useCallback(() => {
+ const loadActivityData = async () => {
+ if (!selectedDate) return;
+
+ // 防止重复请求
+ if (loadingRef.current) return;
+
+ try {
+ loadingRef.current = true;
+ setLoading(true);
+ const data = await fetchActivityRingsForDate(selectedDate);
+ setActivityData(data);
+ } catch (error) {
+ console.error('FitnessRingsCard: 获取健身圆环数据失败:', error);
+ setActivityData(null);
+ } finally {
+ setLoading(false);
+ loadingRef.current = false;
+ }
+ };
+
+ loadActivityData();
+ }, [selectedDate])
+ );
+
+ // 使用获取到的数据或默认值
+ const activeCalories = activityData?.activeEnergyBurned ?? 0;
+ const activeCaloriesGoal = activityData?.activeEnergyBurnedGoal ?? 350;
+ const exerciseMinutes = activityData?.appleExerciseTime ?? 0;
+ const exerciseMinutesGoal = activityData?.appleExerciseTimeGoal ?? 30;
+ const standHours = activityData?.appleStandHours ?? 0;
+ const standHoursGoal = activityData?.appleStandHoursGoal ?? 12;
+
// 计算进度百分比
const caloriesProgress = Math.min(1, Math.max(0, activeCalories / activeCaloriesGoal));
const exerciseProgress = Math.min(1, Math.max(0, exerciseMinutes / exerciseMinutesGoal));
@@ -95,24 +123,42 @@ export function FitnessRingsCard({
- {Math.round(activeCalories)}
- /{activeCaloriesGoal}
+ {loading ? (
+ --
+ ) : (
+ <>
+ {Math.round(activeCalories)}
+ /{activeCaloriesGoal}
+ >
+ )}
千卡
- {Math.round(exerciseMinutes)}
- /{exerciseMinutesGoal}
+ {loading ? (
+ --
+ ) : (
+ <>
+ {Math.round(exerciseMinutes)}
+ /{exerciseMinutesGoal}
+ >
+ )}
分钟
- {Math.round(standHours)}
- /{standHoursGoal}
+ {loading ? (
+ --
+ ) : (
+ <>
+ {Math.round(standHours)}
+ /{standHoursGoal}
+ >
+ )}
小时
diff --git a/components/statistic/SleepCard.tsx b/components/statistic/SleepCard.tsx
index b60ed1d..a10f584 100644
--- a/components/statistic/SleepCard.tsx
+++ b/components/statistic/SleepCard.tsx
@@ -1,18 +1,19 @@
import { fetchCompleteSleepData, formatSleepTime } from '@/utils/sleepHealthKit';
+import dayjs from 'dayjs';
import { Image } from 'expo-image';
+import { router } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+
interface SleepCardProps {
selectedDate?: Date;
style?: object;
- onPress?: () => void;
}
const SleepCard: React.FC = ({
selectedDate,
style,
- onPress
}) => {
const [sleepDuration, setSleepDuration] = useState(null);
const [loading, setLoading] = useState(false);
@@ -52,15 +53,11 @@ const SleepCard: React.FC = ({
);
- if (onPress) {
- return (
-
- {CardContent}
-
- );
- }
-
- return CardContent;
+ return (
+ router.push(`/sleep-detail?date=${dayjs(selectedDate).format('YYYY-MM-DD')}`)} activeOpacity={0.7}>
+ {CardContent}
+
+ );
};
const styles = StyleSheet.create({
diff --git a/hooks/useHealthPermissions.ts b/hooks/useHealthPermissions.ts
new file mode 100644
index 0000000..71383d6
--- /dev/null
+++ b/hooks/useHealthPermissions.ts
@@ -0,0 +1,236 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import {
+ healthPermissionManager,
+ HealthPermissionStatus,
+ ensureHealthPermissions,
+ checkHealthPermissionStatus,
+ fetchTodayHealthData,
+ fetchHealthDataForDate,
+ TodayHealthData
+} from '@/utils/health';
+
+export interface UseHealthPermissionsReturn {
+ // 权限状态
+ permissionStatus: HealthPermissionStatus;
+ isLoading: boolean;
+
+ // 权限操作
+ requestPermissions: () => Promise;
+ checkPermissions: (forceCheck?: boolean) => Promise;
+
+ // 数据刷新
+ refreshHealthData: () => Promise;
+ refreshHealthDataForDate: (date: Date) => Promise;
+
+ // 健康数据
+ healthData: TodayHealthData | null;
+
+ // 状态检查
+ hasPermission: boolean;
+ needsPermission: boolean;
+}
+
+/**
+ * HealthKit权限状态管理Hook
+ *
+ * 功能:
+ * 1. 监听权限状态变化
+ * 2. 自动刷新数据当权限状态改变时
+ * 3. 提供权限请求和数据刷新方法
+ * 4. 缓存健康数据状态
+ */
+export function useHealthPermissions(): UseHealthPermissionsReturn {
+ const [permissionStatus, setPermissionStatus] = useState(
+ healthPermissionManager.getPermissionStatus()
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [healthData, setHealthData] = useState(null);
+
+ // 使用ref避免闭包问题
+ const isLoadingRef = useRef(false);
+ const lastRefreshTime = useRef(0);
+ const refreshThrottle = 2000; // 2秒内避免重复刷新
+
+ // 刷新健康数据
+ const refreshHealthData = useCallback(async () => {
+ const now = Date.now();
+
+ // 防抖:避免短时间内重复刷新
+ if (isLoadingRef.current || (now - lastRefreshTime.current) < refreshThrottle) {
+ console.log('健康数据刷新被节流,跳过本次刷新');
+ return;
+ }
+
+ if (permissionStatus !== HealthPermissionStatus.Authorized) {
+ console.log('没有HealthKit权限,跳过数据刷新');
+ return;
+ }
+
+ isLoadingRef.current = true;
+ setIsLoading(true);
+ lastRefreshTime.current = now;
+
+ try {
+ console.log('开始刷新今日健康数据...');
+ const data = await fetchTodayHealthData();
+ setHealthData(data);
+ console.log('健康数据刷新成功:', data);
+ } catch (error) {
+ console.error('刷新健康数据失败:', error);
+ } finally {
+ isLoadingRef.current = false;
+ setIsLoading(false);
+ }
+ }, [permissionStatus]);
+
+ // 刷新指定日期的健康数据
+ const refreshHealthDataForDate = useCallback(async (date: Date) => {
+ if (permissionStatus !== HealthPermissionStatus.Authorized) {
+ console.log('没有HealthKit权限,跳过数据刷新');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ console.log('开始刷新指定日期健康数据...', date);
+ const data = await fetchHealthDataForDate(date);
+ // 只有是今天的数据才更新state
+ const today = new Date();
+ if (date.toDateString() === today.toDateString()) {
+ setHealthData(data);
+ }
+ console.log('指定日期健康数据刷新成功:', data);
+ } catch (error) {
+ console.error('刷新指定日期健康数据失败:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [permissionStatus]);
+
+ // 请求权限
+ const requestPermissions = useCallback(async (): Promise => {
+ setIsLoading(true);
+ try {
+ console.log('开始请求HealthKit权限...');
+ const granted = await ensureHealthPermissions();
+
+ if (granted) {
+ console.log('权限请求成功,准备刷新数据');
+ // 权限获取成功后,稍微延迟刷新数据
+ setTimeout(() => {
+ refreshHealthData();
+ }, 500);
+ }
+
+ return granted;
+ } catch (error) {
+ console.error('请求HealthKit权限失败:', error);
+ return false;
+ } finally {
+ setIsLoading(false);
+ }
+ }, [refreshHealthData]);
+
+ // 检查权限状态
+ const checkPermissions = useCallback(async (forceCheck: boolean = false): Promise => {
+ try {
+ const status = await checkHealthPermissionStatus(forceCheck);
+ return status;
+ } catch (error) {
+ console.error('检查权限状态失败:', error);
+ return HealthPermissionStatus.Unknown;
+ }
+ }, []);
+
+ // 监听权限状态变化
+ useEffect(() => {
+ console.log('设置HealthKit权限状态监听器...');
+
+ // 权限状态变化监听
+ const handlePermissionStatusChanged = (newStatus: HealthPermissionStatus, oldStatus: HealthPermissionStatus) => {
+ console.log(`权限状态变化: ${oldStatus} -> ${newStatus}`);
+ setPermissionStatus(newStatus);
+
+ // 如果从无权限变为有权限,自动刷新数据
+ if (oldStatus !== HealthPermissionStatus.Authorized && newStatus === HealthPermissionStatus.Authorized) {
+ console.log('权限状态变为已授权,准备刷新健康数据...');
+ setTimeout(() => {
+ refreshHealthData();
+ }, 500);
+ }
+ };
+
+ // 权限获取成功监听
+ const handlePermissionGranted = () => {
+ console.log('权限获取成功事件触发,准备刷新数据...');
+ setTimeout(() => {
+ refreshHealthData();
+ }, 500);
+ };
+
+ healthPermissionManager.on('permissionStatusChanged', handlePermissionStatusChanged);
+ healthPermissionManager.on('permissionGranted', handlePermissionGranted);
+
+ // 组件挂载时检查一次权限状态
+ checkPermissions(true);
+
+ // 如果已经有权限,立即刷新数据
+ if (permissionStatus === HealthPermissionStatus.Authorized) {
+ refreshHealthData();
+ }
+
+ return () => {
+ console.log('清理HealthKit权限状态监听器...');
+ healthPermissionManager.off('permissionStatusChanged', handlePermissionStatusChanged);
+ healthPermissionManager.off('permissionGranted', handlePermissionGranted);
+ };
+ }, [checkPermissions, refreshHealthData, permissionStatus]);
+
+ // 计算派生状态
+ const hasPermission = permissionStatus === HealthPermissionStatus.Authorized;
+ const needsPermission = permissionStatus === HealthPermissionStatus.NotDetermined ||
+ permissionStatus === HealthPermissionStatus.Unknown;
+
+ return {
+ permissionStatus,
+ isLoading,
+ requestPermissions,
+ checkPermissions,
+ refreshHealthData,
+ refreshHealthDataForDate,
+ healthData,
+ hasPermission,
+ needsPermission
+ };
+}
+
+/**
+ * 简化版Hook,只关注权限状态
+ */
+export function useHealthPermissionStatus() {
+ const [permissionStatus, setPermissionStatus] = useState(
+ healthPermissionManager.getPermissionStatus()
+ );
+
+ useEffect(() => {
+ const handlePermissionStatusChanged = (newStatus: HealthPermissionStatus) => {
+ setPermissionStatus(newStatus);
+ };
+
+ healthPermissionManager.on('permissionStatusChanged', handlePermissionStatusChanged);
+
+ // 检查一次当前状态
+ checkHealthPermissionStatus(true);
+
+ return () => {
+ healthPermissionManager.off('permissionStatusChanged', handlePermissionStatusChanged);
+ };
+ }, []);
+
+ return {
+ permissionStatus,
+ hasPermission: permissionStatus === HealthPermissionStatus.Authorized,
+ needsPermission: permissionStatus === HealthPermissionStatus.NotDetermined ||
+ permissionStatus === HealthPermissionStatus.Unknown
+ };
+}
\ No newline at end of file
diff --git a/ios/OutLive/HealthKitManager.m b/ios/OutLive/HealthKitManager.m
index 84c88aa..7c3f747 100644
--- a/ios/OutLive/HealthKitManager.m
+++ b/ios/OutLive/HealthKitManager.m
@@ -55,4 +55,17 @@ RCT_EXTERN_METHOD(getDailyStepCountSamples:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolver
rejecter:(RCTPromiseRejectBlock)rejecter)
+// Hourly Data Methods
+RCT_EXTERN_METHOD(getHourlyActiveEnergyBurned:(NSDictionary *)options
+ resolver:(RCTPromiseResolveBlock)resolver
+ rejecter:(RCTPromiseRejectBlock)rejecter)
+
+RCT_EXTERN_METHOD(getHourlyExerciseTime:(NSDictionary *)options
+ resolver:(RCTPromiseResolveBlock)resolver
+ rejecter:(RCTPromiseRejectBlock)rejecter)
+
+RCT_EXTERN_METHOD(getHourlyStandHours:(NSDictionary *)options
+ resolver:(RCTPromiseResolveBlock)resolver
+ rejecter:(RCTPromiseRejectBlock)rejecter)
+
@end
\ No newline at end of file
diff --git a/ios/OutLive/HealthKitManager.swift b/ios/OutLive/HealthKitManager.swift
index 4795186..f4e7c8d 100644
--- a/ios/OutLive/HealthKitManager.swift
+++ b/ios/OutLive/HealthKitManager.swift
@@ -29,16 +29,16 @@ class HealthKitManager: NSObject, RCTBridgeModule {
static let sleep = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
static let stepCount = HKObjectType.quantityType(forIdentifier: .stepCount)!
static let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate)!
- static let restingHeartRate = HKObjectType.quantityType(forIdentifier: .restingHeartRate)!
static let heartRateVariability = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
static let activeEnergyBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
static let basalEnergyBurned = HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)!
static let appleExerciseTime = HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!
static let appleStandTime = HKObjectType.categoryType(forIdentifier: .appleStandHour)!
static let oxygenSaturation = HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!
+ static let activitySummary = HKObjectType.activitySummaryType()
static var all: Set {
- return [sleep, stepCount, heartRate, restingHeartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation]
+ return [sleep, stepCount, heartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation, activitySummary]
}
}
@@ -566,9 +566,14 @@ class HealthKitManager: NSObject, RCTBridgeModule {
}
let calendar = Calendar.current
- let anchoredDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate)
+ var startDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate)
+ var endDateComponents = calendar.dateComponents([.day, .month, .year], from: endDate)
- let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: anchoredDateComponents, end: anchoredDateComponents)
+ // HealthKit requires DateComponents to have a calendar
+ startDateComponents.calendar = calendar
+ endDateComponents.calendar = calendar
+
+ let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents, end: endDateComponents)
let query = HKActivitySummaryQuery(predicate: predicate) { [weak self] (query, summaries, error) in
DispatchQueue.main.async {
@@ -583,7 +588,10 @@ class HealthKitManager: NSObject, RCTBridgeModule {
}
let summaryData = summaries.map { summary in
- [
+ // 获取对应日期的 DateComponents
+ let summaryDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate)
+
+ return [
"activeEnergyBurned": summary.activeEnergyBurned.doubleValue(for: HKUnit.kilocalorie()),
"activeEnergyBurnedGoal": summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie()),
"appleExerciseTime": summary.appleExerciseTime.doubleValue(for: HKUnit.minute()),
@@ -591,9 +599,9 @@ class HealthKitManager: NSObject, RCTBridgeModule {
"appleStandHours": summary.appleStandHours.doubleValue(for: HKUnit.count()),
"appleStandHoursGoal": summary.appleStandHoursGoal.doubleValue(for: HKUnit.count()),
"dateComponents": [
- "day": anchoredDateComponents.day ?? 0,
- "month": anchoredDateComponents.month ?? 0,
- "year": anchoredDateComponents.year ?? 0
+ "day": summaryDateComponents.day ?? 0,
+ "month": summaryDateComponents.month ?? 0,
+ "year": summaryDateComponents.year ?? 0
]
] as [String : Any]
}
@@ -1060,5 +1068,269 @@ class HealthKitManager: NSObject, RCTBridgeModule {
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.string(from: date)
}
-
+
+ // MARK: - Hourly Data Methods
+
+ @objc
+ func getHourlyActiveEnergyBurned(
+ _ options: NSDictionary,
+ resolver: @escaping RCTPromiseResolveBlock,
+ rejecter: @escaping RCTPromiseRejectBlock
+ ) {
+ guard HKHealthStore.isHealthDataAvailable() else {
+ rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
+ return
+ }
+
+ let activeEnergyType = ReadTypes.activeEnergyBurned
+
+ let startDate: Date
+ if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
+ startDate = d
+ } else {
+ startDate = Calendar.current.startOfDay(for: Date())
+ }
+
+ let endDate: Date
+ if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
+ endDate = d
+ } else {
+ endDate = Date()
+ }
+
+ let calendar = Calendar.current
+ let anchorDate = calendar.startOfDay(for: startDate)
+ let interval = DateComponents(hour: 1)
+
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
+
+ let query = HKStatisticsCollectionQuery(
+ quantityType: activeEnergyType,
+ quantitySamplePredicate: predicate,
+ options: .cumulativeSum,
+ anchorDate: anchorDate,
+ intervalComponents: interval
+ )
+
+ query.initialResultsHandler = { [weak self] query, results, error in
+ DispatchQueue.main.async {
+ if let error = error {
+ rejecter("QUERY_ERROR", "Failed to query hourly active energy: \(error.localizedDescription)", error)
+ return
+ }
+
+ guard let results = results else {
+ resolver([
+ "data": [],
+ "count": 0,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ])
+ return
+ }
+
+ var hourlyData: [[String: Any]] = []
+
+ results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
+ let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? 0
+
+ let hourData: [String: Any] = [
+ "startDate": self?.dateToISOString(statistics.startDate) ?? "",
+ "endDate": self?.dateToISOString(statistics.endDate) ?? "",
+ "value": value,
+ "hour": calendar.component(.hour, from: statistics.startDate)
+ ]
+
+ hourlyData.append(hourData)
+ }
+
+ let result: [String: Any] = [
+ "data": hourlyData,
+ "count": hourlyData.count,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ]
+ resolver(result)
+ }
+ }
+
+ healthStore.execute(query)
+ }
+
+ @objc
+ func getHourlyExerciseTime(
+ _ options: NSDictionary,
+ resolver: @escaping RCTPromiseResolveBlock,
+ rejecter: @escaping RCTPromiseRejectBlock
+ ) {
+ guard HKHealthStore.isHealthDataAvailable() else {
+ rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
+ return
+ }
+
+ let exerciseType = ReadTypes.appleExerciseTime
+
+ let startDate: Date
+ if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
+ startDate = d
+ } else {
+ startDate = Calendar.current.startOfDay(for: Date())
+ }
+
+ let endDate: Date
+ if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
+ endDate = d
+ } else {
+ endDate = Date()
+ }
+
+ let calendar = Calendar.current
+ let anchorDate = calendar.startOfDay(for: startDate)
+ let interval = DateComponents(hour: 1)
+
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
+
+ let query = HKStatisticsCollectionQuery(
+ quantityType: exerciseType,
+ quantitySamplePredicate: predicate,
+ options: .cumulativeSum,
+ anchorDate: anchorDate,
+ intervalComponents: interval
+ )
+
+ query.initialResultsHandler = { [weak self] query, results, error in
+ DispatchQueue.main.async {
+ if let error = error {
+ rejecter("QUERY_ERROR", "Failed to query hourly exercise time: \(error.localizedDescription)", error)
+ return
+ }
+
+ guard let results = results else {
+ resolver([
+ "data": [],
+ "count": 0,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ])
+ return
+ }
+
+ var hourlyData: [[String: Any]] = []
+
+ results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
+ let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.minute()) ?? 0
+
+ let hourData: [String: Any] = [
+ "startDate": self?.dateToISOString(statistics.startDate) ?? "",
+ "endDate": self?.dateToISOString(statistics.endDate) ?? "",
+ "value": value,
+ "hour": calendar.component(.hour, from: statistics.startDate)
+ ]
+
+ hourlyData.append(hourData)
+ }
+
+ let result: [String: Any] = [
+ "data": hourlyData,
+ "count": hourlyData.count,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ]
+ resolver(result)
+ }
+ }
+
+ healthStore.execute(query)
+ }
+
+ @objc
+ func getHourlyStandHours(
+ _ options: NSDictionary,
+ resolver: @escaping RCTPromiseResolveBlock,
+ rejecter: @escaping RCTPromiseRejectBlock
+ ) {
+ guard HKHealthStore.isHealthDataAvailable() else {
+ rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
+ return
+ }
+
+ let standType = ReadTypes.appleStandTime
+
+ let startDate: Date
+ if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
+ startDate = d
+ } else {
+ startDate = Calendar.current.startOfDay(for: Date())
+ }
+
+ let endDate: Date
+ if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
+ endDate = d
+ } else {
+ endDate = Date()
+ }
+
+ let calendar = Calendar.current
+ let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
+
+ let query = HKSampleQuery(sampleType: standType,
+ predicate: predicate,
+ limit: HKObjectQueryNoLimit,
+ sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in
+ DispatchQueue.main.async {
+ if let error = error {
+ rejecter("QUERY_ERROR", "Failed to query hourly stand hours: \(error.localizedDescription)", error)
+ return
+ }
+
+ guard let standSamples = samples as? [HKCategorySample] else {
+ resolver([
+ "data": [],
+ "count": 0,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ])
+ return
+ }
+
+ // 初始化24小时数据
+ var hourlyData: [[String: Any]] = []
+
+ // 为每个小时创建数据结构
+ for hour in 0..<24 {
+ let hourStart = calendar.date(byAdding: .hour, value: hour, to: calendar.startOfDay(for: startDate))!
+ let hourEnd = calendar.date(byAdding: .hour, value: hour + 1, to: calendar.startOfDay(for: startDate))!
+
+ // 检查该小时是否有站立记录
+ let standSamplesInHour = standSamples.filter { sample in
+ return sample.startDate >= hourStart && sample.startDate < hourEnd &&
+ sample.value == HKCategoryValueAppleStandHour.stood.rawValue
+ }
+
+ let hasStood = standSamplesInHour.count > 0 ? 1 : 0
+
+ let hourData: [String: Any] = [
+ "startDate": self?.dateToISOString(hourStart) ?? "",
+ "endDate": self?.dateToISOString(hourEnd) ?? "",
+ "value": hasStood,
+ "hour": hour
+ ]
+
+ hourlyData.append(hourData)
+ }
+
+ let result: [String: Any] = [
+ "data": hourlyData,
+ "count": hourlyData.count,
+ "startDate": self?.dateToISOString(startDate) ?? "",
+ "endDate": self?.dateToISOString(endDate) ?? ""
+ ]
+ resolver(result)
+ }
+ }
+
+ healthStore.execute(query)
+ }
+
} // end class
diff --git a/utils/SimpleEventEmitter.ts b/utils/SimpleEventEmitter.ts
new file mode 100644
index 0000000..61e2ce6
--- /dev/null
+++ b/utils/SimpleEventEmitter.ts
@@ -0,0 +1,109 @@
+/**
+ * React Native兼容的EventEmitter实现
+ *
+ * 提供与Node.js EventEmitter相似的API,但专门为React Native环境设计
+ * 避免了对Node.js内置模块的依赖
+ */
+export class SimpleEventEmitter {
+ private listeners: { [event: string]: ((...args: any[]) => void)[] } = {};
+
+ /**
+ * 添加事件监听器
+ * @param event 事件名称
+ * @param listener 监听器函数
+ */
+ on(event: string, listener: (...args: any[]) => void): void {
+ if (!this.listeners[event]) {
+ this.listeners[event] = [];
+ }
+ this.listeners[event].push(listener);
+ }
+
+ /**
+ * 移除事件监听器
+ * @param event 事件名称
+ * @param listener 要移除的监听器函数
+ */
+ off(event: string, listener: (...args: any[]) => void): void {
+ if (!this.listeners[event]) return;
+
+ const index = this.listeners[event].indexOf(listener);
+ if (index > -1) {
+ this.listeners[event].splice(index, 1);
+ }
+ }
+
+ /**
+ * 触发事件
+ * @param event 事件名称
+ * @param args 传递给监听器的参数
+ */
+ emit(event: string, ...args: any[]): void {
+ if (!this.listeners[event]) return;
+
+ // 复制监听器数组,避免在执行过程中数组被修改
+ const listeners = [...this.listeners[event]];
+
+ listeners.forEach(listener => {
+ try {
+ listener(...args);
+ } catch (error) {
+ console.error(`Error in event listener for event "${event}":`, error);
+ }
+ });
+ }
+
+ /**
+ * 添加一次性事件监听器
+ * @param event 事件名称
+ * @param listener 监听器函数(只会执行一次)
+ */
+ once(event: string, listener: (...args: any[]) => void): void {
+ const onceWrapper = (...args: any[]) => {
+ this.off(event, onceWrapper);
+ listener(...args);
+ };
+
+ this.on(event, onceWrapper);
+ }
+
+ /**
+ * 移除指定事件的所有监听器
+ * @param event 事件名称(可选,如果未提供则移除所有事件的监听器)
+ */
+ removeAllListeners(event?: string): void {
+ if (event) {
+ delete this.listeners[event];
+ } else {
+ this.listeners = {};
+ }
+ }
+
+ /**
+ * 获取指定事件的监听器数量
+ * @param event 事件名称
+ * @returns 监听器数量
+ */
+ listenerCount(event: string): number {
+ return this.listeners[event]?.length || 0;
+ }
+
+ /**
+ * 获取指定事件的所有监听器
+ * @param event 事件名称
+ * @returns 监听器数组的副本
+ */
+ listeners(event: string): ((...args: any[]) => void)[] {
+ return this.listeners[event] ? [...this.listeners[event]] : [];
+ }
+
+ /**
+ * 获取所有事件名称
+ * @returns 事件名称数组
+ */
+ eventNames(): string[] {
+ return Object.keys(this.listeners);
+ }
+}
+
+export default SimpleEventEmitter;
\ No newline at end of file
diff --git a/utils/health.ts b/utils/health.ts
index dfe19b1..3476003 100644
--- a/utils/health.ts
+++ b/utils/health.ts
@@ -1,5 +1,6 @@
import dayjs from 'dayjs';
-import { NativeModules } from 'react-native';
+import { AppState, AppStateStatus, NativeModules } from 'react-native';
+import { SimpleEventEmitter } from './SimpleEventEmitter';
type HealthDataOptions = {
startDate: string;
@@ -9,6 +10,140 @@ type HealthDataOptions = {
// React Native bridge to native HealthKitManager
const { HealthKitManager } = NativeModules;
+// HealthKit权限状态枚举
+export enum HealthPermissionStatus {
+ Unknown = 'unknown',
+ Authorized = 'authorized',
+ Denied = 'denied',
+ NotDetermined = 'notDetermined'
+}
+
+// 权限状态管理类
+class HealthPermissionManager extends SimpleEventEmitter {
+ private permissionStatus: HealthPermissionStatus = HealthPermissionStatus.Unknown;
+ private isChecking: boolean = false;
+ private lastCheckTime: number = 0;
+ private checkInterval: number = 5000; // 5秒检查间隔,避免频繁检查
+ private appStateSubscription: any = null;
+
+ constructor() {
+ super();
+ this.setupAppStateListener();
+ }
+
+ // 设置应用状态监听
+ private setupAppStateListener() {
+ this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange.bind(this));
+ }
+
+ // 处理应用状态变化
+ private handleAppStateChange(nextAppState: AppStateStatus) {
+ if (nextAppState === 'active') {
+ // 应用回到前台时检查权限状态
+ console.log('应用回到前台,检查HealthKit权限状态...');
+ this.checkPermissionStatus(true);
+ }
+ }
+
+ // 获取当前权限状态
+ public getPermissionStatus(): HealthPermissionStatus {
+ return this.permissionStatus;
+ }
+
+ // 设置权限状态
+ private setPermissionStatus(status: HealthPermissionStatus, shouldEmit: boolean = true) {
+ const oldStatus = this.permissionStatus;
+ this.permissionStatus = status;
+
+ if (shouldEmit && oldStatus !== status) {
+ console.log(`HealthKit权限状态变化: ${oldStatus} -> ${status}`);
+ this.emit('permissionStatusChanged', status, oldStatus);
+ }
+ }
+
+ // 检查权限状态(通过尝试读取数据来间接判断)
+ public async checkPermissionStatus(forceCheck: boolean = false): Promise {
+ const now = Date.now();
+
+ // 避免频繁检查
+ if (!forceCheck && this.isChecking) {
+ return this.permissionStatus;
+ }
+
+ if (!forceCheck && (now - this.lastCheckTime) < this.checkInterval) {
+ return this.permissionStatus;
+ }
+
+ this.isChecking = true;
+ this.lastCheckTime = now;
+
+ try {
+ // 尝试获取简单的步数数据来检测权限
+ const today = new Date();
+ const options = {
+ startDate: dayjs(today).startOf('day').toDate().toISOString(),
+ endDate: dayjs(today).endOf('day').toDate().toISOString()
+ };
+
+ const result = await HealthKitManager.getStepCount(options);
+
+ if (result && result.totalValue !== undefined) {
+ // 能够获取数据,说明有权限
+ this.setPermissionStatus(HealthPermissionStatus.Authorized);
+ } else if (result && result.error) {
+ // 有错误返回,可能是权限被拒绝
+ this.setPermissionStatus(HealthPermissionStatus.Denied);
+ } else {
+ // 其他情况
+ this.setPermissionStatus(HealthPermissionStatus.Unknown);
+ }
+ } catch (error) {
+ console.log('HealthKit权限检查失败,可能是权限被拒绝:', error);
+ this.setPermissionStatus(HealthPermissionStatus.Denied);
+ } finally {
+ this.isChecking = false;
+ }
+
+ return this.permissionStatus;
+ }
+
+ // 请求权限
+ public async requestPermission(): Promise {
+ try {
+ console.log('开始请求HealthKit权限...');
+ const result = await HealthKitManager.requestAuthorization();
+
+ if (result && result.success) {
+ console.log('HealthKit权限请求成功');
+ this.setPermissionStatus(HealthPermissionStatus.Authorized);
+
+ // 权限获取成功后触发数据刷新事件
+ this.emit('permissionGranted');
+ return true;
+ } else {
+ console.error('HealthKit权限请求失败');
+ this.setPermissionStatus(HealthPermissionStatus.Denied);
+ return false;
+ }
+ } catch (error) {
+ console.error('HealthKit权限请求出现异常:', error);
+ this.setPermissionStatus(HealthPermissionStatus.Denied);
+ return false;
+ }
+ }
+
+ // 清理资源
+ public destroy() {
+ if (this.appStateSubscription) {
+ this.appStateSubscription.remove();
+ }
+ this.removeAllListeners();
+ }
+}
+
+// 全局权限管理实例
+export const healthPermissionManager = new HealthPermissionManager();
+
// Interface for activity summary data from HealthKit
export interface HealthActivitySummary {
activeEnergyBurned: number;
@@ -86,23 +221,19 @@ export type TodayHealthData = {
heartRate: number | null;
};
+// 更新:使用新的权限管理系统
export async function ensureHealthPermissions(): Promise {
- try {
- console.log('开始请求HealthKit权限...');
- const result = await HealthKitManager.requestAuthorization();
+ return await healthPermissionManager.requestPermission();
+}
- if (result && result.success) {
- console.log('HealthKit权限请求成功');
- console.log('权限状态:', result.permissions);
- return true;
- } else {
- console.error('HealthKit权限请求失败');
- return false;
- }
- } catch (error) {
- console.error('HealthKit权限请求出现异常:', error);
- return false;
- }
+// 获取当前权限状态
+export function getHealthPermissionStatus(): HealthPermissionStatus {
+ return healthPermissionManager.getPermissionStatus();
+}
+
+// 检查权限状态
+export async function checkHealthPermissionStatus(forceCheck: boolean = false): Promise {
+ return await healthPermissionManager.checkPermissionStatus(forceCheck);
}
// 日期工具函数
@@ -218,36 +349,105 @@ export async function fetchHourlyStepSamples(date: Date): Promise {
+// 获取每小时活动热量数据
+async function fetchHourlyActiveCalories(date: Date): Promise {
try {
- // For now, return default data as hourly data is complex and not critical for basic fitness rings
- console.log('每小时活动热量获取暂未实现,返回默认数据');
- return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
+ const options = createDateRange(date);
+ const result = await HealthKitManager.getHourlyActiveEnergyBurned(options);
+
+ if (result && result.data && Array.isArray(result.data)) {
+ logSuccess('每小时活动热量', result);
+
+ // 初始化24小时数据
+ const hourlyData: HourlyActivityData[] = Array.from({ length: 24 }, (_, i) => ({
+ hour: i,
+ calories: 0
+ }));
+
+ // 将API返回的数据映射到对应的小时
+ result.data.forEach((sample: any) => {
+ if (sample && sample.hour !== undefined && sample.value !== undefined) {
+ const hour = sample.hour;
+ if (hour >= 0 && hour < 24) {
+ hourlyData[hour].calories = Math.round(sample.value);
+ }
+ }
+ });
+
+ return hourlyData;
+ } else {
+ logWarning('每小时活动热量', '为空或格式错误');
+ return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
+ }
} catch (error) {
logError('每小时活动热量', error);
return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
}
}
-// 获取每小时锻炼分钟数据(简化实现)
-async function fetchHourlyExerciseMinutes(_date: Date): Promise {
+// 获取每小时锻炼分钟数据
+async function fetchHourlyExerciseMinutes(date: Date): Promise {
try {
- // For now, return default data as hourly data is complex and not critical for basic fitness rings
- console.log('每小时锻炼分钟获取暂未实现,返回默认数据');
- return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
+ const options = createDateRange(date);
+ const result = await HealthKitManager.getHourlyExerciseTime(options);
+
+ if (result && result.data && Array.isArray(result.data)) {
+ logSuccess('每小时锻炼分钟', result);
+
+ // 初始化24小时数据
+ const hourlyData: HourlyExerciseData[] = Array.from({ length: 24 }, (_, i) => ({
+ hour: i,
+ minutes: 0
+ }));
+
+ // 将API返回的数据映射到对应的小时
+ result.data.forEach((sample: any) => {
+ if (sample && sample.hour !== undefined && sample.value !== undefined) {
+ const hour = sample.hour;
+ if (hour >= 0 && hour < 24) {
+ hourlyData[hour].minutes = Math.round(sample.value);
+ }
+ }
+ });
+
+ return hourlyData;
+ } else {
+ logWarning('每小时锻炼分钟', '为空或格式错误');
+ return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
+ }
} catch (error) {
logError('每小时锻炼分钟', error);
return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
}
}
-// 获取每小时站立小时数据(简化实现)
-async function fetchHourlyStandHours(_date: Date): Promise {
+// 获取每小时站立小时数据
+async function fetchHourlyStandHours(date: Date): Promise {
try {
- // For now, return default data as hourly data is complex and not critical for basic fitness rings
- console.log('每小时站立数据获取暂未实现,返回默认数据');
- return Array.from({ length: 24 }, () => 0);
+ const options = createDateRange(date);
+ const result = await HealthKitManager.getHourlyStandHours(options);
+
+ if (result && result.data && Array.isArray(result.data)) {
+ logSuccess('每小时站立数据', result);
+
+ // 初始化24小时数据
+ const hourlyData: number[] = Array.from({ length: 24 }, () => 0);
+
+ // 将API返回的数据映射到对应的小时
+ result.data.forEach((sample: any) => {
+ if (sample && sample.hour !== undefined && sample.value !== undefined) {
+ const hour = sample.hour;
+ if (hour >= 0 && hour < 24) {
+ hourlyData[hour] = sample.value;
+ }
+ }
+ });
+
+ return hourlyData;
+ } else {
+ logWarning('每小时站立数据', '为空或格式错误');
+ return Array.from({ length: 24 }, () => 0);
+ }
} catch (error) {
logError('每小时站立数据', error);
return Array.from({ length: 24 }, () => 0);
@@ -314,15 +514,15 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise {
try {
- // const result = await HealthKitManager.getActivitySummary(options);
+ const result = await HealthKitManager.getActivitySummary(options);
- // if (result && Array.isArray(result) && result.length > 0) {
- // logSuccess('ActivitySummary', result[0]);
- // return result[0];
- // } else {
- // logWarning('ActivitySummary', '为空');
- // return null;
- // }
+ if (result && Array.isArray(result) && result.length > 0) {
+ logSuccess('ActivitySummary', result[0]);
+ return result[0];
+ } else {
+ logWarning('ActivitySummary', '为空');
+ return null;
+ }
} catch (error) {
logError('ActivitySummary', error);
return null;
@@ -366,8 +566,12 @@ async function fetchHeartRate(options: HealthDataOptions): Promise {
+export async function fetchMaximumHeartRate(_options: HealthDataOptions): Promise {
try {
+ // 暂未实现,返回null
+ console.log('最大心率获取暂未实现');
+ return null;
+
// const result = await HealthKitManager.getHeartRateSamples(options);
// if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
@@ -536,10 +740,10 @@ export async function updateWeight(_weight: number) {
}
}
-export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise {
+export async function testOxygenSaturationData(_date: Date = dayjs().toDate()): Promise {
console.log('=== 开始测试血氧饱和度数据获取 ===');
- const options = createDateRange(date);
+ // const options = createDateRange(date);
try {
// const result = await HealthKitManager.getOxygenSaturationSamples(options);
@@ -697,3 +901,65 @@ export async function fetchActivityRingsForDate(date: Date): Promise {
+ healthPermissionManager.checkPermissionStatus(true);
+ }, 1000);
+}
+
+// 监听权限状态变化(用于组件外部使用)
+export function addHealthPermissionListener(
+ event: 'permissionStatusChanged' | 'permissionGranted',
+ listener: (...args: any[]) => void
+) {
+ healthPermissionManager.on(event, listener);
+}
+
+// 移除权限状态监听器
+export function removeHealthPermissionListener(
+ event: 'permissionStatusChanged' | 'permissionGranted',
+ listener: (...args: any[]) => void
+) {
+ healthPermissionManager.off(event, listener);
+}
+
+// 清理权限管理资源(应在应用退出时调用)
+export function cleanupHealthPermissions() {
+ console.log('清理HealthKit权限管理资源...');
+ healthPermissionManager.destroy();
+}
+
+// 获取权限状态的可读文本
+export function getPermissionStatusText(status: HealthPermissionStatus): string {
+ switch (status) {
+ case HealthPermissionStatus.Authorized:
+ return '已授权';
+ case HealthPermissionStatus.Denied:
+ return '已拒绝';
+ case HealthPermissionStatus.NotDetermined:
+ return '未确定';
+ case HealthPermissionStatus.Unknown:
+ default:
+ return '未知';
+ }
+}
+
+// 检查是否需要显示权限请求UI
+export function shouldShowPermissionRequest(): boolean {
+ const status = healthPermissionManager.getPermissionStatus();
+ return status === HealthPermissionStatus.NotDetermined ||
+ status === HealthPermissionStatus.Unknown;
+}
+
+// 检查是否权限被用户拒绝
+export function isPermissionDenied(): boolean {
+ const status = healthPermissionManager.getPermissionStatus();
+ return status === HealthPermissionStatus.Denied;
+}
+
diff --git a/utils/healthKitExample.ts b/utils/healthKitExample.ts
deleted file mode 100644
index 257fd73..0000000
--- a/utils/healthKitExample.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * HealthKit Native Module Usage Example
- * 展示如何使用HealthKit native module的示例代码
- */
-
-import HealthKitManager, { HealthKitUtils, SleepDataSample } from './healthKit';
-
-export class HealthKitService {
-
- /**
- * 初始化HealthKit并请求权限
- */
- static async initializeHealthKit(): Promise {
- 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 {
- 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 {
- 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 {
- 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 || '睡眠分析失败');
- }
-};
\ No newline at end of file