feat: 优化提醒注册逻辑,确保用户姓名存在时注册午餐、晚餐和心情提醒;更新睡眠详情页面,添加清醒时间段的判断和模拟数据展示;调整样式以提升用户体验
This commit is contained in:
@@ -70,28 +70,26 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
// 当用户数据加载完成且用户名存在时,注册所有提醒
|
||||
React.useEffect(() => {
|
||||
const registerAllReminders = async () => {
|
||||
if (userDataLoaded && profile?.name) {
|
||||
try {
|
||||
await notificationService.initialize();
|
||||
// 后台任务
|
||||
await backgroundTaskManager.initialize()
|
||||
// 注册午餐提醒(12:00)
|
||||
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name);
|
||||
console.log('午餐提醒已注册');
|
||||
try {
|
||||
await notificationService.initialize();
|
||||
// 后台任务
|
||||
await backgroundTaskManager.initialize()
|
||||
// 注册午餐提醒(12:00)
|
||||
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
|
||||
console.log('午餐提醒已注册');
|
||||
|
||||
// 注册晚餐提醒(18:00)
|
||||
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name);
|
||||
console.log('晚餐提醒已注册');
|
||||
// 注册晚餐提醒(18:00)
|
||||
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
|
||||
console.log('晚餐提醒已注册');
|
||||
|
||||
// 注册心情提醒(21:00)
|
||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name);
|
||||
console.log('心情提醒已注册');
|
||||
// 注册心情提醒(21:00)
|
||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
|
||||
console.log('心情提醒已注册');
|
||||
|
||||
|
||||
console.log('喝水提醒后台任务已注册');
|
||||
} catch (error) {
|
||||
console.error('注册提醒失败:', error);
|
||||
}
|
||||
console.log('喝水提醒后台任务已注册');
|
||||
} catch (error) {
|
||||
console.error('注册提醒失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
import Svg, { Circle } from 'react-native-svg';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
@@ -90,17 +90,20 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据时间判断可能的睡眠状态
|
||||
// 根据时间判断可能的睡眠状态,包括清醒时间段
|
||||
if (hour >= 0 && hour <= 6) {
|
||||
// 凌晨0-6点,主要睡眠时间
|
||||
if (hour <= 2) return SleepStage.Core;
|
||||
// 凌晨0-6点,主要睡眠时间,包含一些清醒时段
|
||||
if (hour <= 1) return SleepStage.Core;
|
||||
if (hour === 2) return SleepStage.Awake; // 添加清醒时间段
|
||||
if (hour <= 4) return SleepStage.Deep;
|
||||
if (hour === 5) return SleepStage.Awake; // 添加清醒时间段
|
||||
return SleepStage.REM;
|
||||
} else if (hour >= 22) {
|
||||
// 晚上10点后开始入睡
|
||||
if (hour === 23) return SleepStage.Awake; // 入睡前的清醒时间
|
||||
return SleepStage.Core;
|
||||
}
|
||||
return null; // 清醒时间
|
||||
return null; // 白天清醒时间
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -142,11 +145,11 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
|
||||
};
|
||||
|
||||
// Sleep Grade Component 睡眠等级组件
|
||||
const SleepGradeCard = ({
|
||||
icon,
|
||||
grade,
|
||||
range,
|
||||
isActive = false
|
||||
const SleepGradeCard = ({
|
||||
icon,
|
||||
grade,
|
||||
range,
|
||||
isActive = false
|
||||
}: {
|
||||
icon: string;
|
||||
grade: string;
|
||||
@@ -155,7 +158,7 @@ const SleepGradeCard = ({
|
||||
}) => {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
|
||||
const getGradeColor = (grade: string) => {
|
||||
switch (grade) {
|
||||
case '低': return { bg: '#FECACA', text: '#DC2626' };
|
||||
@@ -171,7 +174,7 @@ const SleepGradeCard = ({
|
||||
return (
|
||||
<View style={[
|
||||
styles.gradeCard,
|
||||
{
|
||||
{
|
||||
backgroundColor: isActive ? colors.bg : colorTokens.pageBackgroundEmphasis,
|
||||
borderColor: isActive ? colors.text : 'transparent',
|
||||
}
|
||||
@@ -196,10 +199,10 @@ const SleepGradeCard = ({
|
||||
};
|
||||
|
||||
// Info Modal 组件
|
||||
const InfoModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
const InfoModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
type
|
||||
}: {
|
||||
visible: boolean;
|
||||
@@ -213,6 +216,8 @@ const InfoModal = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
// 重置动画值确保每次打开都有动画
|
||||
slideAnim.setValue(0);
|
||||
Animated.spring(slideAnim, {
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
@@ -254,7 +259,7 @@ const InfoModal = ({
|
||||
];
|
||||
|
||||
const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades;
|
||||
|
||||
|
||||
const getDescription = () => {
|
||||
if (type === 'sleep-time') {
|
||||
return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。';
|
||||
@@ -270,14 +275,14 @@ const InfoModal = ({
|
||||
animationType="none"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Animated.View style={[
|
||||
styles.infoModalContent,
|
||||
{
|
||||
{
|
||||
backgroundColor: colorTokens.background,
|
||||
transform: [{ translateY }],
|
||||
opacity,
|
||||
@@ -292,7 +297,7 @@ const InfoModal = ({
|
||||
<Ionicons name="close" size={20} color={colorTokens.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 等级卡片区域 */}
|
||||
<View style={styles.gradesContainer}>
|
||||
{currentGrades.map((grade, index) => (
|
||||
@@ -321,9 +326,9 @@ export default function SleepDetailScreen() {
|
||||
const [sleepData, setSleepData] = useState<SleepDetailData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedDate] = useState(dayjs().toDate());
|
||||
const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({
|
||||
visible: false,
|
||||
title: '',
|
||||
const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({
|
||||
visible: false,
|
||||
title: '',
|
||||
type: null
|
||||
});
|
||||
|
||||
@@ -428,10 +433,13 @@ export default function SleepDetailScreen() {
|
||||
<View style={styles.statsContainer}>
|
||||
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
|
||||
<View style={styles.statCardHeader}>
|
||||
<View style={styles.statCardIcon}>
|
||||
<Text style={styles.statIcon}>🌙</Text>
|
||||
<View style={styles.statCardLeftGroup}>
|
||||
<View style={styles.statCardIcon}>
|
||||
<Ionicons name="moon-outline" size={18} color="#6B7280" />
|
||||
</View>
|
||||
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}>睡眠时间</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.infoButton}
|
||||
onPress={() => setInfoModal({
|
||||
visible: true,
|
||||
@@ -439,10 +447,9 @@ export default function SleepDetailScreen() {
|
||||
type: 'sleep-time'
|
||||
})}
|
||||
>
|
||||
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} />
|
||||
<Ionicons name="information-circle-outline" size={18} color={colorTokens.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}>睡眠时间</Text>
|
||||
<Text style={[styles.newStatValue, { color: colorTokens.text }]}>
|
||||
{displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'}
|
||||
</Text>
|
||||
@@ -453,10 +460,13 @@ export default function SleepDetailScreen() {
|
||||
|
||||
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
|
||||
<View style={styles.statCardHeader}>
|
||||
<View style={styles.statCardIcon}>
|
||||
<Text style={styles.statIcon}>💎</Text>
|
||||
<View style={styles.statCardLeftGroup}>
|
||||
<View style={styles.statCardIcon}>
|
||||
<Ionicons name="star-outline" size={18} color="#6B7280" />
|
||||
</View>
|
||||
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}>睡眠质量</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.infoButton}
|
||||
onPress={() => setInfoModal({
|
||||
visible: true,
|
||||
@@ -467,7 +477,6 @@ export default function SleepDetailScreen() {
|
||||
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}>睡眠质量</Text>
|
||||
<Text style={[styles.newStatValue, { color: colorTokens.text }]}>
|
||||
{displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'}
|
||||
</Text>
|
||||
@@ -506,13 +515,61 @@ export default function SleepDetailScreen() {
|
||||
</View>
|
||||
</View>
|
||||
)) : (
|
||||
<View style={styles.noDataContainer}>
|
||||
<Text style={styles.noDataText}>暂无睡眠阶段数据</Text>
|
||||
</View>
|
||||
/* 当没有真实数据时,显示包含清醒时间的模拟数据 */
|
||||
<>
|
||||
{/* 深度睡眠 */}
|
||||
<View style={styles.stageRow}>
|
||||
<View style={styles.stageInfo}>
|
||||
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Deep) }]} />
|
||||
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Deep)}</Text>
|
||||
</View>
|
||||
<View style={styles.stageStats}>
|
||||
<Text style={styles.stagePercentage}>28%</Text>
|
||||
<Text style={styles.stageDuration}>2h 04m</Text>
|
||||
<Text style={[styles.stageQuality, { color: '#10B981' }]}>良好</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* REM睡眠 */}
|
||||
<View style={styles.stageRow}>
|
||||
<View style={styles.stageInfo}>
|
||||
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.REM) }]} />
|
||||
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.REM)}</Text>
|
||||
</View>
|
||||
<View style={styles.stageStats}>
|
||||
<Text style={styles.stagePercentage}>22%</Text>
|
||||
<Text style={styles.stageDuration}>1h 37m</Text>
|
||||
<Text style={[styles.stageQuality, { color: '#10B981' }]}>优秀</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 核心睡眠 */}
|
||||
<View style={styles.stageRow}>
|
||||
<View style={styles.stageInfo}>
|
||||
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Core) }]} />
|
||||
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Core)}</Text>
|
||||
</View>
|
||||
<View style={styles.stageStats}>
|
||||
<Text style={styles.stagePercentage}>38%</Text>
|
||||
<Text style={styles.stageDuration}>2h 48m</Text>
|
||||
<Text style={[styles.stageQuality, { color: '#059669' }]}>良好</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* 清醒时间 */}
|
||||
<View style={styles.stageRow}>
|
||||
<View style={styles.stageInfo}>
|
||||
<View style={[styles.stageColorDot, { backgroundColor: getSleepStageColor(SleepStage.Awake) }]} />
|
||||
<Text style={styles.stageName}>{getSleepStageDisplayName(SleepStage.Awake)}</Text>
|
||||
</View>
|
||||
<View style={styles.stageStats}>
|
||||
<Text style={styles.stagePercentage}>12%</Text>
|
||||
<Text style={styles.stageDuration}>54m</Text>
|
||||
<Text style={[styles.stageQuality, { color: '#F59E0B' }]}>正常</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
{infoModal.type && (
|
||||
<InfoModal
|
||||
visible={infoModal.visible}
|
||||
@@ -594,7 +651,7 @@ const styles = StyleSheet.create({
|
||||
newStatCard: {
|
||||
flex: 1,
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.08,
|
||||
@@ -606,19 +663,26 @@ const styles = StyleSheet.create({
|
||||
statCardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
statCardLeftGroup: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
statCardIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(120, 120, 128, 0.08)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
infoButton: {
|
||||
padding: 4,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
statCard: {
|
||||
flex: 1,
|
||||
@@ -638,12 +702,12 @@ const styles = StyleSheet.create({
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
marginBottom: 8,
|
||||
letterSpacing: 0.2,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
newStatValue: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 12,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user