495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
import { Colors } from '@/constants/Colors';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
import { fetchHRVSamples, HRVData } from '@/utils/health';
|
|
import { convertHrvToStressIndex, getStressLevelInfo } from '@/utils/stress';
|
|
import dayjs from 'dayjs';
|
|
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Modal,
|
|
Platform,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View
|
|
} from 'react-native';
|
|
|
|
interface StressAnalysisModalProps {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
hrvValue: number;
|
|
updateTime: Date;
|
|
}
|
|
|
|
interface StressStats {
|
|
percentage: number;
|
|
count: number;
|
|
range: string;
|
|
}
|
|
|
|
interface HistoryData {
|
|
goodEvents: StressStats;
|
|
energetic: StressStats;
|
|
stressed: StressStats;
|
|
totalSamples: number;
|
|
}
|
|
|
|
export function StressAnalysisModal({ visible, onClose, hrvValue, updateTime }: StressAnalysisModalProps) {
|
|
const colorScheme = useColorScheme();
|
|
const colors = Colors[colorScheme ?? 'light'];
|
|
const [loading, setLoading] = useState(true);
|
|
const [historyData, setHistoryData] = useState<HistoryData>({
|
|
goodEvents: { percentage: 0, count: 0, range: '>75毫秒' },
|
|
energetic: { percentage: 0, count: 0, range: '40-75毫秒' },
|
|
stressed: { percentage: 0, count: 0, range: '<40毫秒' },
|
|
totalSamples: 0
|
|
});
|
|
|
|
// 当前压力状态
|
|
const stressIndex = convertHrvToStressIndex(hrvValue);
|
|
const stressInfo = getStressLevelInfo(stressIndex);
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
loadHistoryData();
|
|
}
|
|
}, [visible]);
|
|
|
|
const loadHistoryData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const endDate = new Date();
|
|
const startDate = dayjs().subtract(30, 'day').toDate();
|
|
|
|
const samples = await fetchHRVSamples(startDate, endDate);
|
|
processHistoryData(samples);
|
|
} catch (error) {
|
|
console.error('Failed to load HRV history:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const processHistoryData = (samples: HRVData[]) => {
|
|
if (!samples.length) return;
|
|
|
|
let goodCount = 0;
|
|
let energeticCount = 0;
|
|
let stressedCount = 0;
|
|
|
|
samples.forEach(sample => {
|
|
const val = sample.value;
|
|
if (val > 75) {
|
|
goodCount++;
|
|
} else if (val >= 40) {
|
|
energeticCount++;
|
|
} else {
|
|
stressedCount++;
|
|
}
|
|
});
|
|
|
|
const total = samples.length;
|
|
|
|
setHistoryData({
|
|
goodEvents: {
|
|
percentage: Math.round((goodCount / total) * 100),
|
|
count: goodCount,
|
|
range: '>75毫秒'
|
|
},
|
|
energetic: {
|
|
percentage: Math.round((energeticCount / total) * 100),
|
|
count: energeticCount,
|
|
range: '40-75毫秒'
|
|
},
|
|
stressed: {
|
|
percentage: Math.round((stressedCount / total) * 100),
|
|
count: stressedCount,
|
|
range: '<40毫秒'
|
|
},
|
|
totalSamples: total
|
|
});
|
|
};
|
|
|
|
const getStatusColor = (level: string) => {
|
|
switch (level) {
|
|
case 'low': return '#10B981';
|
|
case 'moderate': return '#3B82F6';
|
|
case 'high': return '#F59E0B';
|
|
default: return colors.text;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
visible={visible}
|
|
animationType="slide"
|
|
presentationStyle="pageSheet"
|
|
onRequestClose={onClose}
|
|
>
|
|
<LinearGradient
|
|
colors={[colors.backgroundGradientStart, colors.backgroundGradientEnd]}
|
|
style={styles.modalContainer}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 0, y: 1 }}
|
|
>
|
|
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
|
{/* 标题区域 */}
|
|
<Text style={styles.title}>压力分析</Text>
|
|
|
|
{/* 当前状态卡片 */}
|
|
<View style={styles.currentStatusCard}>
|
|
<View style={styles.statusHeader}>
|
|
<Text style={styles.statusLabel}>当前状态</Text>
|
|
<Text style={styles.updateTime}>更新于 {dayjs(updateTime).format('HH:mm')}</Text>
|
|
</View>
|
|
|
|
<View style={styles.statusValueContainer}>
|
|
<View>
|
|
<Text style={[styles.statusText, { color: getStatusColor(stressInfo.level) }]}>
|
|
{stressInfo.label}
|
|
</Text>
|
|
<Text style={styles.statusDesc}>{stressInfo.description}</Text>
|
|
</View>
|
|
<View style={styles.hrvValueBox}>
|
|
<Text style={styles.hrvValueLabel}>HRV</Text>
|
|
<Text style={styles.hrvValue}>{Math.round(hrvValue)}<Text style={styles.hrvUnit}>ms</Text></Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
|
|
{/* 最近30天HRV情况 */}
|
|
<Text style={styles.sectionTitle}>最近30天压力分布</Text>
|
|
|
|
{loading ? (
|
|
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 20 }} />
|
|
) : (
|
|
<>
|
|
{/* 彩色横条图 */}
|
|
<View style={styles.chartContainer}>
|
|
<View style={styles.colorBar}>
|
|
{historyData.totalSamples > 0 ? (
|
|
<View style={styles.progressBarContainer}>
|
|
{historyData.stressed.percentage > 0 && (
|
|
<View style={[styles.progressSegment, { flex: historyData.stressed.percentage, backgroundColor: '#F59E0B', marginRight: 2 }]} />
|
|
)}
|
|
{historyData.energetic.percentage > 0 && (
|
|
<View style={[styles.progressSegment, { flex: historyData.energetic.percentage, backgroundColor: '#3B82F6', marginRight: 2 }]} />
|
|
)}
|
|
{historyData.goodEvents.percentage > 0 && (
|
|
<View style={[styles.progressSegment, { flex: historyData.goodEvents.percentage, backgroundColor: '#10B981' }]} />
|
|
)}
|
|
</View>
|
|
) : (
|
|
<View style={[styles.progressBarContainer, { backgroundColor: '#E5E7EB' }]} />
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.legend}>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendDot, { backgroundColor: '#F59E0B' }]} />
|
|
<Text style={styles.legendText}>鸭梨山大</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendDot, { backgroundColor: '#3B82F6' }]} />
|
|
<Text style={styles.legendText}>活力满满</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendDot, { backgroundColor: '#10B981' }]} />
|
|
<Text style={styles.legendText}>好事发生</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 数据统计卡片 */}
|
|
<View style={styles.statsCard}>
|
|
{/* 好事发生 & 活力满满 */}
|
|
<View style={styles.statsRow}>
|
|
<View style={styles.statItem}>
|
|
<Text style={[styles.statTitle, { color: '#10B981' }]}>好事发生</Text>
|
|
<Text style={styles.statPercentage}>{historyData.goodEvents.percentage}%</Text>
|
|
<View style={styles.statDetails}>
|
|
<Text style={[styles.statRange, { color: '#10B981', backgroundColor: '#ECFDF5' }]}>
|
|
HRV {historyData.goodEvents.range}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.statCount}>{historyData.goodEvents.count}次</Text>
|
|
</View>
|
|
|
|
<View style={styles.statItem}>
|
|
<Text style={[styles.statTitle, { color: '#3B82F6' }]}>活力满满</Text>
|
|
<Text style={styles.statPercentage}>{historyData.energetic.percentage}%</Text>
|
|
<View style={styles.statDetails}>
|
|
<Text style={[styles.statRange, { color: '#3B82F6', backgroundColor: '#EFF6FF' }]}>
|
|
HRV {historyData.energetic.range}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.statCount}>{historyData.energetic.count}次</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 鸭梨山大 */}
|
|
<View style={styles.statItem}>
|
|
<Text style={[styles.statTitle, { color: '#F59E0B' }]}>鸭梨山大</Text>
|
|
<Text style={styles.statPercentage}>{historyData.stressed.percentage}%</Text>
|
|
<View style={styles.statDetails}>
|
|
<Text style={[styles.statRange, { color: '#F59E0B', backgroundColor: '#FFFBEB' }]}>
|
|
HRV {historyData.stressed.range}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.statCount}>{historyData.stressed.count}次</Text>
|
|
</View>
|
|
</View>
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* 底部继续按钮 */}
|
|
<View style={styles.bottomContainer}>
|
|
<TouchableOpacity style={styles.continueButton} onPress={onClose} activeOpacity={0.85}>
|
|
{isLiquidGlassAvailable() ? (
|
|
<GlassView
|
|
glassEffectStyle="regular"
|
|
tintColor="rgba(139, 92, 246, 0.85)"
|
|
isInteractive={true}
|
|
style={styles.glassButton}
|
|
>
|
|
<Text style={styles.buttonText}>继续</Text>
|
|
</GlassView>
|
|
) : (
|
|
<LinearGradient
|
|
colors={['#8B5CF6', '#7C3AED']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={styles.buttonGradient}
|
|
>
|
|
<Text style={styles.buttonText}>继续</Text>
|
|
</LinearGradient>
|
|
)}
|
|
</TouchableOpacity>
|
|
<View style={styles.homeIndicator} />
|
|
</View>
|
|
</LinearGradient>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
modalContainer: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
paddingHorizontal: 20,
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
fontWeight: '800',
|
|
color: '#111827',
|
|
textAlign: 'center',
|
|
marginTop: 24,
|
|
marginBottom: 32,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
currentStatusCard: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 20,
|
|
padding: 20,
|
|
marginBottom: 32,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.06,
|
|
shadowRadius: 12,
|
|
elevation: 4,
|
|
},
|
|
statusHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 16,
|
|
},
|
|
statusLabel: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: '#374151',
|
|
},
|
|
updateTime: {
|
|
fontSize: 12,
|
|
color: '#9CA3AF',
|
|
},
|
|
statusValueContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
statusText: {
|
|
fontSize: 28,
|
|
fontWeight: '800',
|
|
marginBottom: 4,
|
|
},
|
|
statusDesc: {
|
|
fontSize: 14,
|
|
color: '#6B7280',
|
|
maxWidth: 200,
|
|
},
|
|
hrvValueBox: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
hrvValueLabel: {
|
|
fontSize: 12,
|
|
color: '#9CA3AF',
|
|
fontWeight: '600',
|
|
marginBottom: 2,
|
|
},
|
|
hrvValue: {
|
|
fontSize: 32,
|
|
fontWeight: '800',
|
|
color: '#111827',
|
|
lineHeight: 36,
|
|
},
|
|
hrvUnit: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: '#6B7280',
|
|
marginLeft: 2,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
color: '#111827',
|
|
marginBottom: 20,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
chartContainer: {
|
|
marginBottom: 32,
|
|
},
|
|
colorBar: {
|
|
height: 16,
|
|
borderRadius: 8,
|
|
overflow: 'hidden',
|
|
marginBottom: 16,
|
|
backgroundColor: '#F3F4F6',
|
|
},
|
|
progressBarContainer: {
|
|
flexDirection: 'row',
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
progressSegment: {
|
|
height: '100%',
|
|
},
|
|
gradientBar: {
|
|
flex: 1,
|
|
},
|
|
legend: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
},
|
|
legendItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
legendDot: {
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
marginRight: 8,
|
|
},
|
|
legendText: {
|
|
fontSize: 13,
|
|
fontWeight: '500',
|
|
color: '#4B5563',
|
|
},
|
|
statsCard: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 20,
|
|
padding: 24,
|
|
marginBottom: 32,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.04,
|
|
shadowRadius: 12,
|
|
elevation: 3,
|
|
},
|
|
statsRow: {
|
|
flexDirection: 'row',
|
|
gap: 24,
|
|
marginBottom: 32,
|
|
},
|
|
statItem: {
|
|
flex: 1,
|
|
},
|
|
statTitle: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
marginBottom: 12,
|
|
},
|
|
statPercentage: {
|
|
fontSize: 32,
|
|
fontWeight: '800',
|
|
color: '#111827',
|
|
marginBottom: 8,
|
|
fontFamily: 'AliBold',
|
|
},
|
|
statDetails: {
|
|
marginBottom: 8,
|
|
},
|
|
statRange: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
borderRadius: 6,
|
|
alignSelf: 'flex-start',
|
|
overflow: 'hidden',
|
|
},
|
|
statCount: {
|
|
fontSize: 13,
|
|
fontWeight: '500',
|
|
color: '#6B7280',
|
|
},
|
|
bottomContainer: {
|
|
paddingHorizontal: 20,
|
|
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
|
|
backgroundColor: 'transparent',
|
|
},
|
|
continueButton: {
|
|
borderRadius: 28,
|
|
overflow: 'hidden',
|
|
marginBottom: 12,
|
|
shadowColor: '#8B5CF6',
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 16,
|
|
elevation: 8,
|
|
},
|
|
glassButton: {
|
|
paddingVertical: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
borderRadius: 28,
|
|
},
|
|
buttonGradient: {
|
|
paddingVertical: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
},
|
|
buttonText: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#FFFFFF',
|
|
letterSpacing: 0.5,
|
|
},
|
|
homeIndicator: {
|
|
width: 134,
|
|
height: 5,
|
|
backgroundColor: Platform.OS === 'ios' ? 'rgba(0, 0, 0, 0.3)' : '#000',
|
|
borderRadius: 3,
|
|
alignSelf: 'center',
|
|
},
|
|
}); |