feat: 优化提醒注册逻辑,确保用户姓名存在时注册午餐、晚餐和心情提醒;更新睡眠详情页面,添加清醒时间段的判断和模拟数据展示;调整样式以提升用户体验

This commit is contained in:
richarjiang
2025-09-08 17:45:30 +08:00
parent f9a175d76c
commit bf3304eb06
5 changed files with 198 additions and 136 deletions

View File

@@ -70,28 +70,26 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
// 当用户数据加载完成且用户名存在时,注册所有提醒 // 当用户数据加载完成且用户名存在时,注册所有提醒
React.useEffect(() => { React.useEffect(() => {
const registerAllReminders = async () => { const registerAllReminders = async () => {
if (userDataLoaded && profile?.name) { try {
try { await notificationService.initialize();
await notificationService.initialize(); // 后台任务
// 后台任务 await backgroundTaskManager.initialize()
await backgroundTaskManager.initialize() // 注册午餐提醒12:00
// 注册午餐提醒12:00 await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name); console.log('午餐提醒已注册');
console.log('午餐提醒已注册');
// 注册晚餐提醒18:00 // 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name); await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
console.log('晚餐提醒已注册'); console.log('晚餐提醒已注册');
// 注册心情提醒21:00 // 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name); await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
console.log('心情提醒已注册'); console.log('心情提醒已注册');
console.log('喝水提醒后台任务已注册'); console.log('喝水提醒后台任务已注册');
} catch (error) { } catch (error) {
console.error('注册提醒失败:', error); console.error('注册提醒失败:', error);
}
} }
}; };

View File

@@ -1,3 +1,4 @@
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router'; import { router } from 'expo-router';
@@ -14,7 +15,6 @@ import {
View View
} from 'react-native'; } from 'react-native';
import Svg, { Circle } from 'react-native-svg'; import Svg, { Circle } from 'react-native-svg';
import { Ionicons } from '@expo/vector-icons';
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
@@ -90,17 +90,20 @@ const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
return null; return null;
} }
// 根据时间判断可能的睡眠状态 // 根据时间判断可能的睡眠状态,包括清醒时间段
if (hour >= 0 && hour <= 6) { if (hour >= 0 && hour <= 6) {
// 凌晨0-6点主要睡眠时间 // 凌晨0-6点主要睡眠时间,包含一些清醒时段
if (hour <= 2) return SleepStage.Core; if (hour <= 1) return SleepStage.Core;
if (hour === 2) return SleepStage.Awake; // 添加清醒时间段
if (hour <= 4) return SleepStage.Deep; if (hour <= 4) return SleepStage.Deep;
if (hour === 5) return SleepStage.Awake; // 添加清醒时间段
return SleepStage.REM; return SleepStage.REM;
} else if (hour >= 22) { } else if (hour >= 22) {
// 晚上10点后开始入睡 // 晚上10点后开始入睡
if (hour === 23) return SleepStage.Awake; // 入睡前的清醒时间
return SleepStage.Core; return SleepStage.Core;
} }
return null; // 清醒时间 return null; // 白天清醒时间
}); });
return ( return (
@@ -213,6 +216,8 @@ const InfoModal = ({
React.useEffect(() => { React.useEffect(() => {
if (visible) { if (visible) {
// 重置动画值确保每次打开都有动画
slideAnim.setValue(0);
Animated.spring(slideAnim, { Animated.spring(slideAnim, {
toValue: 1, toValue: 1,
useNativeDriver: true, useNativeDriver: true,
@@ -428,8 +433,11 @@ export default function SleepDetailScreen() {
<View style={styles.statsContainer}> <View style={styles.statsContainer}>
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}> <View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}> <View style={styles.statCardHeader}>
<View style={styles.statCardIcon}> <View style={styles.statCardLeftGroup}>
<Text style={styles.statIcon}>🌙</Text> <View style={styles.statCardIcon}>
<Ionicons name="moon-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.infoButton} style={styles.infoButton}
@@ -439,10 +447,9 @@ export default function SleepDetailScreen() {
type: 'sleep-time' type: 'sleep-time'
})} })}
> >
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} /> <Ionicons name="information-circle-outline" size={18} color={colorTokens.textMuted} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}> <Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'} {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'}
</Text> </Text>
@@ -453,8 +460,11 @@ export default function SleepDetailScreen() {
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}> <View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}> <View style={styles.statCardHeader}>
<View style={styles.statCardIcon}> <View style={styles.statCardLeftGroup}>
<Text style={styles.statIcon}>💎</Text> <View style={styles.statCardIcon}>
<Ionicons name="star-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.infoButton} style={styles.infoButton}
@@ -467,7 +477,6 @@ export default function SleepDetailScreen() {
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} /> <Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}> <Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'} {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'}
</Text> </Text>
@@ -506,9 +515,57 @@ export default function SleepDetailScreen() {
</View> </View>
</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> </View>
</ScrollView> </ScrollView>
@@ -594,7 +651,7 @@ const styles = StyleSheet.create({
newStatCard: { newStatCard: {
flex: 1, flex: 1,
borderRadius: 20, borderRadius: 20,
padding: 20, padding: 16,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08, shadowOpacity: 0.08,
@@ -606,19 +663,26 @@ const styles = StyleSheet.create({
statCardHeader: { statCardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 8, marginBottom: 8,
}, },
statCardLeftGroup: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statCardIcon: { statCardIcon: {
width: 32, width: 20,
height: 32, height: 20,
borderRadius: 8, borderRadius: 4,
backgroundColor: 'rgba(120, 120, 128, 0.08)', backgroundColor: 'rgba(120, 120, 128, 0.08)',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center',
}, },
infoButton: { infoButton: {
padding: 4, padding: 4,
alignSelf: 'center',
}, },
statCard: { statCard: {
flex: 1, flex: 1,
@@ -638,12 +702,12 @@ const styles = StyleSheet.create({
statLabel: { statLabel: {
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
marginBottom: 8,
letterSpacing: 0.2, letterSpacing: 0.2,
alignSelf: 'center',
}, },
newStatValue: { newStatValue: {
fontSize: 28, fontSize: 20,
fontWeight: '700', fontWeight: '600',
marginBottom: 12, marginBottom: 12,
letterSpacing: -0.5, letterSpacing: -0.5,
}, },

View File

@@ -163,10 +163,10 @@ async function executeBackgroundTasks(): Promise<void> {
} }
// 执行喝水提醒检查任务 // 执行喝水提醒检查任务
await executeWaterReminderTask(); executeWaterReminderTask();
// 执行站立提醒检查任务 // 执行站立提醒检查任务
await executeStandReminderTask(); executeStandReminderTask();
console.log('后台任务执行完成'); console.log('后台任务执行完成');
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,5 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import AppleHealthKit, { HealthKitPermissions } from 'react-native-health'; import AppleHealthKit from 'react-native-health';
// 睡眠阶段枚举(与 HealthKit 保持一致) // 睡眠阶段枚举(与 HealthKit 保持一致)
export enum SleepStage { export enum SleepStage {
@@ -97,7 +97,7 @@ async function fetchSleepSamples(date: Date): Promise<SleepSample[]> {
} }
console.log('获取到睡眠样本:', results.length); console.log('获取到睡眠样本:', results.length);
resolve(results as SleepSample[]); resolve(results as unknown as SleepSample[]);
}); });
}); });
} }
@@ -150,7 +150,7 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
// 计算总睡眠时间(排除在床时间) // 计算总睡眠时间(排除在床时间)
const totalSleepTime = Array.from(stageMap.entries()) const totalSleepTime = Array.from(stageMap.entries())
.filter(([stage]) => stage !== SleepStage.InBed && stage !== SleepStage.Awake) .filter(([stage]) => stage !== SleepStage.InBed)
.reduce((total, [, duration]) => total + duration, 0); .reduce((total, [, duration]) => total + duration, 0);
// 生成统计数据 // 生成统计数据
@@ -166,18 +166,18 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
switch (stage) { switch (stage) {
case SleepStage.Deep: case SleepStage.Deep:
quality = percentage >= 15 ? SleepQuality.Excellent : quality = percentage >= 15 ? SleepQuality.Excellent :
percentage >= 10 ? SleepQuality.Good : percentage >= 10 ? SleepQuality.Good :
percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
case SleepStage.REM: case SleepStage.REM:
quality = percentage >= 20 ? SleepQuality.Excellent : quality = percentage >= 20 ? SleepQuality.Excellent :
percentage >= 15 ? SleepQuality.Good : percentage >= 15 ? SleepQuality.Good :
percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
case SleepStage.Core: case SleepStage.Core:
quality = percentage >= 45 ? SleepQuality.Excellent : quality = percentage >= 45 ? SleepQuality.Excellent :
percentage >= 35 ? SleepQuality.Good : percentage >= 35 ? SleepQuality.Good :
percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor;
break; break;
default: default:
quality = SleepQuality.Fair; quality = SleepQuality.Fair;

View File

@@ -831,7 +831,7 @@ export class WaterNotificationHelpers {
for (const notification of notifications) { for (const notification of notifications) {
if (notification.content.data?.type === 'water_reminder' || if (notification.content.data?.type === 'water_reminder' ||
notification.content.data?.type === 'regular_water_reminder') { notification.content.data?.type === 'regular_water_reminder') {
await notificationService.cancelNotification(notification.identifier); await notificationService.cancelNotification(notification.identifier);
console.log('已取消喝水提醒:', notification.identifier); console.log('已取消喝水提醒:', notification.identifier);
} }