Files
digital-pilates/components/StressMeter.tsx
richarjiang 83e534c4a7 feat(health): 优化HRV数据质量分析与获取逻辑
- 新增HRV质量评分算法,综合评估数值有效性、数据源可靠性与元数据完整性
- 实现最佳质量HRV值自动选取,优先手动测量并过滤异常值
- 扩展TS类型定义,支持完整HRV数据结构及质量分析接口
- 移除StressMeter中未使用的时间格式化函数与注释代码
- 默认采样数提升至50条,增强质量分析准确性
2025-09-24 18:29:58 +08:00

236 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fetchHRVForDate } from '@/utils/health';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StressAnalysisModal } from './StressAnalysisModal';
interface StressMeterProps {
curDate: Date
}
export function StressMeter({ curDate }: StressMeterProps) {
// 将HRV值转换为压力指数0-100
// HRV值范围30-110ms映射到压力指数100-0
// HRV值越高压力越小HRV值越低压力越大
const convertHrvToStressIndex = (hrv: number | null): number | null => {
if (hrv === null || hrv === 0) return null;
// HRV 范围: 30-110ms对应压力指数: 100-0
// 线性映射: stressIndex = 100 - ((hrv - 30) / (110 - 30)) * 100
const normalizedHrv = Math.max(30, Math.min(130, hrv));
const stressIndex = 100 - ((normalizedHrv - 30) / (130 - 30)) * 100;
return Math.round(stressIndex);
};
const [hrvValue, setHrvValue] = useState(0)
useEffect(() => {
getHrvData()
}, [curDate])
const getHrvData = async () => {
try {
const data = await fetchHRVForDate(curDate)
if (data) {
setHrvValue(Math.round(data.value))
}
} catch (error) {
}
}
// 使用传入的 hrvValue 进行转换
const stressIndex = convertHrvToStressIndex(hrvValue);
// 调试信息
console.log('StressMeter 调试:', {
hrvValue,
stressIndex,
progressPercentage: stressIndex !== null ? Math.max(0, Math.min(100, stressIndex)) : 0
});
// 计算进度条位置0-100%
// 压力指数越高,进度条越满(红色区域越多)
const progressPercentage = stressIndex !== null ? Math.max(0, Math.min(100, stressIndex)) : 0;
// 在组件内部添加状态
const [showStressModal, setShowStressModal] = useState(false);
// 修改 onPress 处理函数
const handlePress = () => {
setShowStressModal(true);
};
return (
<>
<TouchableOpacity
style={[styles.container]}
onPress={handlePress}
activeOpacity={0.8}
>
{/* 头部区域 */}
<View style={styles.header}>
<View style={styles.leftSection}>
<Image
source={require('@/assets/images/icons/icon-pressure.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}></Text>
</View>
{/* {updateTime && (
<Text style={styles.headerUpdateTime}>{formatUpdateTime(updateTime)}</Text>
)} */}
</View>
{/* 数值显示区域 */}
<View style={styles.valueSection}>
<Text style={styles.value}>{hrvValue || '--'}</Text>
<Text>ms</Text>
</View>
{/* 进度条区域 */}
<View style={styles.progressContainer}>
<View style={styles.progressTrack}>
{/* 渐变背景进度条 */}
<View style={[styles.progressBar, { width: '100%' }]}>
<LinearGradient
colors={['#EF4444', '#FCD34D', '#10B981']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.gradientBar}
/>
</View>
{/* 白色圆形指示器 */}
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage))}%` }]} />
</View>
</View>
</TouchableOpacity>
{/* 压力分析浮窗 */}
<StressAnalysisModal
visible={showStressModal}
onClose={() => setShowStressModal(false)}
hrvValue={hrvValue}
updateTime={new Date()}
/>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
position: 'relative',
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
leftSection: {
flexDirection: 'row',
alignItems: 'center',
},
iconContainer: {
width: 24,
height: 24,
borderRadius: 6,
backgroundColor: '#EBF4FF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 6,
},
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
title: {
fontSize: 14,
color: '#192126',
fontWeight: '600'
},
valueSection: {
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 12,
},
value: {
fontSize: 20,
fontWeight: '600',
color: '#192126',
lineHeight: 20,
marginTop: 2,
},
unit: {
fontSize: 12,
fontWeight: '500',
color: '#9AA3AE',
marginLeft: 4,
},
progressContainer: {
height: 6,
},
progressTrack: {
height: 6,
borderRadius: 4,
position: 'relative',
overflow: 'visible',
},
progressBar: {
height: '100%',
borderRadius: 4,
overflow: 'hidden',
},
gradientBar: {
height: '100%',
borderRadius: 4,
},
indicator: {
position: 'absolute',
top: -2,
width: 10,
height: 10,
borderRadius: 8,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
borderWidth: 1.5,
borderColor: '#E5E7EB',
},
updateTime: {
fontSize: 10,
color: '#9AA3AE',
textAlign: 'right',
marginTop: 2,
},
headerUpdateTime: {
fontSize: 11,
color: '#9AA3AE',
fontWeight: '500',
},
});