feat: 修复健康数据
This commit is contained in:
@@ -14,7 +14,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||||
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
import { setHealthData } from '@/store/healthSlice';
|
||||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import { fetchTodayWaterStats } from '@/store/waterSlice';
|
import { fetchTodayWaterStats } from '@/store/waterSlice';
|
||||||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fetchBasalEnergyBurned } from '@/utils/health';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface BasalMetabolismCardProps {
|
interface BasalMetabolismCardProps {
|
||||||
@@ -22,8 +22,13 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
const userProfile = useAppSelector(selectUserProfile);
|
const userProfile = useAppSelector(selectUserProfile);
|
||||||
const userAge = useAppSelector(selectUserAge);
|
const userAge = useAppSelector(selectUserAge);
|
||||||
|
|
||||||
// 计算基础代谢率范围
|
// 缓存和防抖相关
|
||||||
const calculateBMRRange = () => {
|
const cacheRef = useRef<Map<string, { data: number | null; timestamp: number }>>(new Map());
|
||||||
|
const loadingRef = useRef<Map<string, Promise<number | null>>>(new Map());
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
|
||||||
|
|
||||||
|
// 使用 useMemo 缓存 BMR 计算,避免每次渲染重复计算
|
||||||
|
const bmrRange = useMemo(() => {
|
||||||
const { gender, weight, height } = userProfile;
|
const { gender, weight, height } = userProfile;
|
||||||
|
|
||||||
// 检查是否有足够的信息来计算BMR
|
// 检查是否有足够的信息来计算BMR
|
||||||
@@ -52,35 +57,83 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
const maxBMR = Math.round(bmr * 1.15);
|
const maxBMR = Math.round(bmr * 1.15);
|
||||||
|
|
||||||
return { min: minBMR, max: maxBMR, base: Math.round(bmr) };
|
return { min: minBMR, max: maxBMR, base: Math.round(bmr) };
|
||||||
};
|
}, [userProfile.gender, userProfile.weight, userProfile.height, userAge]);
|
||||||
|
|
||||||
const bmrRange = calculateBMRRange();
|
// 优化的数据获取函数,包含缓存和去重复请求
|
||||||
|
const fetchBasalMetabolismData = useCallback(async (date: Date): Promise<number | null> => {
|
||||||
|
const dateKey = dayjs(date).format('YYYY-MM-DD');
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
const cached = cacheRef.current.get(dateKey);
|
||||||
|
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经在请求中(防止重复请求)
|
||||||
|
const existingRequest = loadingRef.current.get(dateKey);
|
||||||
|
if (existingRequest) {
|
||||||
|
return existingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的请求
|
||||||
|
const request = (async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
startDate: dayjs(date).startOf('day').toDate().toISOString(),
|
||||||
|
endDate: dayjs(date).endOf('day').toDate().toISOString()
|
||||||
|
};
|
||||||
|
const basalEnergy = await fetchBasalEnergyBurned(options);
|
||||||
|
const result = basalEnergy || null;
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
cacheRef.current.set(dateKey, { data: result, timestamp: now });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('BasalMetabolismCard: 获取基础代谢数据失败:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// 清理请求记录
|
||||||
|
loadingRef.current.delete(dateKey);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 记录请求
|
||||||
|
loadingRef.current.set(dateKey, request);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 获取基础代谢数据
|
// 获取基础代谢数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBasalMetabolismData = async () => {
|
if (!selectedDate) return;
|
||||||
if (!selectedDate) return;
|
|
||||||
|
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const result = await fetchBasalMetabolismData(selectedDate);
|
||||||
const options = {
|
if (!isCancelled) {
|
||||||
startDate: dayjs(selectedDate).startOf('day').toDate().toISOString(),
|
setBasalMetabolism(result);
|
||||||
endDate: dayjs(selectedDate).endOf('day').toDate().toISOString()
|
}
|
||||||
};
|
|
||||||
const basalEnergy = await fetchBasalEnergyBurned(options);
|
|
||||||
setBasalMetabolism(basalEnergy || null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('BasalMetabolismCard: 获取基础代谢数据失败:', error);
|
|
||||||
setBasalMetabolism(null);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!isCancelled) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadBasalMetabolismData();
|
loadData();
|
||||||
}, [selectedDate]);
|
|
||||||
// 获取基础代谢状态描述
|
// 清理函数,防止组件卸载后的状态更新
|
||||||
const getMetabolismStatus = () => {
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [selectedDate, fetchBasalMetabolismData]);
|
||||||
|
// 使用 useMemo 优化状态描述计算
|
||||||
|
const status = useMemo(() => {
|
||||||
if (basalMetabolism === null || basalMetabolism === 0) {
|
if (basalMetabolism === null || basalMetabolism === 0) {
|
||||||
return { text: '未知', color: '#9AA3AE' };
|
return { text: '未知', color: '#9AA3AE' };
|
||||||
}
|
}
|
||||||
@@ -95,9 +148,7 @@ export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCard
|
|||||||
} else {
|
} else {
|
||||||
return { text: '较低', color: '#EF4444' };
|
return { text: '较低', color: '#EF4444' };
|
||||||
}
|
}
|
||||||
};
|
}, [basalMetabolism]);
|
||||||
|
|
||||||
const status = getMetabolismStatus();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||||
import { ROUTES } from '@/constants/Routes';
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
import { useActiveCalories } from '@/hooks/useActiveCalories';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { fetchCompleteNutritionCardData, selectNutritionCardDataByDate } from '@/store/nutritionSlice';
|
import { fetchDailyBasalMetabolism, fetchDailyNutritionData, selectBasalMetabolismByDate, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||||
import { triggerLightHaptic } from '@/utils/haptics';
|
import { triggerLightHaptic } from '@/utils/haptics';
|
||||||
import { calculateRemainingCalories } from '@/utils/nutrition';
|
import { calculateRemainingCalories } from '@/utils/nutrition';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -102,17 +103,24 @@ export function NutritionRadarCard({
|
|||||||
return selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
|
return selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
|
||||||
}, [selectedDate]);
|
}, [selectedDate]);
|
||||||
|
|
||||||
const cardData = useAppSelector(selectNutritionCardDataByDate(dateKey));
|
// 使用专用的选择器获取营养数据和基础代谢
|
||||||
const { nutritionSummary, healthData, basalMetabolism } = cardData;
|
const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(dateKey));
|
||||||
|
const basalMetabolism = useAppSelector(selectBasalMetabolismByDate(dateKey));
|
||||||
|
|
||||||
// 获取营养和健康数据
|
// 使用专用的hook获取运动消耗卡路里
|
||||||
|
const { activeCalories: effectiveActiveCalories, loading: activeCaloriesLoading } = useActiveCalories(selectedDate);
|
||||||
|
|
||||||
|
// 获取营养数据和基础代谢数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadNutritionCardData = async () => {
|
const loadNutritionCardData = async () => {
|
||||||
const targetDate = selectedDate || new Date();
|
const targetDate = selectedDate || new Date();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await dispatch(fetchCompleteNutritionCardData(targetDate)).unwrap();
|
await Promise.all([
|
||||||
|
dispatch(fetchDailyNutritionData(targetDate)).unwrap(),
|
||||||
|
dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(),
|
||||||
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('NutritionRadarCard: 获取营养卡片数据失败:', error);
|
console.error('NutritionRadarCard: 获取营养卡片数据失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -139,7 +147,6 @@ export function NutritionRadarCard({
|
|||||||
|
|
||||||
// 使用从HealthKit获取的数据,如果没有则使用默认值
|
// 使用从HealthKit获取的数据,如果没有则使用默认值
|
||||||
const effectiveBasalMetabolism = basalMetabolism || 0; // 基础代谢默认值
|
const effectiveBasalMetabolism = basalMetabolism || 0; // 基础代谢默认值
|
||||||
const effectiveActiveCalories = healthData?.activeCalories || 0; // 运动消耗卡路里
|
|
||||||
|
|
||||||
const remainingCalories = calculateRemainingCalories({
|
const remainingCalories = calculateRemainingCalories({
|
||||||
basalMetabolism: effectiveBasalMetabolism,
|
basalMetabolism: effectiveBasalMetabolism,
|
||||||
@@ -171,8 +178,8 @@ export function NutritionRadarCard({
|
|||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.radarContainer}>
|
<View style={styles.radarContainer}>
|
||||||
<SimpleRingProgress
|
<SimpleRingProgress
|
||||||
remainingCalories={loading ? 0 : remainingCalories}
|
remainingCalories={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
||||||
totalAvailable={loading ? 0 : effectiveBasalMetabolism + effectiveActiveCalories}
|
totalAvailable={(loading || activeCaloriesLoading) ? 0 : effectiveBasalMetabolism + effectiveActiveCalories}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -195,10 +202,10 @@ export function NutritionRadarCard({
|
|||||||
<Text style={styles.calorieSubtitle}>还能吃</Text>
|
<Text style={styles.calorieSubtitle}>还能吃</Text>
|
||||||
<View style={styles.remainingCaloriesContainer}>
|
<View style={styles.remainingCaloriesContainer}>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={loading ? 0 : remainingCalories}
|
value={(loading || activeCaloriesLoading) ? 0 : remainingCalories}
|
||||||
resetToken={resetToken}
|
resetToken={resetToken}
|
||||||
style={styles.mainValue}
|
style={styles.mainValue}
|
||||||
format={(v) => loading ? '--' : Math.round(v).toString()}
|
format={(v) => (loading || activeCaloriesLoading) ? '--' : Math.round(v).toString()}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.calorieUnit}>千卡</Text>
|
<Text style={styles.calorieUnit}>千卡</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -217,10 +224,10 @@ export function NutritionRadarCard({
|
|||||||
<Text style={styles.calculationLabel}>运动</Text>
|
<Text style={styles.calculationLabel}>运动</Text>
|
||||||
</View>
|
</View>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={loading ? 0 : effectiveActiveCalories}
|
value={activeCaloriesLoading ? 0 : effectiveActiveCalories}
|
||||||
resetToken={resetToken}
|
resetToken={resetToken}
|
||||||
style={styles.calculationValue}
|
style={styles.calculationValue}
|
||||||
format={(v) => loading ? '--' : Math.round(v).toString()}
|
format={(v) => activeCaloriesLoading ? '--' : Math.round(v).toString()}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.calculationText}> - </Text>
|
<Text style={styles.calculationText}> - </Text>
|
||||||
<View style={styles.calculationItem}>
|
<View style={styles.calculationItem}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
|
InteractionManager,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle
|
||||||
InteractionManager
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health';
|
import { fetchHourlyStepSamples, fetchStepCount, HourlyStepData } from '@/utils/health';
|
||||||
@@ -39,18 +39,12 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
logger.info('获取步数数据...');
|
logger.info('获取步数数据...');
|
||||||
|
|
||||||
// 先获取步数,立即更新UI
|
// 先获取步数,立即更新UI
|
||||||
const steps = await fetchStepCount(date);
|
const [steps, hourly] = await Promise.all([
|
||||||
|
fetchStepCount(date),
|
||||||
|
fetchHourlyStepSamples(date)
|
||||||
|
]);
|
||||||
setStepCount(steps);
|
setStepCount(steps);
|
||||||
|
setHourSteps(hourly);
|
||||||
// 使用 InteractionManager 在空闲时获取更复杂的小时数据
|
|
||||||
InteractionManager.runAfterInteractions(async () => {
|
|
||||||
try {
|
|
||||||
const hourly = await fetchHourlyStepSamples(date);
|
|
||||||
setHourSteps(hourly);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('获取小时步数数据失败:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('获取步数数据失败:', error);
|
logger.error('获取步数数据失败:', error);
|
||||||
|
|||||||
58
hooks/useActiveCalories.ts
Normal file
58
hooks/useActiveCalories.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { NativeModules } from 'react-native';
|
||||||
|
|
||||||
|
const { HealthKitManager } = NativeModules;
|
||||||
|
|
||||||
|
type HealthDataOptions = {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专用于获取运动消耗卡路里的hook
|
||||||
|
* 避免使用完整的healthData对象,提升性能
|
||||||
|
*/
|
||||||
|
export function useActiveCalories(selectedDate?: Date) {
|
||||||
|
const [activeCalories, setActiveCalories] = useState<number>(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchActiveCalories = useCallback(async (date: Date) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const options: HealthDataOptions = {
|
||||||
|
startDate: dayjs(date).startOf('day').toDate().toISOString(),
|
||||||
|
endDate: dayjs(date).endOf('day').toDate().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await HealthKitManager.getActiveEnergyBurned(options);
|
||||||
|
|
||||||
|
if (result && result.totalValue !== undefined) {
|
||||||
|
setActiveCalories(Math.round(result.totalValue));
|
||||||
|
} else {
|
||||||
|
setActiveCalories(0);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取运动消耗卡路里失败:', err);
|
||||||
|
setError(err instanceof Error ? err.message : '获取运动消耗卡路里失败');
|
||||||
|
setActiveCalories(0);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const targetDate = selectedDate || new Date();
|
||||||
|
fetchActiveCalories(targetDate);
|
||||||
|
}, [selectedDate, fetchActiveCalories]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeCalories,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: () => fetchActiveCalories(selectedDate || new Date())
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -326,22 +326,20 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
endDate = Date()
|
endDate = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 HKStatisticsQuery 代替 HKSampleQuery 来直接获取总和,避免处理大量样本
|
||||||
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
||||||
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
|
|
||||||
|
|
||||||
let query = HKSampleQuery(sampleType: basalEnergyType,
|
let query = HKStatisticsQuery(quantityType: basalEnergyType,
|
||||||
predicate: predicate,
|
quantitySamplePredicate: predicate,
|
||||||
limit: HKObjectQueryNoLimit,
|
options: .cumulativeSum) { [weak self] (query, statistics, error) in
|
||||||
sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
rejecter("QUERY_ERROR", "Failed to query basal energy: \(error.localizedDescription)", error)
|
rejecter("QUERY_ERROR", "Failed to query basal energy: \(error.localizedDescription)", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let energySamples = samples as? [HKQuantitySample] else {
|
guard let statistics = statistics else {
|
||||||
resolver([
|
resolver([
|
||||||
"data": [],
|
|
||||||
"totalValue": 0,
|
"totalValue": 0,
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
@@ -349,28 +347,10 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let energyData = energySamples.map { sample in
|
let totalValue = statistics.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? 0
|
||||||
[
|
|
||||||
"id": sample.uuid.uuidString,
|
|
||||||
"startDate": self?.dateToISOString(sample.startDate) ?? "",
|
|
||||||
"endDate": self?.dateToISOString(sample.endDate) ?? "",
|
|
||||||
"value": sample.quantity.doubleValue(for: HKUnit.kilocalorie()),
|
|
||||||
"source": [
|
|
||||||
"name": sample.sourceRevision.source.name,
|
|
||||||
"bundleIdentifier": sample.sourceRevision.source.bundleIdentifier
|
|
||||||
],
|
|
||||||
"metadata": sample.metadata ?? [:]
|
|
||||||
] as [String : Any]
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalValue = energySamples.reduce(0.0) { total, sample in
|
|
||||||
return total + sample.quantity.doubleValue(for: HKUnit.kilocalorie())
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: [String: Any] = [
|
let result: [String: Any] = [
|
||||||
"data": energyData,
|
|
||||||
"totalValue": totalValue,
|
"totalValue": totalValue,
|
||||||
"count": energyData.count,
|
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
]
|
]
|
||||||
@@ -872,22 +852,20 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
endDate = Date()
|
endDate = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 HKStatisticsQuery 代替 HKSampleQuery 来直接获取步数总和,提高性能
|
||||||
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
||||||
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
|
|
||||||
|
|
||||||
let query = HKSampleQuery(sampleType: stepType,
|
let query = HKStatisticsQuery(quantityType: stepType,
|
||||||
predicate: predicate,
|
quantitySamplePredicate: predicate,
|
||||||
limit: HKObjectQueryNoLimit,
|
options: .cumulativeSum) { [weak self] (query, statistics, error) in
|
||||||
sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
rejecter("QUERY_ERROR", "Failed to query step count: \(error.localizedDescription)", error)
|
rejecter("QUERY_ERROR", "Failed to query step count: \(error.localizedDescription)", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let stepSamples = samples as? [HKQuantitySample] else {
|
guard let statistics = statistics else {
|
||||||
resolver([
|
resolver([
|
||||||
"data": [],
|
|
||||||
"totalValue": 0,
|
"totalValue": 0,
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
@@ -895,28 +873,10 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stepData = stepSamples.map { sample in
|
let totalValue = statistics.sumQuantity()?.doubleValue(for: HKUnit.count()) ?? 0
|
||||||
[
|
|
||||||
"id": sample.uuid.uuidString,
|
|
||||||
"startDate": self?.dateToISOString(sample.startDate) ?? "",
|
|
||||||
"endDate": self?.dateToISOString(sample.endDate) ?? "",
|
|
||||||
"value": sample.quantity.doubleValue(for: HKUnit.count()),
|
|
||||||
"source": [
|
|
||||||
"name": sample.sourceRevision.source.name,
|
|
||||||
"bundleIdentifier": sample.sourceRevision.source.bundleIdentifier
|
|
||||||
],
|
|
||||||
"metadata": sample.metadata ?? [:]
|
|
||||||
] as [String : Any]
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalValue = stepSamples.reduce(0.0) { total, sample in
|
|
||||||
return total + sample.quantity.doubleValue(for: HKUnit.count())
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: [String: Any] = [
|
let result: [String: Any] = [
|
||||||
"data": stepData,
|
|
||||||
"totalValue": totalValue,
|
"totalValue": totalValue,
|
||||||
"count": stepData.count,
|
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ target 'OutLive' do
|
|||||||
|
|
||||||
use_react_native!(
|
use_react_native!(
|
||||||
:path => config[:reactNativePath],
|
:path => config[:reactNativePath],
|
||||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
:hermes_enabled => false,
|
||||||
# An absolute path to your application root.
|
# An absolute path to your application root.
|
||||||
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||||
|
|||||||
@@ -2770,6 +2770,6 @@ SPEC CHECKSUMS:
|
|||||||
Yoga: 051f086b5ccf465ff2ed38a2cf5a558ae01aaaa1
|
Yoga: 051f086b5ccf465ff2ed38a2cf5a558ae01aaaa1
|
||||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||||
|
|
||||||
PODFILE CHECKSUM: 857afe46eb91e5007e03cd06568df19c8c00dc3e
|
PODFILE CHECKSUM: 78eca51725b1f0fcd006b70b9a09e3fb4f960d03
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
Reference in New Issue
Block a user