From e713ffbacede1ea6f725a8e5ea5f30ee9aca7676 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Wed, 3 Dec 2025 10:13:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=9D=A1=E7=9C=A0?= =?UTF-8?q?=E5=88=86=E6=9E=90=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9D=A1=E7=9C=A0=E8=B4=A8=E9=87=8F=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=E4=B8=8E=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/statistics.tsx | 1 + i18n/en/health.ts | 39 +++++ i18n/zh/health.ts | 39 +++++ ios/OutLive.xcodeproj/project.pbxproj | 41 +++--- ios/en.lproj/InfoPlist.strings | 8 ++ ios/medicine/Info.plist | 2 - ios/zh-Hans.lproj/InfoPlist.strings | 8 ++ services/healthKitSync.ts | 19 ++- services/notifications.ts | 11 ++ services/sleepNotificationService.ts | 100 ++++++------- services/users.ts | 1 + utils/SimpleEventEmitter.ts | 2 +- utils/mockHealthData.ts | 136 ------------------ utils/workoutTestHelper.ts | 198 -------------------------- 14 files changed, 190 insertions(+), 415 deletions(-) create mode 100644 ios/en.lproj/InfoPlist.strings create mode 100644 ios/zh-Hans.lproj/InfoPlist.strings delete mode 100644 utils/mockHealthData.ts delete mode 100644 utils/workoutTestHelper.ts diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 2cc7520..71543c7 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -65,6 +65,7 @@ export default function ExploreScreen() { const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000; const userProfile = useAppSelector((s) => s.user.profile); const todayWaterStats = useAppSelector((s) => s.water.todayStats); + const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard(); const router = useRouter(); diff --git a/i18n/en/health.ts b/i18n/en/health.ts index 3999c21..3e10652 100644 --- a/i18n/en/health.ts +++ b/i18n/en/health.ts @@ -656,6 +656,45 @@ export const workoutDetail = { }, }; +export const sleepNotification = { + // Notification body template + body: 'You slept {{duration}} last night with {{efficiency}}% efficiency. Score: {{score}} 🎯', + + // Sleep quality titles - warm and encouraging tone + quality: { + excellent: 'Amazing! You slept great', + good: 'Nice! Good sleep quality', + fair: 'Not bad, tomorrow will be better', + poor: 'Hang in there, rest well tonight', + veryPoor: 'Take care of yourself', + default: 'Sleep analysis complete', + }, + + // Sleep duration formatting + duration: { + hoursOnly: '{{hours}} hours', + hoursAndMinutes: '{{hours}}h {{minutes}}m', + }, + + // Sleep tips - encouraging tone + tips: { + excellent: { + keepItUp: 'Keep it up, you\'re doing amazing!', + greatJob: 'Your body thanks you for the great care!', + energized: 'You\'ll be full of energy today!', + proud: 'Give yourself a pat on the back!', + }, + suggestions: { + shortSleep: 'Try hitting the pillow earlier - 7-9 hours will boost your energy!', + longSleep: 'Too much sleep can be tiring too - try a consistent wake time!', + lowDeepSleep: 'Put your phone away before bed for deeper rest~', + lowRemSleep: 'A regular schedule helps you dream better!', + lowEfficiency: 'A cozy bedroom environment can work wonders~', + }, + general: 'Every night is a fresh start - take care of yourself!', + }, +}; + export const workoutHistory = { title: 'Workout Summary', loading: 'Loading workout records...', diff --git a/i18n/zh/health.ts b/i18n/zh/health.ts index 9684117..4f6a50c 100644 --- a/i18n/zh/health.ts +++ b/i18n/zh/health.ts @@ -657,6 +657,45 @@ export const workoutDetail = { }, }; +export const sleepNotification = { + // 通知正文模板 + body: '昨晚睡了 {{duration}},睡眠效率 {{efficiency}}%,得分 {{score}} 分 🎯', + + // 睡眠质量标题 - 更温暖鼓励的语气 + quality: { + excellent: '太棒了!睡得真好', + good: '不错哦!睡眠质量良好', + fair: '还行,明天会更好', + poor: '辛苦了,今晚早点休息', + veryPoor: '抱抱,好好照顾自己', + default: '睡眠分析完成啦', + }, + + // 睡眠时长格式化 + duration: { + hoursOnly: '{{hours}} 小时', + hoursAndMinutes: '{{hours}} 小时 {{minutes}} 分钟', + }, + + // 睡眠建议 - 更鼓励的语气 + tips: { + excellent: { + keepItUp: '继续保持,你真的很棒!', + greatJob: '身体一定很感谢你的照顾~', + energized: '今天一定精力满满!', + proud: '为自己的好习惯点赞!', + }, + suggestions: { + shortSleep: '试着早点上床吧,7-9 小时的睡眠会让你更有活力哦~', + longSleep: '睡太久也会累哦,试试固定起床时间~', + lowDeepSleep: '睡前放下手机,让大脑好好休息~', + lowRemSleep: '规律作息能帮助你做更多好梦~', + lowEfficiency: '调整一下卧室环境,会睡得更香哦~', + }, + general: '每一晚都是新的开始,照顾好自己~', + }, +}; + export const workoutHistory = { title: '锻炼总结', loading: '正在加载锻炼记录...', diff --git a/ios/OutLive.xcodeproj/project.pbxproj b/ios/OutLive.xcodeproj/project.pbxproj index cc76989..d95eb95 100644 --- a/ios/OutLive.xcodeproj/project.pbxproj +++ b/ios/OutLive.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,7 @@ 792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792C52612EB05B8F002F3F09 /* NativeToastManager.swift */; }; 794DD5D62ED3E3BB0046E2B4 /* AppStoreReviewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */; }; 794DD5D72ED3E3BB0046E2B4 /* AppStoreReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */; }; + 7981D9922EDFC0B5008D5F2D /* InfoPlist.strings in Sources */ = {isa = PBXBuildFile; fileRef = 7981D9912EDFC0B5008D5F2D /* InfoPlist.strings */; }; 79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */; }; 79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; }; 79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; }; @@ -68,6 +69,8 @@ 792C52612EB05B8F002F3F09 /* NativeToastManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NativeToastManager.swift; path = OutLive/NativeToastManager.swift; sourceTree = ""; }; 794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppStoreReviewManager.m; sourceTree = ""; }; 794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReviewManager.swift; sourceTree = ""; }; + 7981D9902EDFC0B5008D5F2D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + 7981D9932EDFC0B8008D5F2D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = ""; }; 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = ""; }; 79E80BA22EC5D92A004425BE /* medicineExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = medicineExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -91,7 +94,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */ = { + 79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -101,18 +104,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 79E80BA72EC5D92A004425BE /* medicine */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */, - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = medicine; - sourceTree = ""; - }; + 79E80BA72EC5D92A004425BE /* medicine */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = medicine; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -140,6 +132,7 @@ 13B07FAE1A68108700A75B9A /* OutLive */ = { isa = PBXGroup; children = ( + 7981D9912EDFC0B5008D5F2D /* InfoPlist.strings */, F11748412D0307B40044C1D9 /* AppDelegate.swift */, F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */, BB2F792B24A3F905000567C9 /* Supporting */, @@ -309,10 +302,11 @@ }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "OutLive" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; + developmentRegion = "zh-Hans"; hasScannedForEncodings = 0; knownRegions = ( en, + "zh-Hans", Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; @@ -500,6 +494,7 @@ 79E80BFF2EC5E127004425BE /* AppGroupUserDefaultsManager.m in Sources */, 79E80C002EC5E127004425BE /* WidgetManager.m in Sources */, 79E80C522EC5E500004425BE /* WidgetCenterHelper.swift in Sources */, + 7981D9922EDFC0B5008D5F2D /* InfoPlist.strings in Sources */, 792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */, 79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */, 79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */, @@ -530,6 +525,18 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 7981D9912EDFC0B5008D5F2D /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7981D9902EDFC0B5008D5F2D /* zh-Hans */, + 7981D9932EDFC0B8008D5F2D /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; @@ -620,7 +627,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = medicine/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = medicine; + INFOPLIST_KEY_CFBundleDisplayName = "用药计划"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -670,7 +677,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = medicine/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = medicine; + INFOPLIST_KEY_CFBundleDisplayName = "用药计划"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/ios/en.lproj/InfoPlist.strings b/ios/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..191fa9b --- /dev/null +++ b/ios/en.lproj/InfoPlist.strings @@ -0,0 +1,8 @@ +// +// InfoPlist.strings +// OutLive +// +// Created by richard on 2025/12/3. +// + +CFBundleDisplayName = "OutLive"; diff --git a/ios/medicine/Info.plist b/ios/medicine/Info.plist index 8873466..d1a218a 100644 --- a/ios/medicine/Info.plist +++ b/ios/medicine/Info.plist @@ -2,8 +2,6 @@ - CFBundleDisplayName - 用药计划 NSExtension NSExtensionPointIdentifier diff --git a/ios/zh-Hans.lproj/InfoPlist.strings b/ios/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..191fa9b --- /dev/null +++ b/ios/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,8 @@ +// +// InfoPlist.strings +// OutLive +// +// Created by richard on 2025/12/3. +// + +CFBundleDisplayName = "OutLive"; diff --git a/services/healthKitSync.ts b/services/healthKitSync.ts index 07ec399..80cf12c 100644 --- a/services/healthKitSync.ts +++ b/services/healthKitSync.ts @@ -16,10 +16,12 @@ import { fetchOxygenSaturation, fetchPersonalHealthData, fetchSmartHRVData, + fetchStepCount, saveHeight, saveWeight } from '@/utils/health'; import AsyncStorage from '@/utils/kvStore'; +import { logger } from '@/utils/logger'; import { convertHrvToStressIndex } from '@/utils/stress'; import dayjs from 'dayjs'; import { DailyHealthDataDto, updateDailyHealthData } from './users'; @@ -468,8 +470,6 @@ export async function getSyncStatusInfo(): Promise { * @param waterIntake - 当日饮水量(从应用内部获取,因为 HealthKit 可能不包含应用内记录) */ export async function syncDailyHealthReport(waterIntake?: number): Promise { - console.log('=== 开始同步每日健康报表 ==='); - try { const today = new Date(); const dateStr = dayjs(today).format('YYYY-MM-DD'); @@ -483,7 +483,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise 0 && { basalMetabolism: Math.round(basalEnergy) }), ...(sleepMinutes > 0 && { sleepMinutes }), ...(oxygenSaturation !== null && oxygenSaturation > 0 && { bloodOxygen: oxygenSaturation }), - ...(stressLevel > 0 && { stressLevel }) + ...(stressLevel > 0 && { stressLevel }), + ...(stepCount > 0 && { steps: Math.round(stepCount) }) }; - console.log('准备同步每日健康数据:', healthData); + logger.info('准备同步每日健康数据:', healthData); // 4. 检查是否需要同步 (与上次同步的数据比较) const lastSyncStatusStr = await AsyncStorage.getItem(DAILY_HEALTH_SYNC_KEY); @@ -576,7 +580,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise) => i18n.t(`sleepNotification.${key}`, options); + /** * 分析睡眠数据并发送通知 */ @@ -51,12 +55,22 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti // 构建通知正文 const sleepDuration = formatSleepDuration(totalSleepHours); - const efficiencyText = `睡眠效率 ${sleepEfficiency.toFixed(0)}%`; - const body = `您昨晚睡了 ${sleepDuration},${efficiencyText}。评分:${sleepScore}分`; + const body = t('body', { + duration: sleepDuration, + efficiency: sleepEfficiency.toFixed(0), + score: sleepScore + }); // 获取建议 const suggestion = getSleepSuggestion(analysis); + // 计算睡眠日期 + // 睡眠详情页面使用的日期逻辑是:传入的日期会查询从前一天18:00到当天12:00的数据 + // 所以我们应该传递醒来的日期(sessionEnd),这样用户点击通知后能看到正确的睡眠数据 + const sleepDate = analysis.sessionEnd + ? dayjs(analysis.sessionEnd).format('YYYY-MM-DD') + : dayjs().format('YYYY-MM-DD'); + return { title, body: `${body}\n${suggestion}`, @@ -64,6 +78,7 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti type: 'sleep_analysis', score: sleepScore, quality, + date: sleepDate, // 添加日期参数,用于点击通知后跳转 analysis: JSON.stringify(analysis), url: '/sleep-detail', // 点击通知跳转到睡眠详情页 }, @@ -79,32 +94,32 @@ function getQualityConfig(quality: string): { emoji: string; title: string; } { - const configs = { + const configs: Record = { excellent: { - emoji: '😴', - title: '睡眠质量优秀', + emoji: '🥳', + title: t('quality.excellent'), }, good: { - emoji: '😊', - title: '睡眠质量良好', + emoji: '☀️', + title: t('quality.good'), }, fair: { - emoji: '😐', - title: '睡眠质量一般', + emoji: '🌤️', + title: t('quality.fair'), }, poor: { - emoji: '😟', - title: '睡眠质量较差', + emoji: '🌛', + title: t('quality.poor'), }, very_poor: { - emoji: '😰', - title: '睡眠质量很差', + emoji: '🫂', + title: t('quality.veryPoor'), }, }; - return configs[quality as keyof typeof configs] || { - emoji: '💤', - title: '睡眠分析完成', + return configs[quality] || { + emoji: '🛏️', + title: t('quality.default'), }; } @@ -116,9 +131,9 @@ function formatSleepDuration(hours: number): string { const m = Math.round((hours - h) * 60); if (m === 0) { - return `${h}小时`; + return t('duration.hoursOnly', { hours: h }); } - return `${h}小时${m}分钟`; + return t('duration.hoursAndMinutes', { hours: h, minutes: m }); } /** @@ -127,35 +142,36 @@ function formatSleepDuration(hours: number): string { function getSleepSuggestion(analysis: SleepAnalysisData): string { const { quality, totalSleepHours, deepSleepPercentage, remSleepPercentage, sleepEfficiency } = analysis; - // 优秀或良好的睡眠 + // 优秀或良好的睡眠 - 给予鼓励 if (quality === 'excellent' || quality === 'good') { const tips = [ - '继续保持良好的睡眠习惯!', - '坚持规律作息,身体会感谢你!', - '优质睡眠让你精力充沛!', + t('tips.excellent.keepItUp'), + t('tips.excellent.greatJob'), + t('tips.excellent.energized'), + t('tips.excellent.proud'), ]; - return tips[Math.floor(Math.random() * tips.length)]; + return `✨ ${tips[Math.floor(Math.random() * tips.length)]}`; } - // 根据具体问题给出建议 + // 根据具体问题给出温暖的建议 const suggestions: string[] = []; if (totalSleepHours < 7) { - suggestions.push('建议增加睡眠时间至7-9小时'); + suggestions.push(t('tips.suggestions.shortSleep')); } else if (totalSleepHours > 9) { - suggestions.push('睡眠时间偏长,注意睡眠质量'); + suggestions.push(t('tips.suggestions.longSleep')); } if (deepSleepPercentage < 13) { - suggestions.push('深度睡眠不足,睡前避免使用电子设备'); + suggestions.push(t('tips.suggestions.lowDeepSleep')); } if (remSleepPercentage < 20) { - suggestions.push('REM睡眠不足,保持规律的作息时间'); + suggestions.push(t('tips.suggestions.lowRemSleep')); } if (sleepEfficiency < 85) { - suggestions.push('睡眠效率较低,改善睡眠环境'); + suggestions.push(t('tips.suggestions.lowEfficiency')); } // 如果有具体建议,返回第一条;否则返回通用建议 @@ -163,29 +179,5 @@ function getSleepSuggestion(analysis: SleepAnalysisData): string { return `💡 ${suggestions[0]}`; } - return '建议关注睡眠质量,保持良好作息'; + return `💡 ${t('tips.general')}`; } - -/** - * 发送简单的睡眠提醒(用于测试) - */ -export async function sendSimpleSleepReminder(userName: string = '朋友'): Promise { - try { - await Notifications.scheduleNotificationAsync({ - content: { - title: '😴 睡眠质量分析', - body: `${userName},您的睡眠数据已更新,点击查看详细分析`, - data: { - type: 'sleep_reminder', - url: '/sleep-detail', - }, - sound: 'default', - }, - trigger: null, - }); - - logger.info('简单睡眠提醒已发送'); - } catch (error) { - logger.error('发送简单睡眠提醒失败:', error); - } -} \ No newline at end of file diff --git a/services/users.ts b/services/users.ts index 318a6d5..7cb3edb 100644 --- a/services/users.ts +++ b/services/users.ts @@ -64,6 +64,7 @@ export type DailyHealthDataDto = { sleepMinutes?: number; // minutes bloodOxygen?: number; // % (0-100) stressLevel?: number; // ms (based on HRV) + steps?: number; // 步数 }; export async function updateDailyHealthData(dto: DailyHealthDataDto): Promise<{ diff --git a/utils/SimpleEventEmitter.ts b/utils/SimpleEventEmitter.ts index 61e2ce6..a866f22 100644 --- a/utils/SimpleEventEmitter.ts +++ b/utils/SimpleEventEmitter.ts @@ -93,7 +93,7 @@ export class SimpleEventEmitter { * @param event 事件名称 * @returns 监听器数组的副本 */ - listeners(event: string): ((...args: any[]) => void)[] { + getListeners(event: string): ((...args: any[]) => void)[] { return this.listeners[event] ? [...this.listeners[event]] : []; } diff --git a/utils/mockHealthData.ts b/utils/mockHealthData.ts deleted file mode 100644 index 0a93ce8..0000000 --- a/utils/mockHealthData.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { HourlyStepData, TodayHealthData } from './health'; - -// Mock的每小时步数数据,模拟真实的一天活动模式 -export const mockHourlySteps: HourlyStepData[] = [ - { hour: 0, steps: 0 }, // 午夜 - { hour: 1, steps: 0 }, // 凌晨 - { hour: 2, steps: 0 }, - { hour: 3, steps: 0 }, - { hour: 4, steps: 0 }, - { hour: 5, steps: 0 }, - { hour: 6, steps: 120 }, // 早晨起床 - { hour: 7, steps: 450 }, // 晨练/上班准备 - { hour: 8, steps: 680 }, // 上班通勤 - { hour: 9, steps: 320 }, // 工作时间 - { hour: 10, steps: 180 }, // 办公室内活动 - { hour: 11, steps: 280 }, // 会议/活动 - { hour: 12, steps: 520 }, // 午餐时间 - { hour: 13, steps: 150 }, // 午休 - { hour: 14, steps: 240 }, // 下午工作 - { hour: 15, steps: 300 }, // 工作活动 - { hour: 16, steps: 380 }, // 会议/外出 - { hour: 17, steps: 480 }, // 下班通勤 - { hour: 18, steps: 620 }, // 晚餐/活动 - { hour: 19, steps: 350 }, // 晚间活动 - { hour: 20, steps: 280 }, // 散步 - { hour: 21, steps: 150 }, // 休闲时间 - { hour: 22, steps: 80 }, // 准备睡觉 - { hour: 23, steps: 30 }, // 睡前 -]; - -// Mock的完整健康数据 -export const mockHealthData: TodayHealthData = { - steps: 6140, // 总步数 - hourlySteps: mockHourlySteps, - activeEnergyBurned: 420, - basalEnergyBurned: 1680, - sleepDuration: 480, // 8小时 - hrv: 45, - activeCalories: 420, - activeCaloriesGoal: 350, - exerciseMinutes: 32, - exerciseMinutesGoal: 30, - standHours: 8, - standHoursGoal: 12, - oxygenSaturation: 98.2, - heartRate: 72, -}; - -// 生成随机的每小时步数数据(用于测试不同的数据模式) -export const generateRandomHourlySteps = (): HourlyStepData[] => { - return Array.from({ length: 24 }, (_, hour) => { - let steps = 0; - - // 模拟真实的活动模式 - if (hour >= 6 && hour <= 22) { - if (hour >= 7 && hour <= 9) { - // 早晨高峰期 - steps = Math.floor(Math.random() * 600) + 200; - } else if (hour >= 12 && hour <= 13) { - // 午餐时间 - steps = Math.floor(Math.random() * 400) + 300; - } else if (hour >= 17 && hour <= 19) { - // 晚间活跃期 - steps = Math.floor(Math.random() * 500) + 250; - } else if (hour >= 6 && hour <= 22) { - // 白天正常活动 - steps = Math.floor(Math.random() * 300) + 50; - } - } else { - // 夜间很少活动 - steps = Math.floor(Math.random() * 50); - } - - return { hour, steps }; - }); -}; - -// 不同活动模式的预设数据 -export const activityPatterns = { - // 久坐办公族 - sedentary: Array.from({ length: 24 }, (_, hour) => ({ - hour, - steps: hour >= 7 && hour <= 18 ? Math.floor(Math.random() * 200) + 50 : - hour >= 19 && hour <= 21 ? Math.floor(Math.random() * 300) + 100 : - Math.floor(Math.random() * 20) - })), - - // 活跃用户 - active: Array.from({ length: 24 }, (_, hour) => ({ - hour, - steps: hour >= 6 && hour <= 8 ? Math.floor(Math.random() * 800) + 400 : - hour >= 12 && hour <= 13 ? Math.floor(Math.random() * 600) + 300 : - hour >= 17 && hour <= 20 ? Math.floor(Math.random() * 900) + 500 : - hour >= 9 && hour <= 16 ? Math.floor(Math.random() * 400) + 100 : - Math.floor(Math.random() * 50) - })), - - // 健身爱好者 - fitness: Array.from({ length: 24 }, (_, hour) => ({ - hour, - steps: hour === 6 ? Math.floor(Math.random() * 1200) + 800 : // 晨跑 - hour === 18 ? Math.floor(Math.random() * 1000) + 600 : // 晚间锻炼 - hour >= 7 && hour <= 17 ? Math.floor(Math.random() * 300) + 100 : - Math.floor(Math.random() * 50) - })), -}; - -// 用于快速切换测试数据的函数 -export const getTestHealthData = (pattern: 'mock' | 'random' | 'sedentary' | 'active' | 'fitness' = 'mock'): TodayHealthData => { - let hourlySteps: HourlyStepData[]; - - switch (pattern) { - case 'random': - hourlySteps = generateRandomHourlySteps(); - break; - case 'sedentary': - hourlySteps = activityPatterns.sedentary; - break; - case 'active': - hourlySteps = activityPatterns.active; - break; - case 'fitness': - hourlySteps = activityPatterns.fitness; - break; - default: - hourlySteps = mockHourlySteps; - } - - const totalSteps = hourlySteps.reduce((sum, data) => sum + data.steps, 0); - - return { - ...mockHealthData, - steps: totalSteps, - hourlySteps, - }; -}; \ No newline at end of file diff --git a/utils/workoutTestHelper.ts b/utils/workoutTestHelper.ts deleted file mode 100644 index 5a9c0c1..0000000 --- a/utils/workoutTestHelper.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { workoutMonitorService } from '@/services/workoutMonitor'; -import { WorkoutData } from '@/utils/health'; - -/** - * 锻炼测试工具 - * 用于开发和测试锻炼监听功能 - */ -export class WorkoutTestHelper { - /** - * 模拟一个锻炼完成事件 - */ - static async simulateWorkoutCompletion(): Promise { - console.log('=== 开始模拟锻炼完成事件 ==='); - - try { - // 手动触发锻炼检查 - await workoutMonitorService.manualCheck(); - console.log('✅ 锻炼检查已手动触发'); - } catch (error) { - console.error('❌ 模拟锻炼完成事件失败:', error); - } - } - - /** - * 获取锻炼监听服务状态 - */ - static getWorkoutMonitorStatus(): any { - return workoutMonitorService.getStatus(); - } - - /** - * 创建测试用的模拟锻炼数据 - */ - static createMockWorkout(type: string = 'running'): WorkoutData { - const now = new Date(); - const duration = 30 * 60; // 30分钟 - const startTime = new Date(now.getTime() - duration * 1000); - - const workoutTypes: Record = { - running: 37, - cycling: 13, - swimming: 46, - yoga: 57, - functionalstrengthtraining: 20, - traditionalstrengthtraining: 50, - highintensityintervaltraining: 63, - walking: 52, - }; - - return { - id: `mock-workout-${Date.now()}`, - startDate: startTime.toISOString(), - endDate: now.toISOString(), - duration: duration, - workoutActivityType: workoutTypes[type] || 37, - workoutActivityTypeString: type, - totalEnergyBurned: Math.round(Math.random() * 300 + 100), // 100-400千卡 - totalDistance: type === 'running' || type === 'cycling' ? Math.round(Math.random() * 10000 + 1000) : undefined, - averageHeartRate: Math.round(Math.random() * 50 + 120), // 120-170次/分 - source: { - name: 'Test App', - bundleIdentifier: 'com.test.app' - }, - metadata: { - HKAverageMETs: Math.random() * 10 + 5 // 5-15 METs - } - }; - } - - /** - * 测试不同类型的锻炼通知 - */ - static async testWorkoutNotifications(): Promise { - console.log('=== 开始测试不同类型锻炼通知 ==='); - - const workoutTypes = ['running', 'cycling', 'swimming', 'yoga', 'functionalstrengthtraining', 'highintensityintervaltraining']; - - for (const type of workoutTypes) { - console.log(`--- 测试 ${type} 锻炼通知 ---`); - - try { - // 这里需要导入通知服务来直接测试 - const { analyzeWorkoutAndSendNotification } = await import('@/services/workoutNotificationService'); - const mockWorkout = this.createMockWorkout(type); - - await analyzeWorkoutAndSendNotification(mockWorkout); - - console.log(`✅ ${type} 锻炼通知测试成功`); - - // 等待一段时间再测试下一个 - await new Promise(resolve => setTimeout(resolve, 2000)); - } catch (error) { - console.error(`❌ ${type} 锻炼通知测试失败:`, error); - } - } - - console.log('=== 锻炼通知测试完成 ==='); - } - - /** - * 测试偏好设置功能 - */ - static async testPreferences(): Promise { - console.log('=== 开始测试偏好设置功能 ==='); - - try { - const { - getWorkoutNotificationPreferences, - saveWorkoutNotificationPreferences, - isNotificationTimeAllowed, - isWorkoutTypeEnabled - } = await import('@/utils/workoutPreferences'); - - // 获取当前设置 - const currentPrefs = await getWorkoutNotificationPreferences(); - console.log('当前偏好设置:', currentPrefs); - - // 测试时间检查 - const timeAllowed = await isNotificationTimeAllowed(); - console.log('当前时间是否允许通知:', timeAllowed); - - // 测试锻炼类型检查 - const runningEnabled = await isWorkoutTypeEnabled('running'); - console.log('跑步通知是否启用:', runningEnabled); - - // 临时修改设置 - await saveWorkoutNotificationPreferences({ - enabled: true, - startTimeHour: 9, - endTimeHour: 21, - enabledWorkoutTypes: ['running', 'cycling'] - }); - - console.log('临时设置已保存'); - - // 恢复原始设置 - await saveWorkoutNotificationPreferences(currentPrefs); - console.log('原始设置已恢复'); - - console.log('✅ 偏好设置功能测试完成'); - } catch (error) { - console.error('❌ 偏好设置功能测试失败:', error); - } - } - - /** - * 运行完整的测试套件 - */ - static async runFullTestSuite(): Promise { - console.log('🧪 开始运行锻炼监听功能完整测试套件'); - - try { - // 1. 检查服务状态 - console.log('\n1. 检查服务状态...'); - const status = this.getWorkoutMonitorStatus(); - console.log('服务状态:', status); - - // 2. 测试偏好设置 - console.log('\n2. 测试偏好设置...'); - await this.testPreferences(); - - // 3. 测试通知功能 - console.log('\n3. 测试通知功能...'); - await this.testWorkoutNotifications(); - - // 4. 测试手动触发 - console.log('\n4. 测试手动触发...'); - await this.simulateWorkoutCompletion(); - - console.log('\n🎉 完整测试套件运行完成!'); - } catch (error) { - console.error('\n❌ 测试套件运行失败:', error); - } - } -} - -/** - * 开发者调试函数 - * 可以在开发者控制台中调用 - */ -declare global { - interface Window { - testWorkoutNotifications: () => Promise; - testWorkoutPreferences: () => Promise; - simulateWorkoutCompletion: () => Promise; - runWorkoutTestSuite: () => Promise; - } -} - -// 在开发环境中暴露调试函数 -if (__DEV__) { - // 这些函数可以在开发者控制台中调用 - // 例如: window.testWorkoutNotifications() - - // 注意:这些函数需要在实际运行环境中绑定 - // 可以在应用的初始化代码中添加: - // window.testWorkoutNotifications = WorkoutTestHelper.testWorkoutNotifications; -} \ No newline at end of file