diff --git a/app.json b/app.json index 231e1f9..504ef7d 100644 --- a/app.json +++ b/app.json @@ -17,8 +17,11 @@ "NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。", "NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。", "NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。", + "NSUserNotificationsUsageDescription": "应用需要发送通知以提醒您喝水和站立活动。", "UIBackgroundModes": [ - "processing" + "processing", + "fetch", + "remote-notification" ] } }, @@ -55,7 +58,7 @@ "react-native-health", { "enableHealthAPI": true, - "healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。", + "healthSharePermission": "应用需要访问您的健康数据(步数、能量消耗、心率变异性等)以展示运动统计和压力分析。", "healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。" } ], diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 8ece39b..7450afb 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -17,7 +17,7 @@ import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/mo import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; -import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health'; +import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health'; import { getTestHealthData } from '@/utils/mockHealthData'; import { calculateNutritionGoals } from '@/utils/nutrition'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; @@ -460,15 +460,27 @@ export default function ExploreScreen() { {/* 开发环境调试按钮 */} {__DEV__ && ( - { - console.log('🔧 手动触发后台任务测试...'); - // await backgroundTaskManager.triggerTaskForTesting(); - }} - > - 🔧 - + + { + console.log('🔧 手动触发后台任务测试...'); + // await backgroundTaskManager.triggerTaskForTesting(); + }} + > + 🔧 + + + { + console.log('🫀 测试HRV数据获取...'); + await testHRVDataFetch(); + }} + > + 🫀 + + )} @@ -665,6 +677,10 @@ const styles = StyleSheet.create({ fontWeight: '500', color: '#192126', }, + debugButtonsContainer: { + flexDirection: 'row', + gap: 8, + }, debugButton: { width: 32, height: 32, @@ -681,6 +697,9 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 3, }, + hrvTestButton: { + backgroundColor: '#8B5CF6', + }, debugButtonText: { fontSize: 12, }, diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index 5ee464a..e7c5b69 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -82,7 +82,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP {/* 数值显示区域 */} {stressIndex === null ? '--' : stressIndex} - ms + % {/* 进度条区域 */} diff --git a/ios/WaterWidgetExtension.entitlements b/ios/WaterWidgetExtension.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/ios/WaterWidgetExtension.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index a7c4107..8f1c6a9 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -4,7 +4,7 @@ BGTaskSchedulerPermittedIdentifiers - com.anonymous.digitalpilates.backgroundfetch + background-health-reminders CADisableMinimumFrameDurationOnPhone @@ -23,7 +23,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.7 + 1.0.8 CFBundleSignature ???? CFBundleURLTypes @@ -54,13 +54,15 @@ NSCameraUsageDescription 应用需要使用相机以实现相关功能 NSHealthShareUsageDescription - 应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。 + 应用需要访问您的健康数据(步数、能量消耗、心率变异性等)以展示运动统计和压力分析。 NSHealthUpdateUsageDescription 应用需要更新您的健康数据(体重信息)以记录您的健身进度。 NSPhotoLibraryAddUsageDescription 应用需要写入相册以保存拍摄的体态照片(可选)。 NSPhotoLibraryUsageDescription 应用需要访问相册以选择您的体态照片用于AI测评。 + NSUserNotificationsUsageDescription + 应用需要发送通知以提醒您喝水和站立活动。 NSUserActivityTypes $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route @@ -68,7 +70,8 @@ UIBackgroundModes processing - fetch + fetch + remote-notification UILaunchStoryboardName SplashScreen diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts index c8eb95d..71af81a 100644 --- a/services/backgroundTaskManager.ts +++ b/services/backgroundTaskManager.ts @@ -3,6 +3,7 @@ import { StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notifica import AsyncStorage from '@react-native-async-storage/async-storage'; import * as BackgroundTask from 'expo-background-task'; import * as TaskManager from 'expo-task-manager'; +import { TaskManagerTaskBody } from 'expo-task-manager'; /** * 后台任务标识符 @@ -14,7 +15,7 @@ export const BACKGROUND_TASK_IDS = { } as const; // 定义后台任务 -TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async () => { +TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async (body: TaskManagerTaskBody) => { try { console.log('[BackgroundTask] 后台任务执行'); await executeBackgroundTasks(); diff --git a/utils/health.ts b/utils/health.ts index 486807d..c96881c 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -452,22 +452,43 @@ async function fetchSleepDuration(options: HealthDataOptions): Promise { async function fetchHeartRateVariability(options: HealthDataOptions): Promise { return new Promise((resolve) => { + console.log('=== 开始获取HRV数据 ==='); + console.log('查询选项:', options); + AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { + console.log('HRV API调用结果:', { err, res }); + if (err) { logError('HRV数据', err); + console.error('HRV获取错误详情:', err); return resolve(null); } + if (!res || !Array.isArray(res) || res.length === 0) { logWarning('HRV', '为空或格式错误'); + console.warn('HRV数据为空,原始响应:', res); return resolve(null); } + logSuccess('HRV', res); + console.log('HRV数据样本数量:', res.length); + + // 打印最新的几个样本用于调试 + const latestSamples = res.slice(-3); + console.log('最新的HRV样本:', latestSamples.map(sample => ({ + value: sample.value, + startDate: sample.startDate, + endDate: sample.endDate + }))); const latestHrv = res[res.length - 1]; - if (latestHrv && latestHrv.value) { + if (latestHrv && latestHrv.value !== undefined && latestHrv.value !== null) { // HealthKit 中的 HRV 数据已经是毫秒单位,无需转换 - resolve(Math.round(latestHrv.value)); + const hrvValue = Math.round(latestHrv.value); + console.log('最终HRV值:', hrvValue); + resolve(hrvValue); } else { + console.warn('HRV样本值无效:', latestHrv); resolve(null); } }); @@ -681,6 +702,41 @@ export async function fetchRecentHRV(hoursBack: number = 2): Promise { + console.log('=== 开始测试HRV数据获取 ==='); + + try { + // 首先确保权限 + const hasPermission = await ensureHealthPermissions(); + if (!hasPermission) { + console.error('没有健康数据权限,无法测试HRV'); + return; + } + + console.log('权限检查通过,开始获取HRV数据...'); + + // 测试不同时间范围的HRV数据 + const options = createDateRange(date); + + // 获取今日HRV + const todayHRV = await fetchHeartRateVariability(options); + console.log('今日HRV结果:', todayHRV); + + // 获取最近2小时HRV + const recentHRV = await fetchRecentHRV(2); + console.log('最近2小时HRV结果:', recentHRV); + + // 获取指定日期HRV + const dateHRV = await fetchHRVForDate(date); + console.log('指定日期HRV结果:', dateHRV); + + console.log('=== HRV数据测试完成 ==='); + } catch (error) { + console.error('HRV测试过程中出现错误:', error); + } +} + // 更新healthkit中的体重 export async function updateWeight(weight: number) { return new Promise((resolve) => {