diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 1d3e97b..a81bbe4 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -57,7 +57,7 @@ export default function TabLayout() { }; const { icon, title } = getIconAndTitle(); - const activeContentColor = colorTokens.onPrimary; + const activeContentColor = colorTokens.tabIconSelected; // 使用专门为Tab定义的选中颜色 const inactiveContentColor = colorTokens.tabIconDefault; return ( diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 9740c61..aa1f4cc 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -27,6 +27,7 @@ import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; +import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach'; import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession'; @@ -124,7 +125,8 @@ export default function CoachScreen() { const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); // 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色 - const theme = Colors.light; + const colorScheme = useColorScheme(); + const theme = Colors[colorScheme ?? 'light']; const botName = (params?.name || 'Seal').toString(); const [input, setInput] = useState(''); const [isSending, setIsSending] = useState(false); @@ -282,6 +284,16 @@ export default function CoachScreen() { { key: 'weight', label: '#记体重', action: () => insertWeightInputCard() }, { key: 'diet', label: '#记饮食', action: () => insertDietInputCard() }, { key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() }, + { + key: 'mood', + label: '#记心情', + action: () => { + if (Platform.OS === 'ios') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + router.push('/mood/calendar'); + } + }, ], [router, planDraft, checkin]); const scrollToEnd = useCallback(() => { @@ -1333,7 +1345,7 @@ export default function CoachScreen() { {/* 标题部分 */} - + 我的饮食方案 MY DIET PLAN @@ -1522,7 +1534,7 @@ export default function CoachScreen() { )} {isSelected && isPending && ( - + )} {isSelected && !isPending && ( @@ -1829,7 +1841,7 @@ export default function CoachScreen() { {/* 背景渐变 */} - + {userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`} @@ -1876,16 +1888,16 @@ export default function CoachScreen() { - + - + @@ -1948,8 +1960,18 @@ export default function CoachScreen() { contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }} > {chips.map((c) => ( - - {c.label} + + {c.label} ))} @@ -1990,18 +2012,18 @@ export default function CoachScreen() { )} - + - + 0) || (isSending || isStreaming)) ? 1 : 0.5 } ]} @@ -2144,8 +2166,8 @@ const styles = StyleSheet.create({ width: 60, height: 60, borderRadius: 30, - backgroundColor: '#0EA5E9', - opacity: 0.1, + backgroundColor: '#7a5af8', // 紫色主题 + opacity: 0.08, }, decorativeCircle2: { position: 'absolute', @@ -2154,8 +2176,8 @@ const styles = StyleSheet.create({ width: 40, height: 40, borderRadius: 20, - backgroundColor: '#0EA5E9', - opacity: 0.05, + backgroundColor: '#7a5af8', // 紫色主题 + opacity: 0.04, }, headerLeft: { flexDirection: 'row', @@ -2177,6 +2199,11 @@ const styles = StyleSheet.create({ borderRadius: 16, alignItems: 'center', justifyContent: 'center', + shadowColor: '#7a5af8', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, }, historyButton: { width: 32, @@ -2237,7 +2264,7 @@ const styles = StyleSheet.create({ borderRadius: 8, alignItems: 'center', justifyContent: 'center', - backgroundColor: `${Colors.light.accentGreen}99` // 60% opacity + backgroundColor: '#7a5af899' // 紫色主题 60% opacity }, dietOptionsContainer: { gap: 8, @@ -2249,13 +2276,13 @@ const styles = StyleSheet.create({ borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.9)', borderWidth: 1, - borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity + borderColor: '#7a5af84d', // 紫色主题 30% opacity }, dietOptionIconContainer: { width: 40, height: 40, borderRadius: 20, - backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity + backgroundColor: '#7a5af833', // 紫色主题 20% opacity alignItems: 'center', justifyContent: 'center', marginRight: 12, @@ -2304,7 +2331,7 @@ const styles = StyleSheet.create({ borderRadius: 10, alignItems: 'center', justifyContent: 'center', - backgroundColor: `${Colors.light.accentGreen}99`, // 60% opacity + backgroundColor: '#7a5af899', // 紫色主题 60% opacity alignSelf: 'flex-end', }, // markdown 基础样式承载容器的字体尺寸保持与气泡一致 @@ -2349,7 +2376,7 @@ const styles = StyleSheet.create({ borderRadius: 12, overflow: 'hidden', position: 'relative', - backgroundColor: 'rgba(0,0,0,0.06)' + backgroundColor: 'rgba(122,90,248,0.08)' // 使用紫色主题的浅色背景 }, imageThumb: { width: '100%', @@ -2404,7 +2431,7 @@ const styles = StyleSheet.create({ padding: 8, borderWidth: 1, borderRadius: 16, - backgroundColor: 'rgba(0,0,0,0.04)' + backgroundColor: 'rgba(122,90,248,0.04)' // 使用紫色主题的极浅色背景 }, mediaBtn: { width: 40, @@ -2617,17 +2644,17 @@ const styles = StyleSheet.create({ choiceButton: { backgroundColor: 'rgba(255,255,255,0.9)', borderWidth: 1, - borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity + borderColor: '#7a5af84d', // 紫色主题 30% opacity borderRadius: 12, padding: 12, }, choiceButtonRecommended: { - borderColor: `${Colors.light.accentGreen}99`, // 60% opacity - backgroundColor: `${Colors.light.accentGreen}1A`, // 10% opacity + borderColor: '#7a5af899', // 紫色主题 60% opacity + backgroundColor: '#7a5af81a', // 紫色主题 10% opacity }, choiceButtonSelected: { - borderColor: Colors.light.accentGreenDark, - backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity + borderColor: '#19b36e', // success[500] + backgroundColor: '#19b36e33', // 20% opacity borderWidth: 2, }, choiceButtonDisabled: { @@ -2647,10 +2674,10 @@ const styles = StyleSheet.create({ flex: 1, }, choiceLabelRecommended: { - color: Colors.light.accentGreenDark, + color: '#19b36e', // success[500] }, choiceLabelSelected: { - color: Colors.light.accentGreenDark, + color: '#19b36e', // success[500] fontWeight: '700', }, choiceLabelDisabled: { @@ -2662,7 +2689,7 @@ const styles = StyleSheet.create({ gap: 8, }, recommendedBadge: { - backgroundColor: `${Colors.light.accentGreen}CC`, // 80% opacity + backgroundColor: '#7a5af8cc', // 紫色主题 80% opacity borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2, @@ -2670,10 +2697,10 @@ const styles = StyleSheet.create({ recommendedText: { fontSize: 12, fontWeight: '700', - color: Colors.light.accentGreenDark, + color: '#19b36e', // success[500] }, selectedBadge: { - backgroundColor: Colors.light.accentGreenDark, + backgroundColor: '#19b36e', // success[500] borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2, @@ -2714,7 +2741,7 @@ const styles = StyleSheet.create({ padding: 16, gap: 16, borderWidth: 1, - borderColor: `${Colors.light.accentGreen}33`, // 20% opacity + borderColor: '#7a5af833', // 紫色主题 20% opacity }, dietPlanHeader: { gap: 4, @@ -2834,7 +2861,7 @@ const styles = StyleSheet.create({ caloriesValue: { fontSize: 18, fontWeight: '800', - color: Colors.light.accentGreenDark, + color: '#19b36e', // success[500] }, nutritionGrid: { flexDirection: 'row', @@ -2871,7 +2898,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', gap: 8, - backgroundColor: Colors.light.accentGreenDark, + backgroundColor: '#19b36e', // success[500] paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, @@ -2889,7 +2916,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, - backgroundColor: 'rgba(0,0,0,0.06)', + backgroundColor: 'rgba(122,90,248,0.08)', // 紫色主题浅色背景 }, usageIcon: { width: 16, @@ -2898,7 +2925,7 @@ const styles = StyleSheet.create({ usageText: { fontSize: 12, fontWeight: '600', - color: '#687076', + color: '#7a5af8', // 紫色主题文字颜色 }, }); diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index 32464d7..b63fbb0 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -9,7 +9,6 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto import { Ionicons } from '@expo/vector-icons'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; -import { LinearGradient } from 'expo-linear-gradient'; import React, { useMemo, useState } from 'react'; import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -31,7 +30,6 @@ export default function PersonalScreen() { // 颜色主题 const colors = Colors[colorScheme ?? 'light']; - const theme = (colorScheme ?? 'light') as 'light' | 'dark'; // 直接使用 Redux 中的用户信息,避免重复状态管理 const userProfile = useAppSelector((state) => state.user.profile); @@ -66,129 +64,100 @@ export default function PersonalScreen() { // 显示名称 const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME; - // 颜色令牌 - const colorTokens = colors; - - const UserInfoSection = () => ( - - - {/* 头像 */} - - - + // 用户信息头部 + const UserHeader = () => ( + + 个人信息 + + + + + + {displayName} + + pushIfAuthedElseLogin('/profile/edit')}> + 编辑 + - - {/* 用户信息 */} - - {displayName} - - - {/* 编辑按钮 */} - pushIfAuthedElseLogin('/profile/edit')}> - 编辑 - ); + // 数据统计部分 const StatsSection = () => ( - - - {formatHeight()} - 身高 - - - {formatWeight()} - 体重 - - - {formatAge()} - 年龄 + + 身体数据 + + + + {formatHeight()} + 身高 + + + {formatWeight()} + 体重 + + + {formatAge()} + 年龄 + + ); // 菜单项组件 const MenuSection = ({ title, items }: { title: string; items: any[] }) => ( - - {title} - {items.map((item, index) => ( - - - - + + {title} + + {items.map((item, index) => ( + + + + + + {item.title} - {item.title} - - {item.type === 'switch' ? ( - { - if (!isLoggedIn) { - pushIfAuthedElseLogin('/profile/notification-settings'); - return; - } - setNotificationEnabled(value); - }} - trackColor={{ false: '#E5E5E5', true: colors.primary }} - thumbColor="#FFFFFF" - style={styles.switch} - /> - ) : ( - - )} - - ))} + {item.type === 'switch' ? ( + { + if (!isLoggedIn) { + pushIfAuthedElseLogin('/profile/notification-settings'); + return; + } + setNotificationEnabled(value); + }} + trackColor={{ false: '#E5E5E5', true: '#9370DB' }} + thumbColor="#FFFFFF" + style={styles.switch} + /> + ) : ( + + )} + + ))} + ); - // 动态样式 - const dynamicStyles = StyleSheet.create({ - editButton: { - backgroundColor: colors.primary, - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 20, - }, - editButtonText: { - color: colors.onPrimary, - fontSize: 14, - fontWeight: '600', - }, - statValue: { - fontSize: 18, - fontWeight: 'bold', - color: colors.primary, - marginBottom: 4, - }, - floatingButton: { - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: colors.primary, - alignItems: 'center', - justifyContent: 'center', - shadowColor: colors.primary, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - }, - }); - // 菜单项配置 const menuSections = [ { @@ -197,25 +166,8 @@ export default function PersonalScreen() { { icon: 'flag-outline' as const, title: '目标管理', - onPress: () => pushIfAuthedElseLogin('/goals'), + onPress: () => pushIfAuthedElseLogin('/profile/goals'), }, - { - icon: 'telescope-outline' as const, - title: '目标管理演示', - onPress: () => pushIfAuthedElseLogin('/goal-demo'), - }, - // { - // icon: 'stats-chart-outline' as const, - // title: '训练进度', - // onPress: () => { - // // 训练进度页面暂未实现,先显示提示 - // if (isLoggedIn) { - // Alert.alert('提示', '训练进度功能正在开发中'); - // } else { - // pushIfAuthedElseLogin('/profile/training-progress'); - // } - // }, - // }, ], }, { @@ -265,12 +217,6 @@ export default function PersonalScreen() { return ( - - + {menuSections.map((section, index) => ( ))} - @@ -294,48 +239,53 @@ export default function PersonalScreen() { const styles = StyleSheet.create({ container: { flex: 1, - }, - gradientBackground: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, + backgroundColor: '#FAFAFA', }, safeArea: { flex: 1, }, scrollView: { flex: 1, - paddingHorizontal: 20, + paddingHorizontal: 16, + paddingTop: 16, + }, + // 部分容器 + sectionContainer: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#2C3E50', + marginBottom: 10, + paddingHorizontal: 4, + }, + // 卡片容器 + cardContainer: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + overflow: 'hidden', }, // 用户信息区域 - userInfoCard: { - borderRadius: 16, - marginBottom: 20, - shadowColor: 'rgba(135,206,235,0.3)', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 3, - borderWidth: 1, - borderColor: 'rgba(135,206,235,0.1)', - }, userInfoContainer: { flexDirection: 'row', alignItems: 'center', - padding: 20, + padding: 16, }, avatarContainer: { - marginRight: 15, + marginRight: 12, }, avatar: { - width: 80, - height: 80, - borderRadius: 40, - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', + width: 60, + height: 60, + borderRadius: 30, + borderWidth: 2, + borderColor: '#9370DB', }, userDetails: { flex: 1, @@ -343,86 +293,75 @@ const styles = StyleSheet.create({ userName: { fontSize: 18, fontWeight: 'bold', + color: '#2C3E50', marginBottom: 4, }, + userRole: { + fontSize: 14, + color: '#9370DB', + fontWeight: '500', + }, + editButton: { + backgroundColor: '#9370DB', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 16, + }, + editButtonText: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, + // 数据统计 statsContainer: { flexDirection: 'row', justifyContent: 'space-between', - borderRadius: 16, - padding: 20, - marginBottom: 20, - shadowColor: 'rgba(135,206,235,0.25)', - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.12, - shadowRadius: 6, - elevation: 3, - borderWidth: 1, - borderColor: 'rgba(135,206,235,0.08)', + padding: 16, }, statItem: { alignItems: 'center', flex: 1, }, - + statValue: { + fontSize: 18, + fontWeight: 'bold', + color: '#9370DB', + marginBottom: 4, + }, statLabel: { fontSize: 12, + color: '#6C757D', + fontWeight: '500', }, - menuSection: { - marginBottom: 20, - padding: 16, - borderRadius: 16, - shadowColor: 'rgba(135,206,235,0.2)', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - borderWidth: 1, - borderColor: 'rgba(135,206,235,0.06)', - }, - sectionTitle: { - fontSize: 20, - fontWeight: '800', - marginBottom: 12, - paddingHorizontal: 4, - }, + // 菜单项 menuItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 16, + paddingVertical: 14, paddingHorizontal: 16, - borderRadius: 12, - marginBottom: 8, + borderBottomWidth: 1, + borderBottomColor: '#F1F3F4', }, menuItemLeft: { flexDirection: 'row', alignItems: 'center', flex: 1, }, - menuIcon: { - width: 36, - height: 36, - borderRadius: 8, + iconContainer: { + width: 32, + height: 32, + borderRadius: 6, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, menuItemText: { - fontSize: 16, - flex: 1, - fontWeight: '600', + fontSize: 15, + color: '#2C3E50', + fontWeight: '500', }, switch: { transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }], }, - // 浮动按钮 - floatingButtonContainer: { - position: 'absolute', - bottom: 30, - left: 0, - right: 0, - alignItems: 'center', - pointerEvents: 'box-none', - }, - }); diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 385280c..57427f7 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -22,7 +22,6 @@ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; -import { router } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Animated, @@ -120,6 +119,9 @@ export default function ExploreScreen() { standHours: 0, standHoursGoal: 12 }); + // 血氧饱和度和心率数据 + const [oxygenSaturation, setOxygenSaturation] = useState(null); + const [heartRate, setHeartRate] = useState(null); const [isLoading, setIsLoading] = useState(false); // 用于触发动画重置的 token(当日期或数据变化时更新) @@ -225,6 +227,13 @@ export default function ExploreScreen() { // 更新HRV数据时间 setHrvUpdateTime(new Date()); + // 设置血氧饱和度和心率数据 + setOxygenSaturation(data.oxygenSaturation ?? null); + setHeartRate(data.heartRate ?? null); + + console.log('血氧饱和度数据:', data.oxygenSaturation); + console.log('心率数据:', data.heartRate); + setAnimToken((t) => t + 1); } else { console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current); @@ -233,6 +242,9 @@ export default function ExploreScreen() { } catch (error) { console.error('HealthKit流程出现异常:', error); + // 重置血氧饱和度和心率数据 + setOxygenSaturation(null); + setHeartRate(null); } finally { setIsLoading(false); } @@ -389,7 +401,7 @@ export default function ExploreScreen() { router.push('/mood/calendar')} + onPress={() => pushIfAuthedElseLogin('/mood/calendar')} isLoading={isMoodLoading} /> @@ -436,6 +448,7 @@ export default function ExploreScreen() { @@ -444,6 +457,7 @@ export default function ExploreScreen() { diff --git a/app/goal-demo.tsx b/app/goal-demo.tsx deleted file mode 100644 index 4350148..0000000 --- a/app/goal-demo.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Colors } from '@/constants/Colors'; -import { useColorScheme } from '@/hooks/useColorScheme'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { useRouter } from 'expo-router'; -import React from 'react'; -import { SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; - -export default function GoalDemoScreen() { - const router = useRouter(); - const theme = useColorScheme() ?? 'light'; - const colorTokens = Colors[theme]; - - return ( - - - - - - router.back()} - > - - - - - 目标管理演示 - - - - - - - - - - - 智能目标管理系统 - - - - 体验高保真的目标管理界面,包含待办事项卡片滑动、时间筛选器和可滚动时间轴。界面完全按照您的需求设计,支持: - - - - - - - 横向滑动的待办事项卡片(首屏1.5张) - - - - - - - 天/周/月时间筛选选择器 - - - - - - - 可滚动的时间轴和任务显示 - - - - - - - 支持同一时间多任务的左右上下滑动 - - - - - router.push('/goals')} - > - - 进入目标管理页面 - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - backgroundGradient: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - content: { - flex: 1, - padding: 20, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 30, - marginTop: 20, - }, - backButton: { - width: 40, - height: 40, - borderRadius: 20, - justifyContent: 'center', - alignItems: 'center', - marginRight: 16, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - title: { - fontSize: 24, - fontWeight: '800', - }, - demoContainer: { - flex: 1, - justifyContent: 'center', - }, - demoCard: { - borderRadius: 24, - padding: 32, - shadowColor: '#000', - shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.15, - shadowRadius: 20, - elevation: 10, - alignItems: 'center', - }, - iconContainer: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: '#E6F3FF', - justifyContent: 'center', - alignItems: 'center', - marginBottom: 24, - }, - demoTitle: { - fontSize: 24, - fontWeight: '700', - marginBottom: 16, - textAlign: 'center', - }, - demoDescription: { - fontSize: 16, - lineHeight: 24, - textAlign: 'center', - marginBottom: 24, - }, - featureList: { - alignSelf: 'stretch', - marginBottom: 32, - }, - featureItem: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - }, - featureText: { - fontSize: 14, - marginLeft: 8, - flex: 1, - }, - enterButton: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 32, - paddingVertical: 16, - borderRadius: 28, - shadowColor: '#87CEEB', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 6, - }, - enterButtonText: { - fontSize: 16, - fontWeight: '700', - marginRight: 8, - }, -}); diff --git a/app/mood/calendar.tsx b/app/mood/calendar.tsx index b4d7080..9f02f92 100644 --- a/app/mood/calendar.tsx +++ b/app/mood/calendar.tsx @@ -195,11 +195,15 @@ export default function MoodCalendarScreen() { return ( + {/* 装饰性圆圈 */} + + + + + {/* 装饰性圆圈 */} + + + + {/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */} + router.back()} + withSafeTop={false} + transparent={true} + variant="elevated" + /> + - router.back()} withSafeTop={false} transparent /> {/* 头像(带相机蒙层,点击从相册选择) */} @@ -561,7 +570,7 @@ const styles = StyleSheet.create({ backgroundColor: '#F1F5F9', }, modalBtnPrimary: { - backgroundColor: palette.primary, + backgroundColor: '#7a5af8', }, modalBtnText: { color: '#334155', @@ -571,15 +580,6 @@ const styles = StyleSheet.create({ color: '#0F172A', fontWeight: '700', }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 0, - marginBottom: 8, - }, - backButton: { padding: 4, width: 32 }, - headerTitle: { fontSize: 18, fontWeight: '700', color: '#192126' }, }); diff --git a/components/ActivityHeatMap.tsx b/components/ActivityHeatMap.tsx index e66da9b..95cfcf4 100644 --- a/components/ActivityHeatMap.tsx +++ b/components/ActivityHeatMap.tsx @@ -59,19 +59,24 @@ const ActivityHeatMap = () => { console.log('generateActivityData', generateActivityData); - // 根据活跃度计算颜色 + // 根据活跃度计算颜色 - 优化配色方案 const getActivityColor = (level: number): string => { - // 由于useColorScheme总是返回'light',我们直接使用浅色主题的颜色 switch (level) { case 0: - return colors.separator; // 使用主题分隔线色 + // 无活动:使用主题适配的背景色 + return colors.separator; case 1: + // 低活动:使用主题主色的浅色版本 + return 'rgba(122, 90, 248, 0.15)'; // 浅色模式下的浅紫色 case 2: - return 'rgba(135,206,235,0.4)'; + // 中等活动:使用主题主色的中等透明度 + return 'rgba(122, 90, 248, 0.35)'; // 浅色模式下的中等紫色 case 3: - return 'rgba(135,206,235,0.65)'; + // 高活动:使用主题主色的较高透明度 + return 'rgba(122, 90, 248, 0.55)'; // 浅色模式下的较深紫色 case 4: default: + // 最高活动:使用主题主色 return colors.primary; } }; @@ -143,14 +148,20 @@ const ActivityHeatMap = () => { }, [generateActivityData]); return ( - + {/* 标题和统计 */} 最近6个月活跃 {activityStats.activeDays} 天 - + {activityStats.activeRate}% @@ -238,13 +249,10 @@ const styles = StyleSheet.create({ borderRadius: 16, padding: 20, marginBottom: 20, - shadowColor: 'rgba(135,206,235,0.25)', shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.12, shadowRadius: 6, elevation: 3, - borderWidth: 1, - borderColor: 'rgba(135,206,235,0.08)', }, header: { marginBottom: 8, diff --git a/components/statistic/HealthDataCard.tsx b/components/statistic/HealthDataCard.tsx index d97aac4..c1ccf1d 100644 --- a/components/statistic/HealthDataCard.tsx +++ b/components/statistic/HealthDataCard.tsx @@ -24,6 +24,9 @@ const HealthDataCard: React.FC = ({ style={[styles.card, style]} > + + {icon} + {title} @@ -53,14 +56,18 @@ const styles = StyleSheet.create({ }, iconContainer: { marginRight: 16, + alignItems: 'center', + justifyContent: 'center', }, content: { flex: 1, + justifyContent: 'center', }, title: { fontSize: 14, color: '#666', marginBottom: 4, + fontWeight: '600', }, valueContainer: { flexDirection: 'row', @@ -68,14 +75,15 @@ const styles = StyleSheet.create({ }, value: { fontSize: 24, - fontWeight: 'bold', - color: '#333', + fontWeight: '800', + color: '#192126', }, unit: { fontSize: 14, color: '#666', marginLeft: 4, marginBottom: 2, + fontWeight: '500', }, }); diff --git a/components/statistic/HeartRateCard.tsx b/components/statistic/HeartRateCard.tsx index 424987f..5a77ba4 100644 --- a/components/statistic/HeartRateCard.tsx +++ b/components/statistic/HeartRateCard.tsx @@ -1,29 +1,19 @@ import { Ionicons } from '@expo/vector-icons'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { StyleSheet } from 'react-native'; -import HealthDataService from '../../services/healthData'; import HealthDataCard from './HealthDataCard'; interface HeartRateCardProps { resetToken: number; style?: object; + heartRate?: number | null; } const HeartRateCard: React.FC = ({ resetToken, - style + style, + heartRate }) => { - const [heartRate, setHeartRate] = useState(null); - - useEffect(() => { - const fetchHeartRate = async () => { - const data = await HealthDataService.getHeartRate(); - setHeartRate(data); - }; - - fetchHeartRate(); - }, [resetToken]); - const heartIcon = ( ); @@ -31,7 +21,7 @@ const HeartRateCard: React.FC = ({ return ( = ({ resetToken, - style + style, + oxygenSaturation }) => { - const [oxygenSaturation, setOxygenSaturation] = useState(null); - - useEffect(() => { - const fetchOxygenSaturation = async () => { - const data = await HealthDataService.getOxygenSaturation(); - setOxygenSaturation(data); - }; - - fetchOxygenSaturation(); - }, [resetToken]); - const oxygenIcon = ( ); @@ -31,7 +21,7 @@ const OxygenSaturationCard: React.FC = ({ return ( { + if (transparent) return 'transparent'; + + switch (variant) { + case 'elevated': + return theme.background; + case 'minimal': + return theme.background; + default: + return theme.card; + } + }; + + const getBackButtonStyle = () => { + const baseStyle = [styles.backButton]; + + switch (variant) { + case 'elevated': + return [...baseStyle, { + backgroundColor: `${theme.primary}15`, // 15% 透明度 + borderWidth: 1, + borderColor: `${theme.primary}20`, // 20% 透明度 + }]; + case 'minimal': + return [...baseStyle, { + backgroundColor: `${theme.neutral100}80`, // 80% 透明度 + }]; + default: + return [...baseStyle, { + backgroundColor: `${theme.accentGreen}20`, // 20% 透明度 + }]; + } + }; + + const getBackButtonIconColor = () => { + switch (variant) { + case 'elevated': + return theme.primary; + case 'minimal': + return theme.textSecondary; + default: + return theme.onPrimary; + } + }; + + const getBorderStyle = () => { + if (!showBottomBorder) return {}; + + return { + borderBottomWidth: 1, + borderBottomColor: variant === 'elevated' + ? theme.border + : `${theme.border}40`, // 40% 透明度 + }; + }; + return ( {onBack ? ( - - + + ) : ( )} - + {typeof title === 'string' ? ( - {title} + + {title} + ) : ( title )} @@ -68,7 +150,9 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, - paddingBottom: 10, + paddingBottom: 12, + minHeight: 44, + width: '100%', }, backButton: { width: 32, @@ -76,10 +160,25 @@ const styles = StyleSheet.create({ borderRadius: 16, alignItems: 'center', justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + titleContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 8, }, title: { - fontSize: 20, - fontWeight: '800', + fontSize: 18, + textAlign: 'center', + letterSpacing: -0.3, }, }); diff --git a/constants/Colors.ts b/constants/Colors.ts index 37b61d3..dd2e6df 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -1,138 +1,212 @@ /** - * 应用全局配色规范(来自设计规范图)。 - * 说明:保持原有导出结构不变,同时扩展更完整的语义令牌与原子调色板。 + * 应用全局配色规范(基于设计规范图)。 + * 包含完整的语义化颜色系统:灰色、紫色、成功色、错误色、警告色和基础色。 */ // 原子调色板(与设计图一致) export const palette = { - // Primary - primary: '#7A5AF8', - ink: '#FFFFFF', - - // Secondary / Neutrals - neutral100: '#888F92', - neutral200: '#5E6468', - neutral300: '#384046', - - // Accents - purple: '#A48AED', - red: '#ED4747', - orange: '#FCC46F', - blue: '#7A5AF8', // 更贴近logo背景的天空蓝 - blueSecondary: '#4682B4', // 钢蓝色,用于选中状态 - green: '#9ceb87', // 温暖的绿色,用于心情日历等 + // 灰色系统 - 中性色,UI设计的基础 + gray: { + 25: '#fcfcfd', + 50: '#ebecee', + 100: '#c0c4ca', + 200: '#a2a7b0', + 300: '#777f8c', + 400: '#5d6676', + 500: '#344054', + 600: '#2f3a4c', + 700: '#252d3c', + 800: '#1d232e', + 900: '#161b23', + }, + + // 紫色系统 - 品牌主色,用于交互元素 + purple: { + 25: '#fafaff', + 50: '#f4f3ff', + 100: '#ebe9fe', + 200: '#d9d6fe', + 300: '#bdb4fe', + 400: '#9b8afb', + 500: '#7a5af8', + 600: '#6938ef', + 700: '#5925dc', + 800: '#4a1fb8', + 900: '#3e1c96', + }, + + // 成功色系统 - 绿色,用于正面反馈 + success: { + 25: '#f6fef9', + 50: '#e8f7f1', + 100: '#b8e7d2', + 200: '#95dcbc', + 300: '#65cc9e', + 400: '#47c28b', + 500: '#19b36e', + 600: '#17a364', + 700: '#127f4e', + 800: '#0e623d', + 900: '#0b4b2e', + }, + + // 错误色系统 - 红色,用于错误状态和破坏性操作 + error: { + 25: '#fffbfa', + 50: '#feeeee', + 100: '#fdcaca', + 200: '#fcb1b1', + 300: '#fb8d8d', + 400: '#fa7777', + 500: '#f95555', + 600: '#e34d4d', + 700: '#b13c3c', + 800: '#892f2f', + 900: '#692424', + }, + + // 警告色系统 - 黄色/橙色,用于警告和确认 + warning: { + 25: '#fff7ec', + 50: '#fff7ec', + 100: '#ffe6c4', + 200: '#ffd9a8', + 300: '#ffc880', + 400: '#ffbd68', + 500: '#ffad42', + 600: '#e89d3c', + 700: '#b57b2f', + 800: '#8c5f24', + 900: '#6b491c', + }, + + // 基础色 + base: { + white: '#ffffff', + black: '#1d232e', + } } as const; -const primaryColor = palette.blue; // 应用主题色 +// 主色调定义 +const primaryColor = palette.purple[500]; // 紫色500作为主色 const tintColorLight = primaryColor; -const tintColorDark = '#FFFFFF'; +const tintColorDark = palette.base.white; export const Colors = { light: { - // 基础文本/背景(优化对比度) - text: '#1A2027', // 更深的文本色,提高可读性 - textSecondary: '#4A5568', // 温和的次要文本色 - textMuted: '#718096', // 柔和的静音文本色 - background: '#FFFFFF', - surface: '#FFFFFF', - card: 'rgba(255,255,255,0.95)', // 半透明卡片,与渐变背景融合 - - buttonBackground: palette.blue, + // 基础文本/背景 + text: palette.gray[900], // 最深灰色用于主要文本 + textSecondary: palette.gray[600], // 中等灰色用于次要文本 + textMuted: palette.gray[400], // 浅灰色用于静音文本 + background: palette.base.white, + surface: palette.base.white, + card: palette.gray[25], // 最浅灰色用于卡片背景 // 品牌与可交互主色 tint: tintColorLight, primary: primaryColor, - onPrimary: palette.ink, // 与主色搭配的前景色(按钮文字/图标) + onPrimary: palette.base.white, // 主色上的文字/图标颜色 // 中性色与辅助 - neutral100: palette.neutral100, - neutral200: palette.neutral200, - neutral300: palette.neutral300, + neutral100: palette.gray[100], + neutral200: palette.gray[200], + neutral300: palette.gray[300], // 状态/反馈色 - success: palette.primary, - warning: palette.orange, - danger: palette.red, - info: palette.blue, - accentPurple: palette.purple, - accentGreen: palette.green, // 温暖的绿色 - accentGreenDark: palette.blueSecondary, // 深绿色,用于文本和强调 + success: palette.success[500], + successLight: palette.success[100], + successDark: palette.success[700], + warning: palette.warning[500], + warningLight: palette.warning[100], + warningDark: palette.warning[700], + danger: palette.error[500], + dangerLight: palette.error[100], + dangerDark: palette.error[700], + info: palette.purple[500], + accentPurple: palette.purple[400], + accentGreen: palette.success[400], // 日期选择器主题色 - datePickerNormal: palette.blue, - datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态 + datePickerNormal: palette.purple[500], + datePickerSelected: palette.success[500], - // 结构色(优化后的蓝色主题) - border: 'rgba(135,206,235,0.2)', // 蓝色调边框 - separator: 'rgba(135,206,235,0.15)', // 更淡的分隔线 - icon: '#5A6C7D', // 蓝灰色图标 + // 结构色 + border: palette.gray[200], // 浅灰色边框 + separator: palette.gray[100], // 更浅的分隔线 + icon: palette.gray[500], // 中等灰色图标 - // Tab 相关(保持兼容) - tabIconDefault: '#687076', - tabIconSelected: palette.ink, // tab 激活时的文字/图标颜色(深色,在亮色背景上显示) - tabBarBackground: palette.ink, // tab 栏背景色 - tabBarActiveBackground: primaryColor, // tab 激活时的背景色 + // Tab 相关 + tabIconDefault: palette.gray[400], + tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见 + tabBarBackground: palette.base.white, + tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景 - // 页面氛围与装饰(优化后的蓝色配色方案) - pageBackgroundEmphasis: '#F0F8FF', // 淡蓝色背景强调 - heroSurfaceTint: 'rgba(135,206,235,0.12)', // 更柔和的蓝色调表面色彩 - ornamentPrimary: 'rgba(135,206,235,0.15)', // 与主色调和的装饰色 - ornamentAccent: 'rgba(70,130,180,0.12)', // 钢蓝色装饰,增加层次 + // 页面氛围与装饰 + pageBackgroundEmphasis: palette.gray[25], + heroSurfaceTint: palette.purple[25], + ornamentPrimary: palette.purple[100], + ornamentAccent: palette.success[100], - // 优化的背景渐变色(更柔和的蓝色过渡) - backgroundGradientStart: '#E6F3FF', // 更柔和的浅蓝色起始 - backgroundGradientEnd: '#FAFCFF', // 带有微蓝调的白色结束 + // 背景渐变色 + backgroundGradientStart: palette.gray[25], + backgroundGradientEnd: palette.base.white, }, dark: { // 基础文本/背景 - text: '#ECEDEE', - textSecondary: palette.neutral100, - textMuted: '#9BA1A6', - background: '#151718', - surface: '#1A1D1E', - card: '#1A1D1E', + text: palette.gray[25], // 最浅灰色用于主要文本 + textSecondary: palette.gray[100], // 浅灰色用于次要文本 + textMuted: palette.gray[300], // 中等灰色用于静音文本 + background: palette.gray[900], + surface: palette.gray[800], + card: palette.gray[800], // 品牌与可交互主色 tint: tintColorDark, primary: primaryColor, - onPrimary: palette.ink, + onPrimary: palette.base.white, // 中性色与辅助 - neutral100: palette.neutral100, - neutral200: palette.neutral200, - neutral300: palette.neutral300, + neutral100: palette.gray[100], + neutral200: palette.gray[200], + neutral300: palette.gray[300], // 状态/反馈色 - success: palette.primary, - warning: palette.orange, - danger: palette.red, - info: palette.blue, - accentPurple: palette.purple, - accentGreenDark: '#2D5016', // 深绿色,用于文本和强调 + success: palette.success[400], // 深色模式下使用较亮的成功色 + successLight: palette.success[200], + successDark: palette.success[600], + warning: palette.warning[400], + warningLight: palette.warning[200], + warningDark: palette.warning[600], + danger: palette.error[400], + dangerLight: palette.error[200], + dangerDark: palette.error[600], + info: palette.purple[400], + accentPurple: palette.purple[300], + accentGreen: palette.success[300], // 日期选择器主题色 - datePickerNormal: palette.blue, - datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态 + datePickerNormal: palette.purple[400], + datePickerSelected: palette.success[400], // 结构色 - border: '#2A2F32', - separator: '#2A2F32', - icon: '#9BA1A6', + border: palette.gray[700], + separator: palette.gray[700], + icon: palette.gray[300], - // Tab 相关(保持兼容) - tabIconDefault: '#9BA1A6', - tabIconSelected: palette.ink, // 在亮色背景上使用深色文字 - tabBarBackground: palette.ink, - tabBarActiveBackground: primaryColor, + // Tab 相关 + tabIconDefault: palette.gray[400], + tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见 + tabBarBackground: palette.gray[800], + tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景,在深色模式下更突出 - // 页面氛围与装饰(新) - pageBackgroundEmphasis: '#151718', - heroSurfaceTint: 'rgba(187,242,70,0.12)', - ornamentPrimary: 'rgba(187,242,70,0.18)', - ornamentAccent: 'rgba(164,138,237,0.14)', + // 页面氛围与装饰 + pageBackgroundEmphasis: palette.gray[800], + heroSurfaceTint: palette.purple[900], + ornamentPrimary: palette.purple[800], + ornamentAccent: palette.success[800], - // 统一背景渐变色(深色模式) - backgroundGradientStart: '#0A0B0C', // 深黑色起始 - backgroundGradientEnd: '#151718', // 背景色结束 + // 背景渐变色 + backgroundGradientStart: palette.gray[900], + backgroundGradientEnd: palette.gray[800], }, } as const; diff --git a/docs/headerbar-design-system.md b/docs/headerbar-design-system.md new file mode 100644 index 0000000..51db480 --- /dev/null +++ b/docs/headerbar-design-system.md @@ -0,0 +1,201 @@ +# HeaderBar 组件设计系统 + +## 概述 + +HeaderBar 组件已经与 `Colors.ts` 中的设计系统完全集成,提供了多种变体和优美的 UI 效果。 + +## 设计理念 + +### 颜色系统集成 +- 完全基于 `Colors.ts` 中定义的语义化颜色系统 +- 支持浅色和深色主题自动切换 +- 使用透明度创建层次感和视觉深度 + +### 三种变体设计 + +#### 1. Default 变体 (默认) +- **背景**: 透明或卡片背景 +- **返回按钮**: 绿色强调色背景 (20% 透明度) +- **图标颜色**: 白色 (确保在绿色背景上清晰可见) +- **适用场景**: 标准页面导航 + +#### 2. Elevated 变体 (提升) +- **背景**: 卡片背景 +- **返回按钮**: 主色背景 (15% 透明度) + 边框 (20% 透明度) +- **图标颜色**: 主色 +- **边框**: 实线边框 +- **适用场景**: 重要页面、模态框、需要突出显示的导航 + +#### 3. Minimal 变体 (简约) +- **背景**: 页面背景色 +- **返回按钮**: 中性色背景 (80% 透明度) +- **图标颜色**: 次要文本色 +- **适用场景**: 内容页面、列表页面、需要最小化视觉干扰的场景 + +## 颜色搭配详解 + +### 背景色系统 +```typescript +// 透明背景 +transparent: 'transparent' + +// 卡片背景 (浅色主题) +card: palette.gray[25] // #fcfcfd + +// 页面背景 (浅色主题) +background: palette.base.white // #ffffff +``` + +### 按钮背景色 +```typescript +// Default 变体 +backgroundColor: `${theme.accentGreen}20` // 绿色 20% 透明度 + +// Elevated 变体 +backgroundColor: `${theme.primary}15` // 主色 15% 透明度 +borderColor: `${theme.primary}20` // 主色 20% 透明度 + +// Minimal 变体 +backgroundColor: `${theme.neutral100}80` // 中性色 80% 透明度 +``` + +### 文本颜色 +```typescript +// 主标题 +color: theme.text // 主要文本色 + +// 次要文本 +color: theme.textSecondary // 次要文本色 + +// 按钮图标 +color: theme.onPrimary // 主色上的文字/图标颜色 +``` + +## 使用示例 + +### 基础用法 +```tsx +// 默认透明背景 + navigation.goBack()} +/> + +// 卡片背景 + navigation.goBack()} + transparent={false} + showBottomBorder={true} +/> +``` + +### 变体使用 +```tsx +// Elevated 变体 - 重要页面 + navigation.goBack()} + variant="elevated" + showBottomBorder={true} +/> + +// Minimal 变体 - 内容页面 + navigation.goBack()} + variant="minimal" +/> +``` + +### 自定义右侧内容 +```tsx + navigation.goBack()} + right={ + + 保存 + + } + variant="elevated" +/> +``` + +### 自定义标题 +```tsx + + + 主标题 + + + 副标题 + + + } + onBack={() => navigation.goBack()} + variant="elevated" +/> +``` + +## 视觉层次 + +### 阴影效果 +- 返回按钮添加了微妙的阴影效果 +- 增强按钮的立体感和可点击性 +- 在浅色和深色主题下都有良好的表现 + +### 间距系统 +- 使用 8px 的基础间距单位 +- 标题容器有适当的内边距确保居中 +- 按钮有足够的触摸区域 (32x32) + +### 字体系统 +- 标题字体大小: 18px +- 字重根据变体调整 (700-800) +- 字母间距优化 (-0.3) 提升可读性 + +## 主题适配 + +### 浅色主题 +- 使用 `palette.gray[900]` 作为主要文本色 +- 使用 `palette.gray[25]` 作为卡片背景 +- 绿色强调色用于默认按钮 + +### 深色主题 +- 使用 `palette.gray[25]` 作为主要文本色 +- 使用 `palette.gray[800]` 作为卡片背景 +- 自动调整透明度和对比度 + +## 最佳实践 + +1. **选择合适的变体**: 根据页面重要性和视觉需求选择变体 +2. **保持一致性**: 在同一应用中保持 HeaderBar 风格的一致性 +3. **考虑可访问性**: 确保颜色对比度符合可访问性标准 +4. **响应式设计**: 在不同屏幕尺寸下测试显示效果 +5. **性能优化**: 避免在 HeaderBar 中放置复杂的交互元素 + +## 技术实现 + +### 颜色计算 +使用模板字符串和透明度后缀来创建半透明颜色: +```typescript +`${theme.primary}15` // 15% 透明度 +`${theme.accentGreen}20` // 20% 透明度 +``` + +### 动态样式 +根据变体和主题动态计算样式,确保视觉一致性: +```typescript +const getBackButtonStyle = () => { + switch (variant) { + case 'elevated': + return { backgroundColor: `${theme.primary}15` }; + // ... + } +}; +``` + +这种设计确保了 HeaderBar 组件与整个应用的设计系统完美融合,提供了优美且一致的用户体验。 diff --git a/utils/health.ts b/utils/health.ts index 357d974..cf0229a 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -11,6 +11,8 @@ const PERMISSIONS: HealthKitPermissions = { AppleHealthKit.Constants.Permissions.SleepAnalysis, AppleHealthKit.Constants.Permissions.HeartRateVariability, AppleHealthKit.Constants.Permissions.ActivitySummary, + AppleHealthKit.Constants.Permissions.OxygenSaturation, + AppleHealthKit.Constants.Permissions.HeartRate, ], write: [ // 支持体重写入 @@ -32,6 +34,9 @@ export type TodayHealthData = { exerciseMinutesGoal: number; standHours: number; standHoursGoal: number; + // 新增血氧饱和度和心率数据 + oxygenSaturation: number | null; + heartRate: number | null; }; export async function ensureHealthPermissions(): Promise { @@ -74,8 +79,8 @@ export async function fetchHealthDataForDate(date: Date): Promise((resolve) => { AppleHealthKit.getStepCount({ @@ -198,10 +203,68 @@ export async function fetchHealthDataForDate(date: Date): Promise((resolve) => { + AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { + if (err) { + console.error('获取血氧饱和度失败:', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('血氧饱和度数据为空或格式错误'); + return resolve(null); + } + console.log('血氧饱和度数据:', res); + // 获取最新的血氧饱和度值 + const latestOxygen = res[res.length - 1]; + if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { + // 血氧饱和度通常在0-100之间,验证数据有效性 + const value = Number(latestOxygen.value); + if (value >= 0 && value <= 100) { + resolve(Number(value.toFixed(1))); + } else { + console.warn('血氧饱和度数据异常:', value); + resolve(null); + } + } else { + resolve(null); + } + }); + }), + + // 获取心率数据 + new Promise((resolve) => { + AppleHealthKit.getHeartRateSamples(options, (err, res) => { + if (err) { + console.error('获取心率失败:', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn('心率数据为空或格式错误'); + return resolve(null); + } + console.log('心率数据:', res); + // 获取最新的心率值 + const latestHeartRate = res[res.length - 1]; + if (latestHeartRate && latestHeartRate.value !== undefined && latestHeartRate.value !== null) { + // 心率通常在30-200之间,验证数据有效性 + const value = Number(latestHeartRate.value); + if (value >= 30 && value <= 200) { + resolve(Math.round(value)); + } else { + console.warn('心率数据异常:', value); + resolve(null); + } + } else { + resolve(null); + } + }); }) ]); - console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary }); + console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate }); return { steps, @@ -215,7 +278,10 @@ export async function fetchHealthDataForDate(date: Date): Promise