Files
digital-pilates/components/sleep/InfoModal.tsx

307 lines
8.5 KiB
TypeScript
Raw Permalink 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 { Ionicons } from '@expo/vector-icons';
import React, { useState } from 'react';
import {
Animated,
Modal,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
// 睡眠详情数据类型
export type SleepDetailData = {
sleepScore: number;
totalSleepTime: number;
sleepQualityPercentage: number;
bedtime: string;
wakeupTime: string;
timeInBed: number;
sleepStages: any[];
rawSleepSamples: any[];
averageHeartRate: number | null;
sleepHeartRateData: any[];
sleepEfficiency: number;
qualityDescription: string;
recommendation: string;
};
// Sleep Grade Component 睡眠等级组件
const SleepGradeCard = ({
icon,
grade,
range,
isActive = false
}: {
icon: string;
grade: string;
range: string;
isActive?: boolean;
}) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const getGradeColor = (grade: string) => {
switch (grade) {
case '低': case '较差': return { bg: '#FECACA', text: '#DC2626' };
case '正常': case '一般': return { bg: '#D1FAE5', text: '#065F46' };
case '良好': return { bg: '#D1FAE5', text: '#065F46' };
case '优秀': return { bg: '#FEF3C7', text: '#92400E' };
default: return { bg: colorTokens.pageBackgroundEmphasis, text: colorTokens.textSecondary };
}
};
const colors = getGradeColor(grade);
return (
<View style={[
styles.gradeCard,
{
backgroundColor: isActive ? colors.bg : colorTokens.pageBackgroundEmphasis,
borderColor: isActive ? colors.text : 'transparent',
}
]}>
<View style={styles.gradeCardLeft}>
<Ionicons name={icon as any} size={16} color={colors.text} />
<Text style={[
styles.gradeText,
{ color: isActive ? colors.text : colorTokens.textSecondary }
]}>
{grade}
</Text>
</View>
<Text style={[
styles.gradeRange,
{ color: isActive ? colors.text : colorTokens.textSecondary }
]}>
{range}
</Text>
</View>
);
};
// Info Modal 组件
export const InfoModal = ({
visible,
onClose,
title,
type,
sleepData
}: {
visible: boolean;
onClose: () => void;
title: string;
type: 'sleep-time' | 'sleep-quality';
sleepData: SleepDetailData;
}) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const slideAnim = useState(new Animated.Value(0))[0];
React.useEffect(() => {
if (visible) {
// 重置动画值确保每次打开都有动画
slideAnim.setValue(0);
Animated.spring(slideAnim, {
toValue: 1,
useNativeDriver: true,
tension: 100,
friction: 8,
}).start();
} else {
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: true,
tension: 100,
friction: 8,
}).start();
}
}, [visible]);
const translateY = slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [300, 0],
});
const opacity = slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
});
// 根据实际睡眠时间计算等级
const getSleepTimeGrade = (totalSleepMinutes: number) => {
const hours = totalSleepMinutes / 60;
if (hours < 6) return 0; // 低
if ((hours >= 6 && hours < 7) || hours > 9) return 1; // 正常
if (hours >= 7 && hours < 8) return 2; // 良好
if (hours >= 8 && hours <= 9) return 3; // 优秀
return 1; // 默认正常
};
// 根据实际睡眠质量百分比计算等级
const getSleepQualityGrade = (qualityPercentage: number) => {
if (qualityPercentage < 55) return 0; // 较差
if (qualityPercentage < 70) return 1; // 一般
if (qualityPercentage < 85) return 2; // 良好
return 3; // 优秀
};
const currentSleepTimeGrade = getSleepTimeGrade(sleepData.totalSleepTime || 443); // 默认7h23m
const currentSleepQualityGrade = getSleepQualityGrade(sleepData.sleepQualityPercentage || 94); // 默认94%
const sleepTimeGrades = [
{ icon: 'alert-circle-outline', grade: '低', range: '< 6h', isActive: currentSleepTimeGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: '正常', range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 },
{ icon: 'checkmark-circle', grade: '良好', range: '7h - 8h', isActive: currentSleepTimeGrade === 2 },
{ icon: 'star', grade: '优秀', range: '8h - 9h', isActive: currentSleepTimeGrade === 3 },
];
const sleepQualityGrades = [
{ icon: 'alert-circle-outline', grade: '较差', range: '< 55%', isActive: currentSleepQualityGrade === 0 },
{ icon: 'checkmark-circle-outline', grade: '一般', range: '55% - 69%', isActive: currentSleepQualityGrade === 1 },
{ icon: 'checkmark-circle', grade: '良好', range: '70% - 84%', isActive: currentSleepQualityGrade === 2 },
{ icon: 'star', grade: '优秀', range: '85% - 100%', isActive: currentSleepQualityGrade === 3 },
];
const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades;
const getDescription = () => {
if (type === 'sleep-time') {
return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。';
} else {
return '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长还包括睡眠的连续性和各睡眠阶段的平衡。';
}
};
return (
<Modal
transparent
visible={visible}
animationType="none"
onRequestClose={onClose}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={onClose}
>
<Animated.View style={[
styles.infoModalContent,
{
backgroundColor: colorTokens.background,
transform: [{ translateY }],
opacity,
}
]}>
<View style={styles.modalHandle} />
<View style={styles.infoModalHeader}>
<Text style={[styles.infoModalTitle, { color: colorTokens.text }]}>
{title}
</Text>
<TouchableOpacity onPress={onClose} style={styles.infoModalCloseButton}>
<Ionicons name="close" size={20} color={colorTokens.textSecondary} />
</TouchableOpacity>
</View>
{/* 等级卡片区域 */}
<View style={styles.gradesContainer}>
{currentGrades.map((grade, index) => (
<SleepGradeCard
key={index}
icon={grade.icon}
grade={grade.grade}
range={grade.range}
isActive={grade.isActive}
/>
))}
</View>
<Text style={[styles.infoModalText, { color: colorTokens.textSecondary }]}>
{getDescription()}
</Text>
</Animated.View>
</TouchableOpacity>
</Modal>
);
};
const styles = StyleSheet.create({
// Info Modal 样式
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
infoModalContent: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingTop: 12,
paddingHorizontal: 20,
paddingBottom: 34,
minHeight: 200,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 16,
elevation: 8,
},
modalHandle: {
width: 36,
height: 4,
backgroundColor: '#D1D5DB',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 20,
},
infoModalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
infoModalTitle: {
fontSize: 18,
fontWeight: '700',
letterSpacing: -0.3,
},
infoModalCloseButton: {
padding: 4,
},
infoModalText: {
fontSize: 15,
lineHeight: 22,
letterSpacing: -0.1,
},
// Grade Cards 样式
gradesContainer: {
marginBottom: 20,
gap: 8,
},
gradeCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 12,
borderWidth: 1,
},
gradeCardLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
gradeText: {
fontSize: 16,
fontWeight: '600',
letterSpacing: -0.2,
},
gradeRange: {
fontSize: 16,
fontWeight: '700',
letterSpacing: -0.3,
},
});