diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index 97b6d68..7bef5bf 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -1,5 +1,4 @@ import { fetchHRVForDate } from '@/utils/health'; -import dayjs from 'dayjs'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useState } from 'react'; @@ -11,26 +10,6 @@ interface StressMeterProps { } export function StressMeter({ curDate }: StressMeterProps) { - // 格式化更新时间显示 - const formatUpdateTime = (date: Date): string => { - const now = dayjs(); - const updateTime = dayjs(date); - const diffMinutes = now.diff(updateTime, 'minute'); - const diffHours = now.diff(updateTime, 'hour'); - const diffDays = now.diff(updateTime, 'day'); - - if (diffMinutes < 1) { - return '刚刚更新'; - } else if (diffMinutes < 60) { - return `${diffMinutes}分钟前更新`; - } else if (diffHours < 24) { - return `${diffHours}小时前更新`; - } else if (diffDays < 7) { - return `${diffDays}天前更新`; - } else { - return updateTime.format('MM-DD HH:mm'); - } - }; // 将HRV值转换为压力指数(0-100) // HRV值范围:30-110ms,映射到压力指数100-0 @@ -58,7 +37,7 @@ export function StressMeter({ curDate }: StressMeterProps) { const data = await fetchHRVForDate(curDate) if (data) { - setHrvValue(data) + setHrvValue(Math.round(data.value)) } } catch (error) { @@ -138,7 +117,7 @@ export function StressMeter({ curDate }: StressMeterProps) { visible={showStressModal} onClose={() => setShowStressModal(false)} hrvValue={hrvValue} - // updateTime={updateTime || new Date()} + updateTime={new Date()} /> ); diff --git a/ios/OutLive/HealthKitManager.swift b/ios/OutLive/HealthKitManager.swift index e285d28..e1d74fb 100644 --- a/ios/OutLive/HealthKitManager.swift +++ b/ios/OutLive/HealthKitManager.swift @@ -623,7 +623,9 @@ class HealthKitManager: NSObject, RCTBridgeModule { let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit + + // 获取更多样本用于质量分析,默认50个样本 + let limit = options["limit"] as? Int ?? 50 let query = HKSampleQuery(sampleType: hrvType, predicate: predicate, @@ -639,6 +641,7 @@ class HealthKitManager: NSObject, RCTBridgeModule { resolver([ "data": [], "count": 0, + "bestQualityValue": nil, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) @@ -646,22 +649,31 @@ class HealthKitManager: NSObject, RCTBridgeModule { } let hrvData = hrvSamples.map { sample in - [ + let hrvValueMs = sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli)) + let sourceBundle = sample.sourceRevision.source.bundleIdentifier + + return [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", - "value": sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli)), + "value": hrvValueMs, "source": [ "name": sample.sourceRevision.source.name, - "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier + "bundleIdentifier": sourceBundle ], - "metadata": sample.metadata ?? [:] + "metadata": sample.metadata ?? [:], + "isManualMeasurement": self?.isManualHRVMeasurement(sourceBundle: sourceBundle, metadata: sample.metadata) ?? false, + "qualityScore": self?.calculateHRVQualityScore(value: hrvValueMs, sourceBundle: sourceBundle, metadata: sample.metadata) ?? 0 ] as [String : Any] } + // 计算最佳质量的HRV值 + let bestQualityValue = self?.getBestQualityHRVValue(from: hrvData) + let result: [String: Any] = [ "data": hrvData, "count": hrvData.count, + "bestQualityValue": bestQualityValue, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] @@ -1031,6 +1043,130 @@ class HealthKitManager: NSObject, RCTBridgeModule { return formatter.string(from: date) } + // MARK: - HRV Quality Analysis Methods + + /// 判断是否为手动/高质量HRV测量 + private func isManualHRVMeasurement(sourceBundle: String, metadata: [String: Any]?) -> Bool { + // 来自呼吸应用的测量通常是手动触发的高质量测量 + if sourceBundle.contains("Breathe") || sourceBundle.contains("breathe") { + return true + } + + // 来自第三方HRV应用的测量通常是手动的 + let manualHRVApps = ["HRV4Training", "EliteHRV", "HRVLogger", "Stress & Anxiety Companion"] + if manualHRVApps.contains(where: { sourceBundle.contains($0) }) { + return true + } + + // 检查元数据中的手动测量标识 + if let metadata = metadata { + if let wasUserEntered = metadata[HKMetadataKeyWasUserEntered] as? Bool, wasUserEntered { + return true + } + } + + return false + } + + /// 计算HRV测量的质量评分 + private func calculateHRVQualityScore(value: Double, sourceBundle: String, metadata: [String: Any]?) -> Int { + var score = 0 + + // 1. 数值有效性检查 (0-40分) + if value >= 10 && value <= 100 { + // 正常SDNN范围,给予基础分数 + if value >= 18 && value <= 76 { + score += 40 // 完全正常范围 + } else if value >= 10 && value <= 18 { + score += 30 // 偏低但可能有效 + } else if value >= 76 && value <= 100 { + score += 35 // 偏高但可能有效 + } + } else if value > 0 && value < 10 { + score += 10 // 数值过低,质量存疑 + } + + // 2. 数据源质量 (0-35分) + if isManualHRVMeasurement(sourceBundle: sourceBundle, metadata: metadata) { + score += 35 // 手动测量,质量最高 + } else if sourceBundle.contains("com.apple.health") { + score += 20 // 系统自动测量,中等质量 + } else if sourceBundle.contains("Watch") { + score += 25 // Apple Watch测量,较好质量 + } else { + score += 15 // 其他来源,质量一般 + } + + // 3. 元数据质量指标 (0-25分) + if let metadata = metadata { + var metadataScore = 0 + + // 用户手动输入 + if let wasUserEntered = metadata[HKMetadataKeyWasUserEntered] as? Bool, wasUserEntered { + metadataScore += 15 + } + + // 设备信息完整性 + if metadata[HKMetadataKeyDeviceName] != nil { + metadataScore += 5 + } + + // 其他质量指标 + if metadata[HKMetadataKeyHeartRateMotionContext] != nil { + metadataScore += 5 + } + + score += metadataScore + } + + return min(score, 100) // 限制最大分数为100 + } + + /// 从HRV数据中获取最佳质量的测量值 + private func getBestQualityHRVValue(from hrvData: [[String: Any]]) -> Double? { + guard !hrvData.isEmpty else { return nil } + + // 按质量分数和时间排序,优先选择高质量的最新测量 + let sortedData = hrvData.sorted { item1, item2 in + let quality1 = item1["qualityScore"] as? Int ?? 0 + let quality2 = item2["qualityScore"] as? Int ?? 0 + let isManual1 = item1["isManualMeasurement"] as? Bool ?? false + let isManual2 = item2["isManualMeasurement"] as? Bool ?? false + + // 优先级:手动测量 > 质量分数 > 时间新旧 + if isManual1 && !isManual2 { + return true + } else if !isManual1 && isManual2 { + return false + } else if quality1 != quality2 { + return quality1 > quality2 + } else { + // 同等质量下,选择更新的数据 + let date1 = item1["endDate"] as? String ?? "" + let date2 = item2["endDate"] as? String ?? "" + return date1 > date2 + } + } + + // 返回质量最高的测量值 + if let bestValue = sortedData.first?["value"] as? Double { + // 对最终值进行合理性验证 + if bestValue >= 5 && bestValue <= 150 { + return bestValue + } + } + + // 如果最佳值不合理,尝试返回第一个合理的值 + for data in sortedData { + if let value = data["value"] as? Double, value >= 10 && value <= 100 { + return value + } + } + + // 如果都没有合理值,返回第一个值(可能需要用户注意数据质量) + return sortedData.first?["value"] as? Double + } + // MARK: - Hourly Data Methods @objc diff --git a/utils/health.ts b/utils/health.ts index f152222..7c413d9 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -289,6 +289,27 @@ function validateHeartRate(value: any): number | null { return null; } +function validateHRVValue(value: any): number | null { + if (value === undefined || value === null) return null; + + const numValue = Number(value); + + // HRV SDNN 正常范围检查 + // 正常范围: 18-76ms,但允许更宽范围 5-150ms 以包含边缘情况 + if (numValue >= 5 && numValue <= 150) { + // 保留1位小数的精度,避免过度舍入 + return Math.round(numValue * 10) / 10; + } + + // 记录异常值用于调试 + console.warn('HRV数据超出合理范围:', { + value: numValue, + expectedRange: '5-150ms', + normalRange: '18-76ms' + }); + return null; +} + // 健康数据获取函数 export async function fetchStepCount(date: Date): Promise { try { @@ -487,7 +508,7 @@ export async function fetchBasalEnergyBurned(options: HealthDataOptions): Promis } } -async function fetchHeartRateVariability(options: HealthDataOptions): Promise { +async function fetchHeartRateVariability(options: HealthDataOptions): Promise { try { console.log('=== 开始获取HRV数据 ==='); console.log('查询选项:', options); @@ -496,14 +517,67 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise 0) { - const hrvValue = result.data[0].value; - logSuccess('HRV数据', result); - return Math.round(hrvValue); // Value already in ms from native - } else { - logWarning('HRV', '为空或格式错误'); - console.warn('HRV数据为空,原始响应:', result); - return null; + let selectedSample: any = null; + + console.log('result~~~', result); + + // 优先使用优化后的最佳质量值对应的样本 + if (result.bestQualityValue && typeof result.bestQualityValue === 'number') { + const qualityValue = validateHRVValue(result.bestQualityValue); + if (qualityValue !== null) { + // 找到对应的最佳质量样本 + selectedSample = result.data[result.data.length - 1]; + + logSuccess('HRV数据(最佳质量)', { + value: qualityValue, + totalSamples: result.data.length, + recordedAt: selectedSample.endDate + }); + } + } + + // 如果没有找到最佳质量样本,使用第一个有效样本 + if (!selectedSample) { + for (const sample of result.data) { + const sampleValue = validateHRVValue(sample.value); + if (sampleValue !== null) { + selectedSample = sample; + console.log('使用有效HRV样本:', { + value: sampleValue, + source: sample.source?.name, + recordedAt: sample.endDate + }); + break; + } + } + } + + // 构建完整的HRV数据对象 + if (selectedSample) { + const validatedValue = validateHRVValue(selectedSample.value); + if (validatedValue !== null) { + const hrvData: HRVData = { + value: validatedValue, + recordedAt: selectedSample.startDate, + endDate: selectedSample.endDate, + source: { + name: selectedSample.source?.name || 'Unknown', + bundleIdentifier: selectedSample.source?.bundleIdentifier || '' + }, + isManualMeasurement: selectedSample.isManualMeasurement || false, + qualityScore: selectedSample.qualityScore, + sampleId: selectedSample.id + }; + + logSuccess('HRV完整数据', hrvData); + return hrvData; + } + } } + + logWarning('HRV', '为空或格式错误'); + console.warn('HRV数据为空或无效,原始响应:', result); + return null; } catch (error) { logError('HRV数据', error); console.error('HRV获取错误详情:', error); @@ -657,18 +731,18 @@ export async function fetchTodayHealthData(): Promise { return fetchHealthDataForDate(dayjs().toDate()); } -export async function fetchHRVForDate(date: Date): Promise { +export async function fetchHRVForDate(date: Date): Promise { console.log('开始获取指定日期HRV数据...', date); const options = createDateRange(date); return fetchHeartRateVariability(options); } -export async function fetchTodayHRV(): Promise { +export async function fetchTodayHRV(): Promise { return fetchHRVForDate(dayjs().toDate()); } // 获取最近几小时内的实时HRV数据 -export async function fetchRecentHRV(hoursBack: number = 2): Promise { +export async function fetchRecentHRV(hoursBack: number = 2): Promise { console.log(`开始获取最近${hoursBack}小时内的HRV数据...`); const now = new Date(); @@ -697,18 +771,63 @@ export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise { + console.log(`样本 ${index + 1}:`, { + value: sample.value, + source: sample.source?.name, + bundleId: sample.source?.bundleIdentifier, + isManual: sample.isManualMeasurement, + qualityScore: sample.qualityScore, + startDate: sample.startDate, + endDate: sample.endDate + }); + }); + + if (result.bestQualityValue !== undefined) { + console.log('最佳质量HRV值:', result.bestQualityValue); + } + } + + // 使用优化后的方法获取HRV const todayHRV = await fetchHeartRateVariability(options); - console.log('今日HRV结果:', todayHRV); + console.log('最终HRV结果:', todayHRV); // 获取最近2小时HRV + console.log('--- 测试最近2小时HRV ---'); const recentHRV = await fetchRecentHRV(2); console.log('最近2小时HRV结果:', recentHRV); // 获取指定日期HRV + console.log('--- 测试指定日期HRV ---'); const dateHRV = await fetchHRVForDate(date); console.log('指定日期HRV结果:', dateHRV); + // 提供数据解释 + if (todayHRV) { + console.log('--- HRV数据解读 ---'); + console.log(`HRV值: ${todayHRV.value}ms`); + console.log(`记录时间: ${todayHRV.recordedAt}`); + console.log(`数据来源: ${todayHRV.source.name}`); + console.log(`手动测量: ${todayHRV.isManualMeasurement ? '是' : '否'}`); + + if (todayHRV.value >= 18 && todayHRV.value <= 76) { + console.log('✅ HRV值在正常范围内 (18-76ms)'); + } else if (todayHRV.value < 18) { + console.log('⚠️ HRV值偏低,可能表示压力或疲劳状态'); + } else if (todayHRV.value > 76) { + console.log('📈 HRV值较高,通常表示良好的恢复状态'); + } + } + console.log('=== HRV数据测试完成 ==='); } catch (error) { console.error('HRV测试过程中出现错误:', error); @@ -971,3 +1090,102 @@ export function isPermissionDenied(): boolean { return status === HealthPermissionStatus.Denied; } +// HRV数据结构 +export interface HRVData { + value: number; + recordedAt: string; // ISO string format + endDate: string; // ISO string format + source: { + name: string; + bundleIdentifier: string; + }; + isManualMeasurement: boolean; + qualityScore?: number; + sampleId?: string; +} + +// HRV数据质量分析和解读 +export interface HRVAnalysis { + value: number; + quality: 'excellent' | 'good' | 'fair' | 'poor'; + interpretation: string; + recommendations: string[]; + dataSource: string; + isManualMeasurement: boolean; + recordedAt: string; +} + +export function analyzeHRVData(hrvData: HRVData): HRVAnalysis { + const { value: hrvValue, source, isManualMeasurement, recordedAt } = hrvData; + const sourceName = source.name; + + let quality: HRVAnalysis['quality']; + let interpretation: string; + let recommendations: string[] = []; + + // 质量评估基于数值范围和数据来源 + if (hrvValue >= 18 && hrvValue <= 76) { + if (isManualMeasurement) { + quality = 'excellent'; + interpretation = 'HRV值在正常范围内,且来自高质量测量'; + } else { + quality = 'good'; + interpretation = 'HRV值在正常范围内'; + } + } else if (hrvValue >= 10 && hrvValue < 18) { + quality = 'fair'; + interpretation = 'HRV值偏低,可能表示压力、疲劳或恢复不足'; + recommendations.push('考虑增加休息和恢复时间'); + recommendations.push('评估近期的压力水平和睡眠质量'); + } else if (hrvValue > 76 && hrvValue <= 100) { + quality = isManualMeasurement ? 'excellent' : 'good'; + interpretation = 'HRV值较高,通常表示良好的心血管健康和恢复状态'; + recommendations.push('保持当前的生活方式和训练强度'); + } else if (hrvValue < 10) { + quality = 'poor'; + interpretation = 'HRV值异常低,建议关注身体状态或数据准确性'; + recommendations.push('建议使用手动测量(如呼吸应用)获得更准确的数据'); + recommendations.push('如持续偏低,建议咨询医疗专业人士'); + } else if (hrvValue > 100) { + quality = 'fair'; + interpretation = 'HRV值异常高,可能需要验证数据准确性'; + recommendations.push('建议重复测量确认数据准确性'); + } else { + quality = 'poor'; + interpretation = 'HRV数据超出预期范围'; + recommendations.push('建议使用标准化的测量方法'); + } + + // 根据数据来源添加建议 + if (!isManualMeasurement) { + recommendations.push('推荐使用呼吸应用进行手动HRV测量以获得更准确的数据'); + } + + return { + value: hrvValue, + quality, + interpretation, + recommendations, + dataSource: sourceName, + isManualMeasurement, + recordedAt + }; +} + +// 获取HRV数据并提供分析 +export async function fetchHRVWithAnalysis(date: Date): Promise<{ hrvData: HRVData | null; analysis: HRVAnalysis | null }> { + try { + const hrvData = await fetchHRVForDate(date); + + if (hrvData) { + const analysis = analyzeHRVData(hrvData); + return { hrvData, analysis }; + } + + return { hrvData: null, analysis: null }; + } catch (error) { + console.error('获取HRV分析数据失败:', error); + return { hrvData: null, analysis: null }; + } +} +