feat: 增加睡眠分析通知功能,支持睡眠质量评估与建议
This commit is contained in:
@@ -66,6 +66,7 @@ export default function ExploreScreen() {
|
|||||||
const userProfile = useAppSelector((s) => s.user.profile);
|
const userProfile = useAppSelector((s) => s.user.profile);
|
||||||
const todayWaterStats = useAppSelector((s) => s.water.todayStats);
|
const todayWaterStats = useAppSelector((s) => s.water.todayStats);
|
||||||
|
|
||||||
|
|
||||||
const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard();
|
const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
export const workoutHistory = {
|
||||||
title: 'Workout Summary',
|
title: 'Workout Summary',
|
||||||
loading: 'Loading workout records...',
|
loading: 'Loading workout records...',
|
||||||
|
|||||||
@@ -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 = {
|
export const workoutHistory = {
|
||||||
title: '锻炼总结',
|
title: '锻炼总结',
|
||||||
loading: '正在加载锻炼记录...',
|
loading: '正在加载锻炼记录...',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 60;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792C52612EB05B8F002F3F09 /* NativeToastManager.swift */; };
|
792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792C52612EB05B8F002F3F09 /* NativeToastManager.swift */; };
|
||||||
794DD5D62ED3E3BB0046E2B4 /* AppStoreReviewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */; };
|
794DD5D62ED3E3BB0046E2B4 /* AppStoreReviewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */; };
|
||||||
794DD5D72ED3E3BB0046E2B4 /* AppStoreReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */; };
|
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 */; };
|
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 */; };
|
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; };
|
||||||
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; };
|
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 = "<group>"; };
|
792C52612EB05B8F002F3F09 /* NativeToastManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NativeToastManager.swift; path = OutLive/NativeToastManager.swift; sourceTree = "<group>"; };
|
||||||
794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppStoreReviewManager.m; sourceTree = "<group>"; };
|
794DD5D42ED3E3BB0046E2B4 /* AppStoreReviewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppStoreReviewManager.m; sourceTree = "<group>"; };
|
||||||
794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReviewManager.swift; sourceTree = "<group>"; };
|
794DD5D52ED3E3BB0046E2B4 /* AppStoreReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReviewManager.swift; sourceTree = "<group>"; };
|
||||||
|
7981D9902EDFC0B5008D5F2D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
7981D9932EDFC0B8008D5F2D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
|
79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
|
||||||
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
|
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
|
||||||
79E80BA22EC5D92A004425BE /* medicineExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = medicineExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */ = {
|
79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Info.plist,
|
Info.plist,
|
||||||
@@ -101,18 +104,7 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
79E80BA72EC5D92A004425BE /* medicine */ = {
|
79E80BA72EC5D92A004425BE /* medicine */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = medicine; sourceTree = "<group>"; };
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */,
|
|
||||||
);
|
|
||||||
explicitFileTypes = {
|
|
||||||
};
|
|
||||||
explicitFolders = (
|
|
||||||
);
|
|
||||||
path = medicine;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -140,6 +132,7 @@
|
|||||||
13B07FAE1A68108700A75B9A /* OutLive */ = {
|
13B07FAE1A68108700A75B9A /* OutLive */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
7981D9912EDFC0B5008D5F2D /* InfoPlist.strings */,
|
||||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||||
F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */,
|
F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */,
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||||
@@ -309,10 +302,11 @@
|
|||||||
};
|
};
|
||||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "OutLive" */;
|
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "OutLive" */;
|
||||||
compatibilityVersion = "Xcode 3.2";
|
compatibilityVersion = "Xcode 3.2";
|
||||||
developmentRegion = en;
|
developmentRegion = "zh-Hans";
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
|
"zh-Hans",
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||||
@@ -500,6 +494,7 @@
|
|||||||
79E80BFF2EC5E127004425BE /* AppGroupUserDefaultsManager.m in Sources */,
|
79E80BFF2EC5E127004425BE /* AppGroupUserDefaultsManager.m in Sources */,
|
||||||
79E80C002EC5E127004425BE /* WidgetManager.m in Sources */,
|
79E80C002EC5E127004425BE /* WidgetManager.m in Sources */,
|
||||||
79E80C522EC5E500004425BE /* WidgetCenterHelper.swift in Sources */,
|
79E80C522EC5E500004425BE /* WidgetCenterHelper.swift in Sources */,
|
||||||
|
7981D9922EDFC0B5008D5F2D /* InfoPlist.strings in Sources */,
|
||||||
792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */,
|
792C52632EB05B8F002F3F09 /* NativeToastManager.swift in Sources */,
|
||||||
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */,
|
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */,
|
||||||
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */,
|
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */,
|
||||||
@@ -530,6 +525,18 @@
|
|||||||
};
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
7981D9912EDFC0B5008D5F2D /* InfoPlist.strings */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
7981D9902EDFC0B5008D5F2D /* zh-Hans */,
|
||||||
|
7981D9932EDFC0B8008D5F2D /* en */,
|
||||||
|
);
|
||||||
|
name = InfoPlist.strings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@@ -620,7 +627,7 @@
|
|||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = medicine/Info.plist;
|
INFOPLIST_FILE = medicine/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = medicine;
|
INFOPLIST_KEY_CFBundleDisplayName = "用药计划";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -670,7 +677,7 @@
|
|||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = medicine/Info.plist;
|
INFOPLIST_FILE = medicine/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = medicine;
|
INFOPLIST_KEY_CFBundleDisplayName = "用药计划";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
8
ios/en.lproj/InfoPlist.strings
Normal file
8
ios/en.lproj/InfoPlist.strings
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// InfoPlist.strings
|
||||||
|
// OutLive
|
||||||
|
//
|
||||||
|
// Created by richard on 2025/12/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
CFBundleDisplayName = "OutLive";
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>用药计划</string>
|
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
8
ios/zh-Hans.lproj/InfoPlist.strings
Normal file
8
ios/zh-Hans.lproj/InfoPlist.strings
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// InfoPlist.strings
|
||||||
|
// OutLive
|
||||||
|
//
|
||||||
|
// Created by richard on 2025/12/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
CFBundleDisplayName = "OutLive";
|
||||||
@@ -16,10 +16,12 @@ import {
|
|||||||
fetchOxygenSaturation,
|
fetchOxygenSaturation,
|
||||||
fetchPersonalHealthData,
|
fetchPersonalHealthData,
|
||||||
fetchSmartHRVData,
|
fetchSmartHRVData,
|
||||||
|
fetchStepCount,
|
||||||
saveHeight,
|
saveHeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
} from '@/utils/health';
|
} from '@/utils/health';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
import { convertHrvToStressIndex } from '@/utils/stress';
|
import { convertHrvToStressIndex } from '@/utils/stress';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { DailyHealthDataDto, updateDailyHealthData } from './users';
|
import { DailyHealthDataDto, updateDailyHealthData } from './users';
|
||||||
@@ -468,8 +470,6 @@ export async function getSyncStatusInfo(): Promise<SyncStatus | null> {
|
|||||||
* @param waterIntake - 当日饮水量(从应用内部获取,因为 HealthKit 可能不包含应用内记录)
|
* @param waterIntake - 当日饮水量(从应用内部获取,因为 HealthKit 可能不包含应用内记录)
|
||||||
*/
|
*/
|
||||||
export async function syncDailyHealthReport(waterIntake?: number): Promise<boolean> {
|
export async function syncDailyHealthReport(waterIntake?: number): Promise<boolean> {
|
||||||
console.log('=== 开始同步每日健康报表 ===');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dateStr = dayjs(today).format('YYYY-MM-DD');
|
const dateStr = dayjs(today).format('YYYY-MM-DD');
|
||||||
@@ -483,7 +483,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
|
|||||||
exerciseMinutesData,
|
exerciseMinutesData,
|
||||||
standHoursData,
|
standHoursData,
|
||||||
oxygenSaturation,
|
oxygenSaturation,
|
||||||
hrvData
|
hrvData,
|
||||||
|
stepCount
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// 卡路里
|
// 卡路里
|
||||||
fetchActiveEnergyBurned({
|
fetchActiveEnergyBurned({
|
||||||
@@ -507,7 +508,9 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
|
|||||||
endDate: dayjs(today).endOf('day').toISOString()
|
endDate: dayjs(today).endOf('day').toISOString()
|
||||||
}),
|
}),
|
||||||
// HRV (用于计算压力)
|
// HRV (用于计算压力)
|
||||||
fetchSmartHRVData(today)
|
fetchSmartHRVData(today),
|
||||||
|
// 步数
|
||||||
|
fetchStepCount(today)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. 数据处理与计算
|
// 2. 数据处理与计算
|
||||||
@@ -555,10 +558,11 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
|
|||||||
...(basalEnergy > 0 && { basalMetabolism: Math.round(basalEnergy) }),
|
...(basalEnergy > 0 && { basalMetabolism: Math.round(basalEnergy) }),
|
||||||
...(sleepMinutes > 0 && { sleepMinutes }),
|
...(sleepMinutes > 0 && { sleepMinutes }),
|
||||||
...(oxygenSaturation !== null && oxygenSaturation > 0 && { bloodOxygen: oxygenSaturation }),
|
...(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. 检查是否需要同步 (与上次同步的数据比较)
|
// 4. 检查是否需要同步 (与上次同步的数据比较)
|
||||||
const lastSyncStatusStr = await AsyncStorage.getItem(DAILY_HEALTH_SYNC_KEY);
|
const lastSyncStatusStr = await AsyncStorage.getItem(DAILY_HEALTH_SYNC_KEY);
|
||||||
@@ -576,7 +580,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
|
|||||||
healthData.basalMetabolism !== lastData.basalMetabolism ||
|
healthData.basalMetabolism !== lastData.basalMetabolism ||
|
||||||
healthData.sleepMinutes !== lastData.sleepMinutes ||
|
healthData.sleepMinutes !== lastData.sleepMinutes ||
|
||||||
healthData.bloodOxygen !== lastData.bloodOxygen ||
|
healthData.bloodOxygen !== lastData.bloodOxygen ||
|
||||||
healthData.stressLevel !== lastData.stressLevel;
|
healthData.stressLevel !== lastData.stressLevel ||
|
||||||
|
healthData.steps !== lastData.steps;
|
||||||
|
|
||||||
if (!isDifferent) {
|
if (!isDifferent) {
|
||||||
console.log('每日健康数据无变化,跳过同步');
|
console.log('每日健康数据无变化,跳过同步');
|
||||||
|
|||||||
@@ -265,6 +265,15 @@ export class NotificationService {
|
|||||||
console.log('用户点击了 HRV 压力通知', data);
|
console.log('用户点击了 HRV 压力通知', data);
|
||||||
const targetUrl = (data?.url as string) || '/(tabs)/statistics';
|
const targetUrl = (data?.url as string) || '/(tabs)/statistics';
|
||||||
router.push(targetUrl as any);
|
router.push(targetUrl as any);
|
||||||
|
} else if (data?.type === NotificationTypes.SLEEP_ANALYSIS || data?.type === NotificationTypes.SLEEP_REMINDER) {
|
||||||
|
// 处理睡眠分析通知
|
||||||
|
console.log('用户点击了睡眠分析通知', data);
|
||||||
|
// 从通知数据中获取日期,如果没有则使用今天
|
||||||
|
const sleepDate = data?.date as string || new Date().toISOString().split('T')[0];
|
||||||
|
router.push({
|
||||||
|
pathname: ROUTES.SLEEP_DETAIL,
|
||||||
|
params: { date: sleepDate },
|
||||||
|
} as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +616,8 @@ export const NotificationTypes = {
|
|||||||
FASTING_START: 'fasting_start',
|
FASTING_START: 'fasting_start',
|
||||||
FASTING_END: 'fasting_end',
|
FASTING_END: 'fasting_end',
|
||||||
HRV_STRESS_ALERT: 'hrv_stress_alert',
|
HRV_STRESS_ALERT: 'hrv_stress_alert',
|
||||||
|
SLEEP_ANALYSIS: 'sleep_analysis',
|
||||||
|
SLEEP_REMINDER: 'sleep_reminder',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 便捷方法
|
// 便捷方法
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
* 负责在睡眠分析完成后发送通知,提供睡眠质量评估和建议
|
* 负责在睡眠分析完成后发送通知,提供睡眠质量评估和建议
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import i18n from '@/i18n';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { SleepAnalysisData } from './sleepMonitor';
|
import { SleepAnalysisData } from './sleepMonitor';
|
||||||
|
|
||||||
|
const t = (key: string, options?: Record<string, unknown>) => i18n.t(`sleepNotification.${key}`, options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分析睡眠数据并发送通知
|
* 分析睡眠数据并发送通知
|
||||||
*/
|
*/
|
||||||
@@ -51,12 +55,22 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti
|
|||||||
|
|
||||||
// 构建通知正文
|
// 构建通知正文
|
||||||
const sleepDuration = formatSleepDuration(totalSleepHours);
|
const sleepDuration = formatSleepDuration(totalSleepHours);
|
||||||
const efficiencyText = `睡眠效率 ${sleepEfficiency.toFixed(0)}%`;
|
const body = t('body', {
|
||||||
const body = `您昨晚睡了 ${sleepDuration},${efficiencyText}。评分:${sleepScore}分`;
|
duration: sleepDuration,
|
||||||
|
efficiency: sleepEfficiency.toFixed(0),
|
||||||
|
score: sleepScore
|
||||||
|
});
|
||||||
|
|
||||||
// 获取建议
|
// 获取建议
|
||||||
const suggestion = getSleepSuggestion(analysis);
|
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 {
|
return {
|
||||||
title,
|
title,
|
||||||
body: `${body}\n${suggestion}`,
|
body: `${body}\n${suggestion}`,
|
||||||
@@ -64,6 +78,7 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti
|
|||||||
type: 'sleep_analysis',
|
type: 'sleep_analysis',
|
||||||
score: sleepScore,
|
score: sleepScore,
|
||||||
quality,
|
quality,
|
||||||
|
date: sleepDate, // 添加日期参数,用于点击通知后跳转
|
||||||
analysis: JSON.stringify(analysis),
|
analysis: JSON.stringify(analysis),
|
||||||
url: '/sleep-detail', // 点击通知跳转到睡眠详情页
|
url: '/sleep-detail', // 点击通知跳转到睡眠详情页
|
||||||
},
|
},
|
||||||
@@ -79,32 +94,32 @@ function getQualityConfig(quality: string): {
|
|||||||
emoji: string;
|
emoji: string;
|
||||||
title: string;
|
title: string;
|
||||||
} {
|
} {
|
||||||
const configs = {
|
const configs: Record<string, { emoji: string; title: string }> = {
|
||||||
excellent: {
|
excellent: {
|
||||||
emoji: '😴',
|
emoji: '🥳',
|
||||||
title: '睡眠质量优秀',
|
title: t('quality.excellent'),
|
||||||
},
|
},
|
||||||
good: {
|
good: {
|
||||||
emoji: '😊',
|
emoji: '☀️',
|
||||||
title: '睡眠质量良好',
|
title: t('quality.good'),
|
||||||
},
|
},
|
||||||
fair: {
|
fair: {
|
||||||
emoji: '😐',
|
emoji: '🌤️',
|
||||||
title: '睡眠质量一般',
|
title: t('quality.fair'),
|
||||||
},
|
},
|
||||||
poor: {
|
poor: {
|
||||||
emoji: '😟',
|
emoji: '🌛',
|
||||||
title: '睡眠质量较差',
|
title: t('quality.poor'),
|
||||||
},
|
},
|
||||||
very_poor: {
|
very_poor: {
|
||||||
emoji: '😰',
|
emoji: '🫂',
|
||||||
title: '睡眠质量很差',
|
title: t('quality.veryPoor'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return configs[quality as keyof typeof configs] || {
|
return configs[quality] || {
|
||||||
emoji: '💤',
|
emoji: '🛏️',
|
||||||
title: '睡眠分析完成',
|
title: t('quality.default'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +131,9 @@ function formatSleepDuration(hours: number): string {
|
|||||||
const m = Math.round((hours - h) * 60);
|
const m = Math.round((hours - h) * 60);
|
||||||
|
|
||||||
if (m === 0) {
|
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 {
|
function getSleepSuggestion(analysis: SleepAnalysisData): string {
|
||||||
const { quality, totalSleepHours, deepSleepPercentage, remSleepPercentage, sleepEfficiency } = analysis;
|
const { quality, totalSleepHours, deepSleepPercentage, remSleepPercentage, sleepEfficiency } = analysis;
|
||||||
|
|
||||||
// 优秀或良好的睡眠
|
// 优秀或良好的睡眠 - 给予鼓励
|
||||||
if (quality === 'excellent' || quality === 'good') {
|
if (quality === 'excellent' || quality === 'good') {
|
||||||
const tips = [
|
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[] = [];
|
const suggestions: string[] = [];
|
||||||
|
|
||||||
if (totalSleepHours < 7) {
|
if (totalSleepHours < 7) {
|
||||||
suggestions.push('建议增加睡眠时间至7-9小时');
|
suggestions.push(t('tips.suggestions.shortSleep'));
|
||||||
} else if (totalSleepHours > 9) {
|
} else if (totalSleepHours > 9) {
|
||||||
suggestions.push('睡眠时间偏长,注意睡眠质量');
|
suggestions.push(t('tips.suggestions.longSleep'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deepSleepPercentage < 13) {
|
if (deepSleepPercentage < 13) {
|
||||||
suggestions.push('深度睡眠不足,睡前避免使用电子设备');
|
suggestions.push(t('tips.suggestions.lowDeepSleep'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remSleepPercentage < 20) {
|
if (remSleepPercentage < 20) {
|
||||||
suggestions.push('REM睡眠不足,保持规律的作息时间');
|
suggestions.push(t('tips.suggestions.lowRemSleep'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sleepEfficiency < 85) {
|
if (sleepEfficiency < 85) {
|
||||||
suggestions.push('睡眠效率较低,改善睡眠环境');
|
suggestions.push(t('tips.suggestions.lowEfficiency'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有具体建议,返回第一条;否则返回通用建议
|
// 如果有具体建议,返回第一条;否则返回通用建议
|
||||||
@@ -163,29 +179,5 @@ function getSleepSuggestion(analysis: SleepAnalysisData): string {
|
|||||||
return `💡 ${suggestions[0]}`;
|
return `💡 ${suggestions[0]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return '建议关注睡眠质量,保持良好作息';
|
return `💡 ${t('tips.general')}`;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送简单的睡眠提醒(用于测试)
|
|
||||||
*/
|
|
||||||
export async function sendSimpleSleepReminder(userName: string = '朋友'): Promise<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ export type DailyHealthDataDto = {
|
|||||||
sleepMinutes?: number; // minutes
|
sleepMinutes?: number; // minutes
|
||||||
bloodOxygen?: number; // % (0-100)
|
bloodOxygen?: number; // % (0-100)
|
||||||
stressLevel?: number; // ms (based on HRV)
|
stressLevel?: number; // ms (based on HRV)
|
||||||
|
steps?: number; // 步数
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updateDailyHealthData(dto: DailyHealthDataDto): Promise<{
|
export async function updateDailyHealthData(dto: DailyHealthDataDto): Promise<{
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class SimpleEventEmitter {
|
|||||||
* @param event 事件名称
|
* @param event 事件名称
|
||||||
* @returns 监听器数组的副本
|
* @returns 监听器数组的副本
|
||||||
*/
|
*/
|
||||||
listeners(event: string): ((...args: any[]) => void)[] {
|
getListeners(event: string): ((...args: any[]) => void)[] {
|
||||||
return this.listeners[event] ? [...this.listeners[event]] : [];
|
return this.listeners[event] ? [...this.listeners[event]] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { workoutMonitorService } from '@/services/workoutMonitor';
|
|
||||||
import { WorkoutData } from '@/utils/health';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 锻炼测试工具
|
|
||||||
* 用于开发和测试锻炼监听功能
|
|
||||||
*/
|
|
||||||
export class WorkoutTestHelper {
|
|
||||||
/**
|
|
||||||
* 模拟一个锻炼完成事件
|
|
||||||
*/
|
|
||||||
static async simulateWorkoutCompletion(): Promise<void> {
|
|
||||||
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<string, number> = {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void>;
|
|
||||||
testWorkoutPreferences: () => Promise<void>;
|
|
||||||
simulateWorkoutCompletion: () => Promise<void>;
|
|
||||||
runWorkoutTestSuite: () => Promise<void>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在开发环境中暴露调试函数
|
|
||||||
if (__DEV__) {
|
|
||||||
// 这些函数可以在开发者控制台中调用
|
|
||||||
// 例如: window.testWorkoutNotifications()
|
|
||||||
|
|
||||||
// 注意:这些函数需要在实际运行环境中绑定
|
|
||||||
// 可以在应用的初始化代码中添加:
|
|
||||||
// window.testWorkoutNotifications = WorkoutTestHelper.testWorkoutNotifications;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user