Files
digital-pilates/components/StressMeter.tsx

236 lines
5.9 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 { 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)
const [updateTime, setUpdateTime] = useState<Date>(new Date())
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));
if (result.hrvData.recordedAt) {
setUpdateTime(new Date(result.hrvData.recordedAt));
}
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={['#10B981', '#FCD34D', '#EF4444']}
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={updateTime}
/>
</>
);
}
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',
fontFamily: 'AliBold',
},
valueSection: {
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 12,
},
value: {
fontSize: 20,
fontWeight: '600',
color: '#192126',
lineHeight: 20,
marginTop: 2,
fontFamily: 'AliBold',
},
unit: {
fontSize: 12,
fontWeight: '500',
color: '#9AA3AE',
marginLeft: 4,
fontFamily: 'AliRegular',
},
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',
},
});