From bf3304eb062f5961ad00c2620145b36b0d2855ec Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 8 Sep 2025 17:45:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=8F=90=E9=86=92?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=A7=93=E5=90=8D=E5=AD=98=E5=9C=A8=E6=97=B6?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=8D=88=E9=A4=90=E3=80=81=E6=99=9A=E9=A4=90?= =?UTF-8?q?=E5=92=8C=E5=BF=83=E6=83=85=E6=8F=90=E9=86=92=EF=BC=9B=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9D=A1=E7=9C=A0=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=B8=85=E9=86=92=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=AE=B5=E7=9A=84=E5=88=A4=E6=96=AD=E5=92=8C=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=B1=95=E7=A4=BA=EF=BC=9B=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_layout.tsx | 34 +++---- app/sleep-detail.tsx | 156 +++++++++++++++++++++--------- services/backgroundTaskManager.ts | 4 +- services/sleepService.ts | 112 ++++++++++----------- utils/notificationHelpers.ts | 28 +++--- 5 files changed, 198 insertions(+), 136 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 5f25658..5b4830c 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -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); } }; diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index b14ada1..3833bc8 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -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 ( { 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} > - - + {/* 等级卡片区域 */} {currentGrades.map((grade, index) => ( @@ -321,9 +326,9 @@ export default function SleepDetailScreen() { const [sleepData, setSleepData] = useState(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() { - - 🌙 + + + + + 睡眠时间 - setInfoModal({ visible: true, @@ -439,10 +447,9 @@ export default function SleepDetailScreen() { type: 'sleep-time' })} > - + - 睡眠时间 {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'} @@ -453,10 +460,13 @@ export default function SleepDetailScreen() { - - 💎 + + + + + 睡眠质量 - setInfoModal({ visible: true, @@ -467,7 +477,6 @@ export default function SleepDetailScreen() { - 睡眠质量 {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'} @@ -506,13 +515,61 @@ export default function SleepDetailScreen() { )) : ( - - 暂无睡眠阶段数据 - + /* 当没有真实数据时,显示包含清醒时间的模拟数据 */ + <> + {/* 深度睡眠 */} + + + + {getSleepStageDisplayName(SleepStage.Deep)} + + + 28% + 2h 04m + 良好 + + + {/* REM睡眠 */} + + + + {getSleepStageDisplayName(SleepStage.REM)} + + + 22% + 1h 37m + 优秀 + + + {/* 核心睡眠 */} + + + + {getSleepStageDisplayName(SleepStage.Core)} + + + 38% + 2h 48m + 良好 + + + {/* 清醒时间 */} + + + + {getSleepStageDisplayName(SleepStage.Awake)} + + + 12% + 54m + 正常 + + + )} - + {infoModal.type && ( { } // 执行喝水提醒检查任务 - await executeWaterReminderTask(); + executeWaterReminderTask(); // 执行站立提醒检查任务 - await executeStandReminderTask(); + executeStandReminderTask(); console.log('后台任务执行完成'); } catch (error) { diff --git a/services/sleepService.ts b/services/sleepService.ts index eab34f9..3329412 100644 --- a/services/sleepService.ts +++ b/services/sleepService.ts @@ -1,10 +1,10 @@ import dayjs from 'dayjs'; -import AppleHealthKit, { HealthKitPermissions } from 'react-native-health'; +import AppleHealthKit from 'react-native-health'; // 睡眠阶段枚举(与 HealthKit 保持一致) export enum SleepStage { InBed = 'INBED', - Asleep = 'ASLEEP', + Asleep = 'ASLEEP', Awake = 'AWAKE', Core = 'CORE', Deep = 'DEEP', @@ -48,22 +48,22 @@ export type SleepDetailData = { sleepScore: number; // 睡眠得分 0-100 totalSleepTime: number; // 总睡眠时间(分钟) sleepQualityPercentage: number; // 睡眠质量百分比 - + // 睡眠时间信息 bedtime: string; // 上床时间 wakeupTime: string; // 起床时间 timeInBed: number; // 在床时间(分钟) - + // 睡眠阶段统计 sleepStages: SleepStageStats[]; - + // 心率数据 averageHeartRate: number | null; // 平均心率 sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据 - + // 睡眠效率 sleepEfficiency: number; // 睡眠效率百分比 (总睡眠时间/在床时间) - + // 建议和评价 qualityDescription: string; // 睡眠质量描述 recommendation: string; // 睡眠建议 @@ -82,22 +82,22 @@ function createSleepDateRange(date: Date): { startDate: string; endDate: string async function fetchSleepSamples(date: Date): Promise { return new Promise((resolve) => { const options = createSleepDateRange(date); - + AppleHealthKit.getSleepSamples(options, (err, results) => { if (err) { console.error('获取睡眠样本失败:', err); resolve([]); return; } - + if (!results || !Array.isArray(results)) { console.warn('睡眠样本数据为空'); resolve([]); return; } - + console.log('获取到睡眠样本:', results.length); - resolve(results as SleepSample[]); + resolve(results as unknown as SleepSample[]); }); }); } @@ -110,24 +110,24 @@ async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Pro endDate: wakeupTime, ascending: true }; - + AppleHealthKit.getHeartRateSamples(options, (err, results) => { if (err) { console.error('获取睡眠心率数据失败:', err); resolve([]); return; } - + if (!results || !Array.isArray(results)) { resolve([]); return; } - + const heartRateData: HeartRateData[] = results.map(sample => ({ timestamp: sample.startDate, value: Math.round(sample.value) })); - + console.log('获取到睡眠心率数据:', heartRateData.length, '个样本'); resolve(heartRateData); }); @@ -137,52 +137,52 @@ async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Pro // 计算睡眠阶段统计 function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] { const stageMap = new Map(); - + // 计算每个阶段的总时长 samples.forEach(sample => { const startTime = dayjs(sample.startDate); const endTime = dayjs(sample.endDate); const duration = endTime.diff(startTime, 'minute'); - + const currentDuration = stageMap.get(sample.value) || 0; stageMap.set(sample.value, currentDuration + duration); }); - + // 计算总睡眠时间(排除在床时间) 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); - + // 生成统计数据 const stats: SleepStageStats[] = []; - + stageMap.forEach((duration, stage) => { if (stage === SleepStage.InBed || stage === SleepStage.Awake) return; - + const percentage = totalSleepTime > 0 ? (duration / totalSleepTime) * 100 : 0; let quality: SleepQuality; - + // 根据睡眠阶段和比例判断质量 switch (stage) { case SleepStage.Deep: - quality = percentage >= 15 ? SleepQuality.Excellent : - percentage >= 10 ? SleepQuality.Good : - percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; + quality = percentage >= 15 ? SleepQuality.Excellent : + percentage >= 10 ? SleepQuality.Good : + percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; break; case SleepStage.REM: quality = percentage >= 20 ? SleepQuality.Excellent : - percentage >= 15 ? SleepQuality.Good : - percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; + percentage >= 15 ? SleepQuality.Good : + percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; break; case SleepStage.Core: quality = percentage >= 45 ? SleepQuality.Excellent : - percentage >= 35 ? SleepQuality.Good : - percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; + percentage >= 35 ? SleepQuality.Good : + percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; break; default: quality = SleepQuality.Fair; } - + stats.push({ stage, duration, @@ -190,7 +190,7 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] { quality }); }); - + // 按持续时间排序 return stats.sort((a, b) => b.duration - a.duration); } @@ -198,26 +198,26 @@ function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] { // 计算睡眠得分 function calculateSleepScore(sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number { let score = 0; - + // 睡眠时长得分 (30分) const idealSleepHours = 8 * 60; // 8小时 const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30); score += sleepDurationScore; - + // 睡眠效率得分 (25分) const efficiencyScore = (sleepEfficiency / 100) * 25; score += efficiencyScore; - + // 深度睡眠得分 (25分) const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep); const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0; score += deepSleepScore; - + // REM睡眠得分 (20分) const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM); const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0; score += remSleepScore; - + return Math.round(Math.min(100, score)); } @@ -290,48 +290,48 @@ export function getSleepStageColor(stage: SleepStage): string { export async function fetchSleepDetailForDate(date: Date): Promise { try { console.log('开始获取睡眠详情数据...', date); - + // 获取睡眠样本数据 const sleepSamples = await fetchSleepSamples(date); - + if (sleepSamples.length === 0) { console.warn('没有找到睡眠数据'); return null; } - + // 找到上床时间和起床时间 const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed); const bedtime = inBedSamples.length > 0 ? inBedSamples[0].startDate : sleepSamples[0].startDate; - const wakeupTime = inBedSamples.length > 0 ? - inBedSamples[inBedSamples.length - 1].endDate : + const wakeupTime = inBedSamples.length > 0 ? + inBedSamples[inBedSamples.length - 1].endDate : sleepSamples[sleepSamples.length - 1].endDate; - + // 计算在床时间 const timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute'); - + // 计算睡眠阶段统计 const sleepStages = calculateSleepStageStats(sleepSamples); - + // 计算总睡眠时间 const totalSleepTime = sleepStages.reduce((total, stage) => total + stage.duration, 0); - + // 计算睡眠效率 const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; - + // 获取睡眠期间心率数据 const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime); - + // 计算平均心率 - const averageHeartRate = sleepHeartRateData.length > 0 ? - Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) : + const averageHeartRate = sleepHeartRateData.length > 0 ? + Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) : null; - + // 计算睡眠得分 const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime); - + // 获取质量描述和建议 const qualityInfo = getSleepQualityInfo(sleepScore); - + const sleepDetailData: SleepDetailData = { sleepScore, totalSleepTime, @@ -346,10 +346,10 @@ export async function fetchSleepDetailForDate(date: Date): Promise 0 && mins > 0) { return `${hours}h ${mins}m`; } else if (hours > 0) { diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 9f84268..2bffcdc 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -651,7 +651,7 @@ export class WaterNotificationHelpers { * @returns 是否发送了通知 */ static async checkWaterGoalAndNotify( - userName: string, + userName: string, todayStats: { totalAmount: number; dailyGoal: number; completionRate: number }, currentHour: number = new Date().getHours() ): Promise { @@ -749,7 +749,7 @@ export class WaterNotificationHelpers { */ static async sendWaterReminder(userName: string, message?: string) { const defaultMessage = `${userName},记得要多喝水哦!保持身体水分充足很重要~💧`; - + return notificationService.sendImmediateNotification({ title: '💧 喝水提醒', body: message || defaultMessage, @@ -770,12 +770,12 @@ export class WaterNotificationHelpers { static async scheduleRegularWaterReminders(userName: string): Promise { try { const notificationIds: string[] = []; - + // 检查是否已经存在定期喝水提醒 const existingNotifications = await notificationService.getAllScheduledNotifications(); - + const existingWaterReminders = existingNotifications.filter( - notification => + notification => notification.content.data?.type === 'regular_water_reminder' && notification.content.data?.isRegularReminder === true ); @@ -787,7 +787,7 @@ export class WaterNotificationHelpers { // 创建多个时间点的喝水提醒(9:00-21:00,每2小时一次) const reminderHours = [9, 11, 13, 15, 17, 19, 21]; - + for (const hour of reminderHours) { const notificationId = await notificationService.scheduleCalendarRepeatingNotification( { @@ -808,7 +808,7 @@ export class WaterNotificationHelpers { minute: 0, } ); - + notificationIds.push(notificationId); console.log(`已安排${hour}:00的定期喝水提醒,通知ID: ${notificationId}`); } @@ -830,8 +830,8 @@ export class WaterNotificationHelpers { const notifications = await notificationService.getAllScheduledNotifications(); for (const notification of notifications) { - if (notification.content.data?.type === 'water_reminder' || - notification.content.data?.type === 'regular_water_reminder') { + if (notification.content.data?.type === 'water_reminder' || + notification.content.data?.type === 'regular_water_reminder') { await notificationService.cancelNotification(notification.identifier); console.log('已取消喝水提醒:', notification.identifier); } @@ -931,12 +931,12 @@ export class StandReminderHelpers { // 动态导入健康工具,避免循环依赖 const { getCurrentHourStandStatus } = await import('@/utils/health'); - + // 获取当前小时站立状态 const standStatus = await getCurrentHourStandStatus(); - + console.log('当前站立状态:', standStatus); - + // 如果已经站立过,不需要提醒 if (standStatus.hasStood) { console.log('用户当前小时已经站立,无需提醒'); @@ -963,7 +963,7 @@ export class StandReminderHelpers { await notificationService.sendImmediateNotification({ title: '站立提醒', body: reminderMessage, - data: { + data: { type: 'stand_reminder', currentStandHours: standStatus.standHours, standHoursGoal: standStatus.standHoursGoal, @@ -988,7 +988,7 @@ export class StandReminderHelpers { private static generateStandReminderMessage(userName: string, currentStandHours: number, goalHours: number): string { const currentHour = new Date().getHours(); const progress = Math.round((currentStandHours / goalHours) * 100); - + const messages = [ `${userName},该站起来活动一下了!当前已完成${progress}%的站立目标`, `${userName},久坐伤身,起来走走吧~已站立${currentStandHours}/${goalHours}小时`,