feat(health): 优化HRV数据质量分析与获取逻辑
- 新增HRV质量评分算法,综合评估数值有效性、数据源可靠性与元数据完整性 - 实现最佳质量HRV值自动选取,优先手动测量并过滤异常值 - 扩展TS类型定义,支持完整HRV数据结构及质量分析接口 - 移除StressMeter中未使用的时间格式化函数与注释代码 - 默认采样数提升至50条,增强质量分析准确性
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { fetchHRVForDate } from '@/utils/health';
|
import { fetchHRVForDate } from '@/utils/health';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -11,26 +10,6 @@ interface StressMeterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StressMeter({ curDate }: 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值转换为压力指数(0-100)
|
||||||
// HRV值范围:30-110ms,映射到压力指数100-0
|
// HRV值范围:30-110ms,映射到压力指数100-0
|
||||||
@@ -58,7 +37,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
|
|||||||
const data = await fetchHRVForDate(curDate)
|
const data = await fetchHRVForDate(curDate)
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setHrvValue(data)
|
setHrvValue(Math.round(data.value))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
@@ -138,7 +117,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
|
|||||||
visible={showStressModal}
|
visible={showStressModal}
|
||||||
onClose={() => setShowStressModal(false)}
|
onClose={() => setShowStressModal(false)}
|
||||||
hrvValue={hrvValue}
|
hrvValue={hrvValue}
|
||||||
// updateTime={updateTime || new Date()}
|
updateTime={new Date()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -623,7 +623,9 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
|
|
||||||
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 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,
|
let query = HKSampleQuery(sampleType: hrvType,
|
||||||
predicate: predicate,
|
predicate: predicate,
|
||||||
@@ -639,6 +641,7 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
resolver([
|
resolver([
|
||||||
"data": [],
|
"data": [],
|
||||||
"count": 0,
|
"count": 0,
|
||||||
|
"bestQualityValue": nil,
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
])
|
])
|
||||||
@@ -646,22 +649,31 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hrvData = hrvSamples.map { sample in
|
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,
|
"id": sample.uuid.uuidString,
|
||||||
"startDate": self?.dateToISOString(sample.startDate) ?? "",
|
"startDate": self?.dateToISOString(sample.startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(sample.endDate) ?? "",
|
"endDate": self?.dateToISOString(sample.endDate) ?? "",
|
||||||
"value": sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli)),
|
"value": hrvValueMs,
|
||||||
"source": [
|
"source": [
|
||||||
"name": sample.sourceRevision.source.name,
|
"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]
|
] as [String : Any]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算最佳质量的HRV值
|
||||||
|
let bestQualityValue = self?.getBestQualityHRVValue(from: hrvData)
|
||||||
|
|
||||||
let result: [String: Any] = [
|
let result: [String: Any] = [
|
||||||
"data": hrvData,
|
"data": hrvData,
|
||||||
"count": hrvData.count,
|
"count": hrvData.count,
|
||||||
|
"bestQualityValue": bestQualityValue,
|
||||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||||
]
|
]
|
||||||
@@ -1031,6 +1043,130 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
|||||||
return formatter.string(from: date)
|
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
|
// MARK: - Hourly Data Methods
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|||||||
244
utils/health.ts
244
utils/health.ts
@@ -289,6 +289,27 @@ function validateHeartRate(value: any): number | null {
|
|||||||
return 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<number> {
|
export async function fetchStepCount(date: Date): Promise<number> {
|
||||||
try {
|
try {
|
||||||
@@ -487,7 +508,7 @@ export async function fetchBasalEnergyBurned(options: HealthDataOptions): Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
|
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<HRVData | null> {
|
||||||
try {
|
try {
|
||||||
console.log('=== 开始获取HRV数据 ===');
|
console.log('=== 开始获取HRV数据 ===');
|
||||||
console.log('查询选项:', options);
|
console.log('查询选项:', options);
|
||||||
@@ -496,14 +517,67 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise<nu
|
|||||||
console.log('HRV API调用结果:', result);
|
console.log('HRV API调用结果:', result);
|
||||||
|
|
||||||
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
|
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
|
||||||
const hrvValue = result.data[0].value;
|
let selectedSample: any = null;
|
||||||
logSuccess('HRV数据', result);
|
|
||||||
return Math.round(hrvValue); // Value already in ms from native
|
console.log('result~~~', result);
|
||||||
} else {
|
|
||||||
logWarning('HRV', '为空或格式错误');
|
// 优先使用优化后的最佳质量值对应的样本
|
||||||
console.warn('HRV数据为空,原始响应:', result);
|
if (result.bestQualityValue && typeof result.bestQualityValue === 'number') {
|
||||||
return null;
|
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) {
|
} catch (error) {
|
||||||
logError('HRV数据', error);
|
logError('HRV数据', error);
|
||||||
console.error('HRV获取错误详情:', error);
|
console.error('HRV获取错误详情:', error);
|
||||||
@@ -657,18 +731,18 @@ export async function fetchTodayHealthData(): Promise<TodayHealthData> {
|
|||||||
return fetchHealthDataForDate(dayjs().toDate());
|
return fetchHealthDataForDate(dayjs().toDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchHRVForDate(date: Date): Promise<number | null> {
|
export async function fetchHRVForDate(date: Date): Promise<HRVData | null> {
|
||||||
console.log('开始获取指定日期HRV数据...', date);
|
console.log('开始获取指定日期HRV数据...', date);
|
||||||
const options = createDateRange(date);
|
const options = createDateRange(date);
|
||||||
return fetchHeartRateVariability(options);
|
return fetchHeartRateVariability(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTodayHRV(): Promise<number | null> {
|
export async function fetchTodayHRV(): Promise<HRVData | null> {
|
||||||
return fetchHRVForDate(dayjs().toDate());
|
return fetchHRVForDate(dayjs().toDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最近几小时内的实时HRV数据
|
// 获取最近几小时内的实时HRV数据
|
||||||
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
|
export async function fetchRecentHRV(hoursBack: number = 2): Promise<HRVData | null> {
|
||||||
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
|
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -697,18 +771,63 @@ export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise<v
|
|||||||
// 测试不同时间范围的HRV数据
|
// 测试不同时间范围的HRV数据
|
||||||
const options = createDateRange(date);
|
const options = createDateRange(date);
|
||||||
|
|
||||||
// 获取今日HRV
|
// 获取今日HRV(带详细分析)
|
||||||
|
console.log('--- 测试今日HRV ---');
|
||||||
|
const result = await HealthKitManager.getHeartRateVariabilitySamples(options);
|
||||||
|
console.log('原始HRV API响应:', result);
|
||||||
|
|
||||||
|
if (result && result.data && Array.isArray(result.data)) {
|
||||||
|
console.log(`获取到 ${result.data.length} 个HRV样本`);
|
||||||
|
|
||||||
|
// 分析数据质量
|
||||||
|
result.data.forEach((sample: any, index: number) => {
|
||||||
|
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);
|
const todayHRV = await fetchHeartRateVariability(options);
|
||||||
console.log('今日HRV结果:', todayHRV);
|
console.log('最终HRV结果:', todayHRV);
|
||||||
|
|
||||||
// 获取最近2小时HRV
|
// 获取最近2小时HRV
|
||||||
|
console.log('--- 测试最近2小时HRV ---');
|
||||||
const recentHRV = await fetchRecentHRV(2);
|
const recentHRV = await fetchRecentHRV(2);
|
||||||
console.log('最近2小时HRV结果:', recentHRV);
|
console.log('最近2小时HRV结果:', recentHRV);
|
||||||
|
|
||||||
// 获取指定日期HRV
|
// 获取指定日期HRV
|
||||||
|
console.log('--- 测试指定日期HRV ---');
|
||||||
const dateHRV = await fetchHRVForDate(date);
|
const dateHRV = await fetchHRVForDate(date);
|
||||||
console.log('指定日期HRV结果:', dateHRV);
|
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数据测试完成 ===');
|
console.log('=== HRV数据测试完成 ===');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('HRV测试过程中出现错误:', error);
|
console.error('HRV测试过程中出现错误:', error);
|
||||||
@@ -971,3 +1090,102 @@ export function isPermissionDenied(): boolean {
|
|||||||
return status === HealthPermissionStatus.Denied;
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user