Files
digital-pilates/components/StressMeter.tsx

221 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 dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StressAnalysisModal } from './StressAnalysisModal';
interface StressMeterProps {
value: number | null;
updateTime?: Date;
style?: any;
hrvValue: number;
}
export function StressMeter({ value, updateTime, style, hrvValue }: 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(110, hrv));
const stressIndex = 100 - ((normalizedHrv - 30) / (110 - 30)) * 100;
return Math.round(stressIndex);
};
// 使用传入的 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, style]}
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}>{stressIndex === null ? '--' : stressIndex}</Text>
<Text style={styles.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={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',
},
});