From ed3a178aa028e3042448f2863ec304b728f16a04 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sat, 11 Oct 2025 21:53:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(workout):=20=E4=BC=98=E5=8C=96=E5=BF=83?= =?UTF-8?q?=E7=8E=87=E5=9B=BE=E8=A1=A8=E6=80=A7=E8=83=BD=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=AF=8F=E6=97=A5=E6=80=BB=E7=BB=93=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构心率数据采样算法,采用智能采样保留峰值、谷值和变化率大的点 - 减少心率图表最大数据点数和查询限制,提升渲染性能 - 移除图表背景线样式,简化视觉呈现 - 完全移除每日总结通知功能相关代码和调用 --- app/_layout.tsx | 4 +- components/workout/WorkoutDetailModal.tsx | 94 ++++++- utils/health.ts | 2 +- utils/notificationHelpers.ts | 299 ---------------------- 4 files changed, 84 insertions(+), 315 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 93ec2d3..4039b59 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -17,7 +17,7 @@ import { store } from '@/store'; import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice'; import { createWaterRecordAction } from '@/store/waterSlice'; import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health'; -import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers'; +import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers'; import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync'; import React, { useEffect } from 'react'; @@ -93,8 +93,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || ''); console.log('心情提醒已注册'); - await DailySummaryNotificationHelpers.scheduleDailySummaryNotification(profile.name || '') - // 初始化快捷动作 await setupQuickActions(); diff --git a/components/workout/WorkoutDetailModal.tsx b/components/workout/WorkoutDetailModal.tsx index b9460da..a3a8654 100644 --- a/components/workout/WorkoutDetailModal.tsx +++ b/components/workout/WorkoutDetailModal.tsx @@ -46,7 +46,7 @@ interface WorkoutDetailModalProps { const SCREEN_HEIGHT = Dimensions.get('window').height; const SHEET_MAX_HEIGHT = SCREEN_HEIGHT * 0.9; -const HEART_RATE_CHART_MAX_POINTS = 120; +const HEART_RATE_CHART_MAX_POINTS = 80; export function WorkoutDetailModal({ visible, @@ -338,7 +338,7 @@ export function WorkoutDetailModal({ height={220} fromZero={false} yAxisSuffix="次/分" - withInnerLines + withInnerLines={false} bezier chartConfig={{ backgroundColor: '#FFFFFF', @@ -352,11 +352,6 @@ export function WorkoutDetailModal({ strokeWidth: '2', stroke: '#FFFFFF', }, - propsForBackgroundLines: { - strokeDasharray: '3,3', - stroke: '#E3E6F4', - strokeWidth: 1, - }, fillShadowGradientFromOpacity: 0.1, fillShadowGradientToOpacity: 0.02, }} @@ -475,14 +470,88 @@ function trimHeartRateSeries(series: WorkoutDetailMetrics['heartRateSeries']) { return series; } - const step = Math.ceil(series.length / HEART_RATE_CHART_MAX_POINTS); - const reduced = series.filter((_, index) => index % step === 0); + // 智能采样算法:保留重要特征点(峰值、谷值、变化率大的点) + const result: typeof series = []; + const n = series.length; - if (reduced[reduced.length - 1] !== series[series.length - 1]) { - reduced.push(series[series.length - 1]); + // 总是保留第一个和最后一个点 + result.push(series[0]); + + // 计算心率变化率 + const changeRates: number[] = []; + for (let i = 1; i < n; i++) { + const prevValue = series[i - 1].value; + const currValue = series[i].value; + const prevTime = dayjs(series[i - 1].timestamp).valueOf(); + const currTime = dayjs(series[i].timestamp).valueOf(); + const timeDiff = Math.max(currTime - prevTime, 1000); // 至少1秒,避免除零 + const valueDiff = Math.abs(currValue - prevValue); + changeRates.push(valueDiff / timeDiff * 1000); // 变化率:每秒变化量 } - return reduced; + // 计算变化率的阈值(前75%的分位数) + const sortedRates = [...changeRates].sort((a, b) => a - b); + const thresholdIndex = Math.floor(sortedRates.length * 0.75); + const changeThreshold = sortedRates[thresholdIndex] || 0; + + // 识别局部极值点 + const isLocalExtremum = (index: number): boolean => { + if (index === 0 || index === n - 1) return false; + + const prev = series[index - 1].value; + const curr = series[index].value; + const next = series[index + 1].value; + + // 局部最大值 + if (curr > prev && curr > next) return true; + // 局部最小值 + if (curr < prev && curr < next) return true; + + return false; + }; + + // 遍历所有点,选择重要点 + let minDistance = Math.max(1, Math.floor(n / HEART_RATE_CHART_MAX_POINTS)); + + for (let i = 1; i < n - 1; i++) { + const shouldKeep = + // 1. 是局部极值点 + isLocalExtremum(i) || + // 2. 变化率超过阈值 + (i > 0 && changeRates[i - 1] > changeThreshold) || + // 3. 均匀分布的点(确保基本覆盖) + (i % minDistance === 0); + + if (shouldKeep) { + // 检查与上一个选中点的距离,避免过于密集 + const lastSelectedIndex = result.length > 0 ? + series.findIndex(p => p.timestamp === result[result.length - 1].timestamp) : 0; + + if (i - lastSelectedIndex >= minDistance || isLocalExtremum(i)) { + result.push(series[i]); + } + } + } + + // 确保最后一个点被包含 + if (result[result.length - 1].timestamp !== series[n - 1].timestamp) { + result.push(series[n - 1]); + } + + // 如果结果仍然太多,进行二次采样 + if (result.length > HEART_RATE_CHART_MAX_POINTS) { + const finalStep = Math.ceil(result.length / HEART_RATE_CHART_MAX_POINTS); + const finalResult = result.filter((_, index) => index % finalStep === 0); + + // 确保最后一个点被包含 + if (finalResult[finalResult.length - 1] !== result[result.length - 1]) { + finalResult.push(result[result.length - 1]); + } + + return finalResult; + } + + return result; } function renderHeartRateZone(zone: HeartRateZoneStat) { @@ -745,6 +814,7 @@ const styles = StyleSheet.create({ }, chartStyle: { marginLeft: -10, + marginRight: -10, }, chartEmpty: { paddingVertical: 32, diff --git a/utils/health.ts b/utils/health.ts index f6b4c54..9f150d9 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -785,7 +785,7 @@ export async function fetchOxygenSaturation(options: HealthDataOptions): Promise export async function fetchHeartRateSamplesForRange( startDate: Date, endDate: Date, - limit: number = 2000 + limit: number = 500 ): Promise { try { const options = { diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 6029ea6..bd79e99 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -1138,305 +1138,6 @@ export class StandReminderHelpers { } } -/** - * 每日总结通知助手 - */ -export class DailySummaryNotificationHelpers { - /** - * 获取当日数据汇总 - */ - static async getDailySummaryData(date: string = new Date().toISOString().split('T')[0]) { - try { - console.log('获取每日汇总数据:', date); - - // 动态导入相关服务,避免循环依赖 - const { getDietRecords } = await import('@/services/dietRecords'); - const { getDailyMoodCheckins } = await import('@/services/moodCheckins'); - const { getWaterRecords } = await import('@/services/waterRecords'); - const { workoutsApi } = await import('@/services/workoutsApi'); - - // 设置日期范围 - const startDate = new Date(`${date}T00:00:00.000Z`).toISOString(); - const endDate = new Date(`${date}T23:59:59.999Z`).toISOString(); - - // 并行获取各项数据 - const [dietData, moodData, waterData, workoutData] = await Promise.allSettled([ - getDietRecords({ startDate, endDate, limit: 100 }), - getDailyMoodCheckins(date), - getWaterRecords({ date, limit: 100 }), - workoutsApi.getTodayWorkout() - ]); - - // 处理饮食数据 - const dietSummary = { - hasRecords: false, - mealCount: 0, - recordCount: 0 - }; - if (dietData.status === 'fulfilled' && dietData.value.records.length > 0) { - dietSummary.hasRecords = true; - dietSummary.recordCount = dietData.value.records.length; - dietSummary.mealCount = new Set(dietData.value.records.map(r => r.mealType)).size; - } - - // 处理心情数据 - const moodSummary = { - hasRecords: false, - recordCount: 0, - latestMood: null as string | null - }; - if (moodData.status === 'fulfilled' && moodData.value.length > 0) { - moodSummary.hasRecords = true; - moodSummary.recordCount = moodData.value.length; - moodSummary.latestMood = moodData.value[0].moodType; - } - - // 处理饮水数据 - const waterSummary = { - hasRecords: false, - recordCount: 0, - totalAmount: 0, - completionRate: 0 - }; - if (waterData.status === 'fulfilled' && waterData.value.records.length > 0) { - waterSummary.hasRecords = true; - waterSummary.recordCount = waterData.value.records.length; - waterSummary.totalAmount = waterData.value.records.reduce((sum, r) => sum + r.amount, 0); - // 假设默认目标是2000ml,实际应该从用户设置获取 - const dailyGoal = 2000; - waterSummary.completionRate = Math.round((waterSummary.totalAmount / dailyGoal) * 100); - } - - // 处理锻炼数据 - const workoutSummary = { - hasWorkout: false, - isCompleted: false, - exerciseCount: 0, - completedCount: 0, - duration: 0 - }; - if (workoutData.status === 'fulfilled' && workoutData.value) { - workoutSummary.hasWorkout = true; - workoutSummary.isCompleted = workoutData.value.status === 'completed'; - workoutSummary.exerciseCount = workoutData.value.exercises?.length || 0; - workoutSummary.completedCount = workoutData.value.exercises?.filter(e => e.status === 'completed').length || 0; - if (workoutData.value.completedAt && workoutData.value.startedAt) { - workoutSummary.duration = Math.round((new Date(workoutData.value.completedAt).getTime() - new Date(workoutData.value.startedAt).getTime()) / (1000 * 60)); - } - } - - return { - date, - diet: dietSummary, - mood: moodSummary, - water: waterSummary, - workout: workoutSummary - }; - - } catch (error) { - console.error('获取每日汇总数据失败:', error); - throw error; - } - } - - /** - * 生成每日总结推送消息 - */ - static generateDailySummaryMessage(userName: string, summaryData: any): { title: string; body: string } { - const { diet, mood, water, workout } = summaryData; - - // 计算完成的项目数量 - const completedItems = []; - const encouragementItems = []; - - // 饮食记录检查 - if (diet.hasRecords) { - completedItems.push(`记录了${diet.mealCount}餐饮食`); - } else { - encouragementItems.push('饮食记录'); - } - - // 心情记录检查 - if (mood.hasRecords) { - completedItems.push(`记录了心情状态`); - } else { - encouragementItems.push('心情记录'); - } - - // 饮水记录检查 - if (water.hasRecords) { - if (water.completionRate >= 80) { - completedItems.push(`完成了${water.completionRate}%的饮水目标`); - } else { - completedItems.push(`喝水${water.completionRate}%`); - encouragementItems.push('多喝水'); - } - } else { - encouragementItems.push('饮水记录'); - } - - // 锻炼记录检查 - if (workout.hasWorkout) { - if (workout.isCompleted) { - completedItems.push(`完成了${workout.duration}分钟锻炼`); - } else { - completedItems.push(`开始了锻炼训练`); - encouragementItems.push('完成锻炼'); - } - } else { - encouragementItems.push('运动锻炼'); - } - - // 生成标题和内容 - let title = '今日健康总结'; - let body = ''; - - if (completedItems.length > 0) { - if (completedItems.length >= 3) { - // 完成度很高的鼓励 - const titles = ['今天表现棒极了!', '健康习惯养成中!', '今日收获满满!']; - title = titles[Math.floor(Math.random() * titles.length)]; - body = `${userName},今天您${completedItems.join('、')},真的很棒!`; - - if (encouragementItems.length > 0) { - body += `明天在${encouragementItems.join('、')}方面再加把劲哦~`; - } else { - body += '继续保持这样的好习惯!🌟'; - } - } else { - // 中等完成度的鼓励 - title = '今日健康小结'; - body = `${userName},今天您${completedItems.join('、')},已经很不错了!`; - - if (encouragementItems.length > 0) { - body += `明天记得关注一下${encouragementItems.join('、')},让健康生活更完整~`; - } - } - } else { - // 完成度较低的温柔提醒 - const titles = ['明天是新的开始', '健康从每一天开始', '小步前进也是进步']; - title = titles[Math.floor(Math.random() * titles.length)]; - body = `${userName},今天可能比较忙碌。明天记得关注${encouragementItems.slice(0, 2).join('和')},每一个小改变都是向健康生活迈进的一步~💪`; - } - - return { title, body }; - } - - /** - * 发送每日总结推送 - */ - static async sendDailySummaryNotification(userName: string, date?: string): Promise { - try { - console.log('开始发送每日总结推送...'); - - // 检查是否启用了通知 - if (!(await getNotificationEnabled())) { - console.log('用户未启用通知功能,跳过每日总结推送'); - return false; - } - - // 获取当日数据汇总 - const summaryData = await this.getDailySummaryData(date); - console.log('每日汇总数据:', summaryData); - - // 生成推送消息 - const { title, body } = this.generateDailySummaryMessage(userName, summaryData); - - // 发送通知 - await notificationService.sendImmediateNotification({ - title, - body, - data: { - type: 'daily_summary', - date: summaryData.date, - summaryData, - url: '/statistics' // 跳转到统计页面 - }, - sound: true, - priority: 'normal', - }); - - console.log('每日总结推送发送成功'); - return true; - - } catch (error) { - console.error('发送每日总结推送失败:', error); - return false; - } - } - - /** - * 安排每日总结推送(每天晚上9点) - */ - static async scheduleDailySummaryNotification( - userName: string, - hour: number = 21, - minute: number = 0 - ): Promise { - try { - // 检查是否已经存在每日总结提醒 - const existingNotifications = await notificationService.getAllScheduledNotifications(); - - const existingSummaryReminder = existingNotifications.find( - notification => - notification.content.data?.type === 'daily_summary_reminder' && - notification.content.data?.isDailyReminder === true - ); - - if (existingSummaryReminder) { - console.log('每日总结推送已存在,跳过重复注册:', existingSummaryReminder.identifier); - return existingSummaryReminder.identifier; - } - - // 创建每日总结推送通知 - const notificationId = await notificationService.scheduleCalendarRepeatingNotification( - { - title: '今日健康总结', - body: `${userName},来看看今天的健康生活总结吧~每一份记录都是成长的足迹!✨`, - data: { - type: 'daily_summary_reminder', - isDailyReminder: true, - url: '/statistics' - }, - sound: true, - priority: 'normal', - }, - { - type: Notifications.SchedulableTriggerInputTypes.DAILY, - hour: hour, - minute: minute, - } - ); - - console.log('每日总结推送已安排,ID:', notificationId); - return notificationId; - } catch (error) { - console.error('安排每日总结推送失败:', error); - throw error; - } - } - - /** - * 取消每日总结推送 - */ - static async cancelDailySummaryNotification(): Promise { - try { - const notifications = await notificationService.getAllScheduledNotifications(); - - for (const notification of notifications) { - if (notification.content.data?.type === 'daily_summary_reminder' && - notification.content.data?.isDailyReminder === true) { - await notificationService.cancelNotification(notification.identifier); - console.log('已取消每日总结推送:', notification.identifier); - } - } - } catch (error) { - console.error('取消每日总结推送失败:', error); - throw error; - } - } -} - /** * 通知模板 */