Files
digital-pilates/components/StressMeter.tsx
richarjiang 21e57634e0 feat(hrv): 添加心率变异性监控和压力评估功能
- 新增 HRV 监听服务,实时监控心率变异性数据
- 实现 HRV 到压力指数的转换算法和压力等级评估
- 添加智能通知服务,在压力偏高时推送健康建议
- 优化日志系统,修复日志丢失问题并增强刷新机制
- 改进个人页面下拉刷新,支持并行数据加载
- 优化勋章数据缓存策略,减少不必要的网络请求
- 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序
- 移除冗余日志输出,提升应用性能
2025-11-18 14:08:20 +08:00

229 lines
5.6 KiB
TypeScript
Raw Permalink 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 { fetchHRVWithStatus } from '@/utils/health';
import { convertHrvToStressIndex } from '@/utils/stress';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StressAnalysisModal } from './StressAnalysisModal';
interface StressMeterProps {
curDate: Date
}
export function StressMeter({ curDate }: StressMeterProps) {
const { t } = useTranslation();
const [hrvValue, setHrvValue] = useState(0)
useEffect(() => {
getHrvData()
}, [curDate])
const getHrvData = async () => {
try {
console.log('StressMeter: Starting to get HRV data...', curDate);
// 使用智能HRV数据获取功能
const result = await fetchHRVWithStatus(curDate);
console.log('StressMeter: HRV data fetch result:', result);
if (result.hrvData) {
setHrvValue(Math.round(result.hrvData.value));
console.log(`StressMeter: Using ${result.message}, HRV value: ${result.hrvData.value}ms`);
} else {
console.log('StressMeter: No HRV data obtained');
// 可以设置一个默认值或者显示无数据状态
setHrvValue(0);
}
} catch (error) {
console.error('StressMeter: Failed to get HRV data:', error);
setHrvValue(0);
}
}
// 使用传入的 hrvValue 进行转换
const stressIndex = convertHrvToStressIndex(hrvValue);
// 计算进度条位置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}>{t('statistics.components.stress.title')}</Text>
</View>
{/* {updateTime && (
<Text style={styles.headerUpdateTime}>{formatUpdateTime(updateTime)}</Text>
)} */}
</View>
{/* 数值显示区域 */}
<View style={styles.valueSection}>
<Text style={styles.value}>{hrvValue || '--'}</Text>
<Text>{t('statistics.components.stress.unit')}</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',
},
});