From 3c416545db6df7ce225c538f3c079410e1c80528 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 5 Sep 2025 21:58:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=80=E5=A4=A7?= =?UTF-8?q?=E5=BF=83=E7=8E=87=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=B5=84=E6=96=99=E7=BC=96=E8=BE=91=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BB=A5=E6=98=BE=E7=A4=BA=E6=9C=80=E5=A4=A7=E5=BF=83?= =?UTF-8?q?=E7=8E=87=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=BC=98=E5=8C=96=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/statistics.tsx | 5 ++-- app/profile/edit.tsx | 55 ++++++++++++++++++++++++++++++++--- ios/Podfile.lock | 8 ++--- ios/digitalpilates/Info.plist | 48 ------------------------------ services/quickActions.ts | 4 +-- utils/health.ts | 51 ++++++++++++++++++++++++++------ 6 files changed, 100 insertions(+), 71 deletions(-) diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 18b0a42..fd4f424 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -3,7 +3,6 @@ import { DateSelector } from '@/components/DateSelector'; import { FitnessRingsCard } from '@/components/FitnessRingsCard'; import { MoodCard } from '@/components/MoodCard'; import { NutritionRadarCard } from '@/components/NutritionRadarCard'; -import HeartRateCard from '@/components/statistic/HeartRateCard'; import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard'; import StepsCard from '@/components/StepsCard'; import { StressMeter } from '@/components/StressMeter'; @@ -655,13 +654,13 @@ export default function ExploreScreen() { {/* 心率卡片 */} - + {/* - + */} diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx index 88fdd15..1d22c7f 100644 --- a/app/profile/edit.tsx +++ b/app/profile/edit.tsx @@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { fetchMyProfile, updateUserProfile } from '@/store/userSlice'; +import { fetchMaximumHeartRate } from '@/utils/health'; import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import DateTimePicker from '@react-native-community/datetimepicker'; @@ -41,6 +42,7 @@ interface UserProfile { avatarUri?: string | null; avatarBase64?: string | null; // 兼容旧逻辑(不再上报) activityLevel?: number; // 活动水平 1-4 + maxHeartRate?: number; // 最大心率 } const STORAGE_KEY = '@user_profile'; @@ -68,6 +70,7 @@ export default function EditProfileScreen() { height: undefined, avatarUri: null, activityLevel: undefined, + maxHeartRate: undefined, }); // 出生日期选择器 @@ -93,6 +96,7 @@ export default function EditProfileScreen() { height: undefined, avatarUri: null, activityLevel: undefined, + maxHeartRate: undefined, }; if (fromOnboarding) { try { @@ -122,6 +126,29 @@ export default function EditProfileScreen() { loadLocalProfile(); }, []); + // 获取最大心率数据 + useEffect(() => { + const loadMaximumHeartRate = async () => { + try { + const today = new Date(); + const startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); // 过去7天 + + const maxHeartRate = await fetchMaximumHeartRate({ + startDate: startDate.toISOString(), + endDate: today.toISOString(), + }); + + if (maxHeartRate !== null) { + setProfile(prev => ({ ...prev, maxHeartRate })); + } + } catch (error) { + console.warn('获取最大心率失败', error); + } + }; + + loadMaximumHeartRate(); + }, []); + // 页面聚焦时拉取最新用户信息,并刷新本地 UI useFocusEffect( React.useCallback(() => { @@ -152,6 +179,8 @@ export default function EditProfileScreen() { weight: accountProfile?.weight ?? prev.weight ?? undefined, height: accountProfile?.height ?? prev.height ?? undefined, activityLevel: accountProfile?.activityLevel ?? prev.activityLevel ?? undefined, + // maxHeartRate 不从后端获取,保持本地状态 + maxHeartRate: prev.maxHeartRate, })); }, [accountProfile]); @@ -366,6 +395,20 @@ export default function EditProfileScreen() { openDatePicker(); }} /> + + {/* 最大心率 */} + { + // 最大心率不可编辑,只显示 + Alert.alert('提示', '最大心率数据从健康应用自动获取'); + }} + disabled={true} + hideArrow={true} + /> {/* 编辑弹窗 */} @@ -460,16 +503,20 @@ export default function EditProfileScreen() { ); } -function ProfileCard({ icon, iconUri, iconColor, title, value, onPress }: { +function ProfileCard({ icon, iconUri, iconColor, title, value, onPress, disabled, hideArrow }: { icon?: keyof typeof Ionicons.glyphMap; iconUri?: string; iconColor?: string; title: string; value: string; onPress: () => void; + disabled?: boolean; + hideArrow?: boolean; }) { + const Container = disabled ? View : TouchableOpacity; + return ( - + {iconUri ? {value} - + {!hideArrow && } - + ); } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 557c29b..ce48827 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2376,10 +2376,10 @@ SPEC CHECKSUMS: ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 - fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 + fast_float: 23278fd30b349f976d2014f4aec9e2d7bc1c3806 FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 - fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd - glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 + fmt: b85d977e8fe789fd71c77123f9f4920d88c4d170 + glog: 682871fb30f4a65f657bf357581110656ea90b08 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -2389,7 +2389,7 @@ SPEC CHECKSUMS: QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8 - RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + RCT-Folly: 031db300533e2dfa954cdc5a859b792d5c14ed7b RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5 RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8 RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index 3dae8a1..a7c4107 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -89,53 +89,5 @@ Light UIViewControllerBasedStatusBarAppearance - UIApplicationShortcutItems - - - UIApplicationShortcutItemIconFile - IconGlass - UIApplicationShortcutItemTitle - 喝水 - UIApplicationShortcutItemSubtitle - 快速记录饮水 - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).drink_water - UIApplicationShortcutItemUserInfo - - amount - 250 - - - - UIApplicationShortcutItemIconFile - IconGlass - UIApplicationShortcutItemTitle - 喝水 100ml - UIApplicationShortcutItemSubtitle - 快速记录饮水 - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).drink_water_100 - UIApplicationShortcutItemUserInfo - - amount - 100 - - - - UIApplicationShortcutItemIconFile - IconGlass - UIApplicationShortcutItemTitle - 喝水 200ml - UIApplicationShortcutItemSubtitle - 快速记录饮水 - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).drink_water_200 - UIApplicationShortcutItemUserInfo - - amount - 200 - - - diff --git a/services/quickActions.ts b/services/quickActions.ts index aeac99b..3c3e390 100644 --- a/services/quickActions.ts +++ b/services/quickActions.ts @@ -60,9 +60,7 @@ export const setupQuickActions = async () => { params: { amount: quickAmount } }, // 固定选项 - QUICK_ACTIONS.DRINK_WATER_100, - QUICK_ACTIONS.DRINK_WATER_200, - QUICK_ACTIONS.DRINK_WATER_250 + QUICK_ACTIONS.DRINK_WATER_100 ]; // 设置快捷动作 diff --git a/utils/health.ts b/utils/health.ts index 209783c..55f9833 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -183,12 +183,6 @@ async function fetchStepCount(date: Date): Promise { }); } -// 获取指定日期每小时步数数据 (已弃用,使用 fetchHourlyStepSamples 替代) -// 保留此函数以防后向兼容需求 -async function fetchHourlyStepCount(date: Date): Promise { - // 直接调用更准确的样本数据获取函数 - return fetchHourlyStepSamples(date); -} // 使用样本数据获取每小时步数 async function fetchHourlyStepSamples(date: Date): Promise { @@ -483,7 +477,7 @@ async function fetchActivitySummary(options: HealthDataOptions): Promise { AppleHealthKit.getActivitySummary( options, - (err: Object, results: HealthActivitySummary[]) => { + (err: string, results: HealthActivitySummary[]) => { if (err) { logError('ActivitySummary', err); return resolve(null); @@ -537,6 +531,44 @@ async function fetchHeartRate(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getHeartRateSamples(options, (err, res) => { + if (err) { + logError('最大心率', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('最大心率', '为空或格式错误'); + return resolve(null); + } + + // 从所有心率样本中找出最大值 + let maxHeartRate = 0; + let validSamplesCount = 0; + + res.forEach((sample: any) => { + if (sample && sample.value !== undefined) { + const heartRate = validateHeartRate(sample.value); + if (heartRate !== null) { + maxHeartRate = Math.max(maxHeartRate, heartRate); + validSamplesCount++; + } + } + }); + + if (validSamplesCount > 0 && maxHeartRate > 0) { + logSuccess('最大心率', { maxHeartRate, validSamplesCount }); + resolve(maxHeartRate); + } else { + logWarning('最大心率', '没有找到有效的样本数据'); + resolve(null); + } + }); + }); +} + // 默认健康数据 function getDefaultHealthData(): TodayHealthData { return { @@ -721,7 +753,7 @@ export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: st endDate: recordedAt ? new Date(recordedAt).toISOString() : new Date().toISOString(), }; - AppleHealthKit.saveWater(waterOptions, (error: Object, result) => { + AppleHealthKit.saveWater(waterOptions, (error: string, result) => { if (error) { console.error('添加饮水记录到 HealthKit 失败:', error); resolve(false); @@ -742,7 +774,7 @@ export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: st // 获取 HealthKit 中的饮水记录 export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): Promise { return new Promise((resolve) => { - AppleHealthKit.getWaterSamples(options, (error: Object, results: any[]) => { + AppleHealthKit.getWaterSamples(options, (error: string, results: any[]) => { if (error) { console.error('获取 HealthKit 饮水记录失败:', error); resolve([]); @@ -850,3 +882,4 @@ export async function fetchActivityRingsForDate(date: Date): Promise