245 lines
6.0 KiB
TypeScript
245 lines
6.0 KiB
TypeScript
import { fetchHRVForDate } from '@/utils/health';
|
||
import dayjs from 'dayjs';
|
||
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) {
|
||
// 格式化更新时间显示
|
||
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值范围: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(data)
|
||
}
|
||
} 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}>
|
||
<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={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,
|
||
},
|
||
title: {
|
||
fontSize: 14,
|
||
color: '#192126',
|
||
},
|
||
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',
|
||
},
|
||
});
|