236 lines
5.9 KiB
TypeScript
236 lines
5.9 KiB
TypeScript
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',
|
||
},
|
||
});
|