feat: 移除目标管理演示页面并优化相关组件

- 删除目标管理演示页面的代码,简化项目结构
- 更新底部导航,移除目标管理演示页面的路由
- 调整相关组件的样式和逻辑,确保界面一致性
- 优化颜色常量的使用,提升视觉效果
This commit is contained in:
2025-08-22 21:24:31 +08:00
parent 9e719a9eda
commit c12329bc96
16 changed files with 1130 additions and 759 deletions

View File

@@ -57,7 +57,7 @@ export default function TabLayout() {
}; };
const { icon, title } = getIconAndTitle(); const { icon, title } = getIconAndTitle();
const activeContentColor = colorTokens.onPrimary; const activeContentColor = colorTokens.tabIconSelected; // 使用专门为Tab定义的选中颜色
const inactiveContentColor = colorTokens.tabIconDefault; const inactiveContentColor = colorTokens.tabIconDefault;
return ( return (

View File

@@ -27,6 +27,7 @@ import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar'; import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload'; import { useCosUpload } from '@/hooks/useCosUpload';
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach'; import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession'; import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
@@ -124,7 +125,8 @@ export default function CoachScreen() {
const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
// 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色 // 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色
const theme = Colors.light; const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const botName = (params?.name || 'Seal').toString(); const botName = (params?.name || 'Seal').toString();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
@@ -282,6 +284,16 @@ export default function CoachScreen() {
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() }, { key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() }, { key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() }, { 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]); ], [router, planDraft, checkin]);
const scrollToEnd = useCallback(() => { const scrollToEnd = useCallback(() => {
@@ -1333,7 +1345,7 @@ export default function CoachScreen() {
{/* 标题部分 */} {/* 标题部分 */}
<View style={styles.dietPlanHeader}> <View style={styles.dietPlanHeader}>
<View style={styles.dietPlanTitleContainer}> <View style={styles.dietPlanTitleContainer}>
<Ionicons name="restaurant-outline" size={20} color={Colors.light.accentGreenDark} /> <Ionicons name="restaurant-outline" size={20} color={theme.success} />
<Text style={styles.dietPlanTitle}></Text> <Text style={styles.dietPlanTitle}></Text>
</View> </View>
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text> <Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
@@ -1522,7 +1534,7 @@ export default function CoachScreen() {
</View> </View>
)} )}
{isSelected && isPending && ( {isSelected && isPending && (
<ActivityIndicator size="small" color={Colors.light.accentGreenDark} /> <ActivityIndicator size="small" color={theme.success} />
)} )}
{isSelected && !isPending && ( {isSelected && !isPending && (
<View style={styles.selectedBadge}> <View style={styles.selectedBadge}>
@@ -1829,7 +1841,7 @@ export default function CoachScreen() {
<View style={styles.screen}> <View style={styles.screen}>
{/* 背景渐变 */} {/* 背景渐变 */}
<LinearGradient <LinearGradient
colors={['#F0F9FF', '#E0F2FE']} colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
style={styles.gradientBackground} style={styles.gradientBackground}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
@@ -1866,7 +1878,7 @@ export default function CoachScreen() {
source={require('@/assets/images/icons/iconFlash.png')} source={require('@/assets/images/icons/iconFlash.png')}
style={styles.usageIcon} style={styles.usageIcon}
/> />
<Text style={[styles.usageText, { color: theme.text }]}> <Text style={styles.usageText}>
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`} {userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@@ -1876,16 +1888,16 @@ export default function CoachScreen() {
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={startNewConversation} onPress={startNewConversation}
style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity style={[styles.headerActionButton, { backgroundColor: `${theme.primary}20` }]} // 20% opacity
> >
<Ionicons name="add-outline" size={18} color={theme.onPrimary} /> <Ionicons name="add-outline" size={18} color={theme.primary} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={openHistory} onPress={openHistory}
style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity style={[styles.headerActionButton, { backgroundColor: `${theme.primary}20` }]} // 20% opacity
> >
<Ionicons name="time-outline" size={18} color={theme.onPrimary} /> <Ionicons name="time-outline" size={18} color={theme.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@@ -1948,8 +1960,18 @@ export default function CoachScreen() {
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }} contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
> >
{chips.map((c) => ( {chips.map((c) => (
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}1F` }]} onPress={c.action}> <TouchableOpacity
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text> key={c.key}
style={[
styles.chip,
{
borderColor: c.key === 'mood' ? `${theme.success}40` : `${theme.primary}40`,
backgroundColor: c.key === 'mood' ? `${theme.success}15` : `${theme.primary}15`
}
]}
onPress={c.action}
>
<Text style={[styles.chipText, { color: c.key === 'mood' ? theme.success : theme.text }]}>{c.label}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
@@ -1990,18 +2012,18 @@ export default function CoachScreen() {
</ScrollView> </ScrollView>
)} )}
<View style={[styles.inputRow, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}14` }]}> <View style={[styles.inputRow, { borderColor: `${theme.primary}30`, backgroundColor: `${theme.primary}08` }]}>
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={pickImages} onPress={pickImages}
style={[styles.mediaBtn, { backgroundColor: `${Colors.light.accentGreen}28` }]} style={[styles.mediaBtn, { backgroundColor: `${theme.primary}20` }]}
> >
<Ionicons name="image-outline" size={18} color={'#192126'} /> <Ionicons name="image-outline" size={18} color={theme.text} />
</TouchableOpacity> </TouchableOpacity>
<TextInput <TextInput
placeholder="问我任何健康相关的问题,如营养、健身、生活管理等..." placeholder="问我任何健康相关的问题,如营养、健身、生活管理等..."
placeholderTextColor={theme.textMuted} placeholderTextColor={theme.textMuted}
style={[styles.input, { color: '#192126' }]} style={[styles.input, { color: theme.text }]}
value={input} value={input}
onChangeText={setInput} onChangeText={setInput}
multiline multiline
@@ -2021,7 +2043,7 @@ export default function CoachScreen() {
style={[ style={[
styles.sendBtn, styles.sendBtn,
{ {
backgroundColor: (isSending || isStreaming) ? '#FF4444' : theme.primary, backgroundColor: (isSending || isStreaming) ? theme.danger : theme.primary,
opacity: ((input.trim() || selectedImages.length > 0) || (isSending || isStreaming)) ? 1 : 0.5 opacity: ((input.trim() || selectedImages.length > 0) || (isSending || isStreaming)) ? 1 : 0.5
} }
]} ]}
@@ -2144,8 +2166,8 @@ const styles = StyleSheet.create({
width: 60, width: 60,
height: 60, height: 60,
borderRadius: 30, borderRadius: 30,
backgroundColor: '#0EA5E9', backgroundColor: '#7a5af8', // 紫色主题
opacity: 0.1, opacity: 0.08,
}, },
decorativeCircle2: { decorativeCircle2: {
position: 'absolute', position: 'absolute',
@@ -2154,8 +2176,8 @@ const styles = StyleSheet.create({
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 20, borderRadius: 20,
backgroundColor: '#0EA5E9', backgroundColor: '#7a5af8', // 紫色主题
opacity: 0.05, opacity: 0.04,
}, },
headerLeft: { headerLeft: {
flexDirection: 'row', flexDirection: 'row',
@@ -2177,6 +2199,11 @@ const styles = StyleSheet.create({
borderRadius: 16, borderRadius: 16,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
}, },
historyButton: { historyButton: {
width: 32, width: 32,
@@ -2237,7 +2264,7 @@ const styles = StyleSheet.create({
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: `${Colors.light.accentGreen}99` // 60% opacity backgroundColor: '#7a5af899' // 紫色主题 60% opacity
}, },
dietOptionsContainer: { dietOptionsContainer: {
gap: 8, gap: 8,
@@ -2249,13 +2276,13 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
backgroundColor: 'rgba(255,255,255,0.9)', backgroundColor: 'rgba(255,255,255,0.9)',
borderWidth: 1, borderWidth: 1,
borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity borderColor: '#7a5af84d', // 紫色主题 30% opacity
}, },
dietOptionIconContainer: { dietOptionIconContainer: {
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 20, borderRadius: 20,
backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity backgroundColor: '#7a5af833', // 紫色主题 20% opacity
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 12, marginRight: 12,
@@ -2304,7 +2331,7 @@ const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: `${Colors.light.accentGreen}99`, // 60% opacity backgroundColor: '#7a5af899', // 紫色主题 60% opacity
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
// markdown 基础样式承载容器的字体尺寸保持与气泡一致 // markdown 基础样式承载容器的字体尺寸保持与气泡一致
@@ -2349,7 +2376,7 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
backgroundColor: 'rgba(0,0,0,0.06)' backgroundColor: 'rgba(122,90,248,0.08)' // 使用紫色主题的浅色背景
}, },
imageThumb: { imageThumb: {
width: '100%', width: '100%',
@@ -2404,7 +2431,7 @@ const styles = StyleSheet.create({
padding: 8, padding: 8,
borderWidth: 1, borderWidth: 1,
borderRadius: 16, borderRadius: 16,
backgroundColor: 'rgba(0,0,0,0.04)' backgroundColor: 'rgba(122,90,248,0.04)' // 使用紫色主题的极浅色背景
}, },
mediaBtn: { mediaBtn: {
width: 40, width: 40,
@@ -2617,17 +2644,17 @@ const styles = StyleSheet.create({
choiceButton: { choiceButton: {
backgroundColor: 'rgba(255,255,255,0.9)', backgroundColor: 'rgba(255,255,255,0.9)',
borderWidth: 1, borderWidth: 1,
borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity borderColor: '#7a5af84d', // 紫色主题 30% opacity
borderRadius: 12, borderRadius: 12,
padding: 12, padding: 12,
}, },
choiceButtonRecommended: { choiceButtonRecommended: {
borderColor: `${Colors.light.accentGreen}99`, // 60% opacity borderColor: '#7a5af899', // 紫色主题 60% opacity
backgroundColor: `${Colors.light.accentGreen}1A`, // 10% opacity backgroundColor: '#7a5af81a', // 紫色主题 10% opacity
}, },
choiceButtonSelected: { choiceButtonSelected: {
borderColor: Colors.light.accentGreenDark, borderColor: '#19b36e', // success[500]
backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity backgroundColor: '#19b36e33', // 20% opacity
borderWidth: 2, borderWidth: 2,
}, },
choiceButtonDisabled: { choiceButtonDisabled: {
@@ -2647,10 +2674,10 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
choiceLabelRecommended: { choiceLabelRecommended: {
color: Colors.light.accentGreenDark, color: '#19b36e', // success[500]
}, },
choiceLabelSelected: { choiceLabelSelected: {
color: Colors.light.accentGreenDark, color: '#19b36e', // success[500]
fontWeight: '700', fontWeight: '700',
}, },
choiceLabelDisabled: { choiceLabelDisabled: {
@@ -2662,7 +2689,7 @@ const styles = StyleSheet.create({
gap: 8, gap: 8,
}, },
recommendedBadge: { recommendedBadge: {
backgroundColor: `${Colors.light.accentGreen}CC`, // 80% opacity backgroundColor: '#7a5af8cc', // 紫色主题 80% opacity
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 2,
@@ -2670,10 +2697,10 @@ const styles = StyleSheet.create({
recommendedText: { recommendedText: {
fontSize: 12, fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: Colors.light.accentGreenDark, color: '#19b36e', // success[500]
}, },
selectedBadge: { selectedBadge: {
backgroundColor: Colors.light.accentGreenDark, backgroundColor: '#19b36e', // success[500]
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 2,
@@ -2714,7 +2741,7 @@ const styles = StyleSheet.create({
padding: 16, padding: 16,
gap: 16, gap: 16,
borderWidth: 1, borderWidth: 1,
borderColor: `${Colors.light.accentGreen}33`, // 20% opacity borderColor: '#7a5af833', // 紫色主题 20% opacity
}, },
dietPlanHeader: { dietPlanHeader: {
gap: 4, gap: 4,
@@ -2834,7 +2861,7 @@ const styles = StyleSheet.create({
caloriesValue: { caloriesValue: {
fontSize: 18, fontSize: 18,
fontWeight: '800', fontWeight: '800',
color: Colors.light.accentGreenDark, color: '#19b36e', // success[500]
}, },
nutritionGrid: { nutritionGrid: {
flexDirection: 'row', flexDirection: 'row',
@@ -2871,7 +2898,7 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 8,
backgroundColor: Colors.light.accentGreenDark, backgroundColor: '#19b36e', // success[500]
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 16, paddingHorizontal: 16,
borderRadius: 12, borderRadius: 12,
@@ -2889,7 +2916,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 12, borderRadius: 12,
backgroundColor: 'rgba(0,0,0,0.06)', backgroundColor: 'rgba(122,90,248,0.08)', // 紫色主题浅色背景
}, },
usageIcon: { usageIcon: {
width: 16, width: 16,
@@ -2898,7 +2925,7 @@ const styles = StyleSheet.create({
usageText: { usageText: {
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
color: '#687076', color: '#7a5af8', // 紫色主题文字颜色
}, },
}); });

View File

@@ -9,7 +9,6 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -31,7 +30,6 @@ export default function PersonalScreen() {
// 颜色主题 // 颜色主题
const colors = Colors[colorScheme ?? 'light']; const colors = Colors[colorScheme ?? 'light'];
const theme = (colorScheme ?? 'light') as 'light' | 'dark';
// 直接使用 Redux 中的用户信息,避免重复状态管理 // 直接使用 Redux 中的用户信息,避免重复状态管理
const userProfile = useAppSelector((state) => state.user.profile); const userProfile = useAppSelector((state) => state.user.profile);
@@ -66,72 +64,76 @@ export default function PersonalScreen() {
// 显示名称 // 显示名称
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME; const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
// 颜色令牌 // 用户信息头部
const colorTokens = colors; const UserHeader = () => (
<View style={styles.sectionContainer}>
const UserInfoSection = () => ( <Text style={styles.sectionTitle}></Text>
<View style={[styles.userInfoCard, { backgroundColor: colorTokens.card }]}> <View style={styles.cardContainer}>
<View style={styles.userInfoContainer}> <View style={styles.userInfoContainer}>
{/* 头像 */}
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<View style={[styles.avatar, { backgroundColor: colorTokens.ornamentAccent }]}> <Image
<Image source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }} style={{ width: '100%', height: '100%' }} /> source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }}
style={styles.avatar}
/>
</View> </View>
</View>
{/* 用户信息 */}
<View style={styles.userDetails}> <View style={styles.userDetails}>
<Text style={[styles.userName, { color: colorTokens.text }]}>{displayName}</Text> <Text style={styles.userName}>{displayName}</Text>
</View> </View>
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
{/* 编辑按钮 */} <Text style={styles.editButtonText}></Text>
<TouchableOpacity style={dynamicStyles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={dynamicStyles.editButtonText}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</View>
); );
// 数据统计部分
const StatsSection = () => ( const StatsSection = () => (
<View style={[styles.statsContainer, { backgroundColor: colorTokens.card }]}> <View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.cardContainer}>
<View style={styles.statsContainer}>
<View style={styles.statItem}> <View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text> <Text style={styles.statValue}>{formatHeight()}</Text>
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}></Text> <Text style={styles.statLabel}></Text>
</View> </View>
<View style={styles.statItem}> <View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text> <Text style={styles.statValue}>{formatWeight()}</Text>
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}></Text> <Text style={styles.statLabel}></Text>
</View> </View>
<View style={styles.statItem}> <View style={styles.statItem}>
<Text style={dynamicStyles.statValue}>{formatAge()}</Text> <Text style={styles.statValue}>{formatAge()}</Text>
<Text style={[styles.statLabel, { color: colorTokens.textMuted }]}></Text> <Text style={styles.statLabel}></Text>
</View>
</View>
</View> </View>
</View> </View>
); );
// 菜单项组件 // 菜单项组件
const MenuSection = ({ title, items }: { title: string; items: any[] }) => ( const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
<View style={[styles.menuSection, { backgroundColor: colorTokens.card }]}> <View style={styles.sectionContainer}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}>{title}</Text> <Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.cardContainer}>
{items.map((item, index) => ( {items.map((item, index) => (
<TouchableOpacity <TouchableOpacity
key={index} key={index}
style={styles.menuItem} style={[styles.menuItem, index === items.length - 1 && { borderBottomWidth: 0 }]}
onPress={item.type === 'switch' ? undefined : item.onPress} onPress={item.type === 'switch' ? undefined : item.onPress}
disabled={item.type === 'switch'} disabled={item.type === 'switch'}
> >
<View style={styles.menuItemLeft}> <View style={styles.menuItemLeft}>
<View style={[ <View style={[
styles.menuIcon, styles.iconContainer,
{ backgroundColor: item.isDanger ? 'rgba(255,68,68,0.12)' : 'rgba(135,206,235,0.15)' } { backgroundColor: item.isDanger ? 'rgba(255,68,68,0.1)' : 'rgba(147, 112, 219, 0.1)' }
]}> ]}>
<Ionicons <Ionicons
name={item.icon} name={item.icon}
size={20} size={20}
color={item.isDanger ? colors.danger : '#4682B4'} color={item.isDanger ? '#FF4444' : '#9370DB'}
/> />
</View> </View>
<Text style={[styles.menuItemText, { color: colorTokens.text }]}>{item.title}</Text> <Text style={styles.menuItemText}>{item.title}</Text>
</View> </View>
{item.type === 'switch' ? ( {item.type === 'switch' ? (
<Switch <Switch
@@ -143,52 +145,19 @@ export default function PersonalScreen() {
} }
setNotificationEnabled(value); setNotificationEnabled(value);
}} }}
trackColor={{ false: '#E5E5E5', true: colors.primary }} trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
thumbColor="#FFFFFF" thumbColor="#FFFFFF"
style={styles.switch} style={styles.switch}
/> />
) : ( ) : (
<Ionicons name="chevron-forward" size={20} color={colorTokens.icon} /> <Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
)} )}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
</View>
); );
// 动态样式
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 = [ const menuSections = [
{ {
@@ -197,25 +166,8 @@ export default function PersonalScreen() {
{ {
icon: 'flag-outline' as const, icon: 'flag-outline' as const,
title: '目标管理', 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 ( return (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient
colors={[colors.backgroundGradientStart, colors.backgroundGradientEnd]}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent /> <StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
<SafeAreaView style={styles.safeArea}> <SafeAreaView style={styles.safeArea}>
<ScrollView <ScrollView
@@ -278,13 +224,12 @@ export default function PersonalScreen() {
contentContainerStyle={{ paddingBottom: bottomPadding }} contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<UserInfoSection /> <UserHeader />
<StatsSection /> <StatsSection />
<ActivityHeatMap /> <ActivityHeatMap />
{menuSections.map((section, index) => ( {menuSections.map((section, index) => (
<MenuSection key={index} title={section.title} items={section.items} /> <MenuSection key={index} title={section.title} items={section.items} />
))} ))}
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
</View> </View>
@@ -294,48 +239,53 @@ export default function PersonalScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, backgroundColor: '#FAFAFA',
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
}, },
safeArea: { safeArea: {
flex: 1, flex: 1,
}, },
scrollView: { scrollView: {
flex: 1, 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: { userInfoContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 20, padding: 16,
}, },
avatarContainer: { avatarContainer: {
marginRight: 15, marginRight: 12,
}, },
avatar: { avatar: {
width: 80, width: 60,
height: 80, height: 60,
borderRadius: 40, borderRadius: 30,
alignItems: 'center', borderWidth: 2,
justifyContent: 'center', borderColor: '#9370DB',
overflow: 'hidden',
}, },
userDetails: { userDetails: {
flex: 1, flex: 1,
@@ -343,86 +293,75 @@ const styles = StyleSheet.create({
userName: { userName: {
fontSize: 18, fontSize: 18,
fontWeight: 'bold', fontWeight: 'bold',
color: '#2C3E50',
marginBottom: 4, 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: { statsContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
borderRadius: 16, padding: 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)',
}, },
statItem: { statItem: {
alignItems: 'center', alignItems: 'center',
flex: 1, flex: 1,
}, },
statValue: {
fontSize: 18,
fontWeight: 'bold',
color: '#9370DB',
marginBottom: 4,
},
statLabel: { statLabel: {
fontSize: 12, 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: { menuItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 16, paddingVertical: 14,
paddingHorizontal: 16, paddingHorizontal: 16,
borderRadius: 12, borderBottomWidth: 1,
marginBottom: 8, borderBottomColor: '#F1F3F4',
}, },
menuItemLeft: { menuItemLeft: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
flex: 1, flex: 1,
}, },
menuIcon: { iconContainer: {
width: 36, width: 32,
height: 36, height: 32,
borderRadius: 8, borderRadius: 6,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 12, marginRight: 12,
}, },
menuItemText: { menuItemText: {
fontSize: 16, fontSize: 15,
flex: 1, color: '#2C3E50',
fontWeight: '600', fontWeight: '500',
}, },
switch: { switch: {
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }], transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
}, },
// 浮动按钮
floatingButtonContainer: {
position: 'absolute',
bottom: 30,
left: 0,
right: 0,
alignItems: 'center',
pointerEvents: 'box-none',
},
}); });

View File

@@ -22,7 +22,6 @@ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { import {
Animated, Animated,
@@ -120,6 +119,9 @@ export default function ExploreScreen() {
standHours: 0, standHours: 0,
standHoursGoal: 12 standHoursGoal: 12
}); });
// 血氧饱和度和心率数据
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
const [heartRate, setHeartRate] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// 用于触发动画重置的 token当日期或数据变化时更新 // 用于触发动画重置的 token当日期或数据变化时更新
@@ -225,6 +227,13 @@ export default function ExploreScreen() {
// 更新HRV数据时间 // 更新HRV数据时间
setHrvUpdateTime(new Date()); setHrvUpdateTime(new Date());
// 设置血氧饱和度和心率数据
setOxygenSaturation(data.oxygenSaturation ?? null);
setHeartRate(data.heartRate ?? null);
console.log('血氧饱和度数据:', data.oxygenSaturation);
console.log('心率数据:', data.heartRate);
setAnimToken((t) => t + 1); setAnimToken((t) => t + 1);
} else { } else {
console.log('忽略过期健康数据请求结果key=', requestKey, '最新key=', latestRequestKeyRef.current); console.log('忽略过期健康数据请求结果key=', requestKey, '最新key=', latestRequestKeyRef.current);
@@ -233,6 +242,9 @@ export default function ExploreScreen() {
} catch (error) { } catch (error) {
console.error('HealthKit流程出现异常:', error); console.error('HealthKit流程出现异常:', error);
// 重置血氧饱和度和心率数据
setOxygenSaturation(null);
setHeartRate(null);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -389,7 +401,7 @@ export default function ExploreScreen() {
<FloatingCard style={[styles.masonryCard, styles.moodCard]} delay={1500}> <FloatingCard style={[styles.masonryCard, styles.moodCard]} delay={1500}>
<MoodCard <MoodCard
moodCheckin={currentMoodCheckin} moodCheckin={currentMoodCheckin}
onPress={() => router.push('/mood/calendar')} onPress={() => pushIfAuthedElseLogin('/mood/calendar')}
isLoading={isMoodLoading} isLoading={isMoodLoading}
/> />
</FloatingCard> </FloatingCard>
@@ -436,6 +448,7 @@ export default function ExploreScreen() {
<OxygenSaturationCard <OxygenSaturationCard
resetToken={animToken} resetToken={animToken}
style={styles.basalMetabolismCardOverride} style={styles.basalMetabolismCardOverride}
oxygenSaturation={oxygenSaturation}
/> />
</FloatingCard> </FloatingCard>
@@ -444,6 +457,7 @@ export default function ExploreScreen() {
<HeartRateCard <HeartRateCard
resetToken={animToken} resetToken={animToken}
style={styles.basalMetabolismCardOverride} style={styles.basalMetabolismCardOverride}
heartRate={heartRate}
/> />
</FloatingCard> </FloatingCard>

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<LinearGradient
colors={[
colorTokens.backgroundGradientStart,
colorTokens.backgroundGradientEnd,
]}
style={styles.backgroundGradient}
/>
<View style={styles.content}>
<View style={styles.header}>
<TouchableOpacity
style={[styles.backButton, { backgroundColor: colorTokens.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={colorTokens.text} />
</TouchableOpacity>
<Text style={[styles.title, { color: colorTokens.text }]}>
</Text>
</View>
<View style={styles.demoContainer}>
<View style={[styles.demoCard, { backgroundColor: colorTokens.card }]}>
<View style={styles.iconContainer}>
<Ionicons name="flag" size={48} color={colorTokens.primary} />
</View>
<Text style={[styles.demoTitle, { color: colorTokens.text }]}>
</Text>
<Text style={[styles.demoDescription, { color: colorTokens.textSecondary }]}>
</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
1.5
</Text>
</View>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
//
</Text>
</View>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
</Text>
</View>
<View style={styles.featureItem}>
<Ionicons name="checkmark-circle" size={16} color={colorTokens.primary} />
<Text style={[styles.featureText, { color: colorTokens.textSecondary }]}>
</Text>
</View>
</View>
<TouchableOpacity
style={[styles.enterButton, { backgroundColor: colorTokens.primary }]}
onPress={() => router.push('/goals')}
>
<Text style={[styles.enterButtonText, { color: colorTokens.onPrimary }]}>
</Text>
<Ionicons name="arrow-forward" size={20} color={colorTokens.onPrimary} />
</TouchableOpacity>
</View>
</View>
</View>
</SafeAreaView>
);
}
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,
},
});

View File

@@ -195,11 +195,15 @@ export default function MoodCalendarScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient <LinearGradient
colors={backgroundGradientColors} colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
style={styles.gradientBackground} style={styles.gradientBackground}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }} end={{ x: 0, y: 1 }}
/> />
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<SafeAreaView style={styles.safeArea}> <SafeAreaView style={styles.safeArea}>
<HeaderBar <HeaderBar
title="心情日历" title="心情日历"
@@ -345,6 +349,26 @@ const styles = StyleSheet.create({
top: 0, top: 0,
bottom: 0, bottom: 0,
}, },
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#7a5af8',
opacity: 0.08,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#7a5af8',
opacity: 0.04,
},
safeArea: { safeArea: {
flex: 1, flex: 1,
}, },
@@ -353,45 +377,56 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
calendar: { calendar: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
monthNavigation: { monthNavigation: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 20, marginBottom: 24,
}, },
navButton: { navButton: {
width: 40, width: 44,
height: 40, height: 44,
borderRadius: 20, borderRadius: 22,
backgroundColor: '#f8f9fa', backgroundColor: 'rgba(122,90,248,0.1)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
}, },
navButtonText: { navButtonText: {
fontSize: 20, fontSize: 24,
color: '#333', color: '#7a5af8',
fontWeight: '600', fontWeight: '700',
}, },
monthTitle: { monthTitle: {
fontSize: 18, fontSize: 20,
fontWeight: '700', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
weekHeader: { weekHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
marginBottom: 16, marginBottom: 20,
}, },
weekDay: { weekDay: {
fontSize: 14, fontSize: 13,
color: '#666', color: '#5d6676',
textAlign: 'center', textAlign: 'center',
width: (width - 96) / 7, width: (width - 96) / 7,
fontWeight: '600',
}, },
weekRow: { weekRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -403,19 +438,25 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
dayButton: { dayButton: {
width: 40, width: 44,
height: 40, height: 44,
borderRadius: 20, borderRadius: 22,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 8, marginBottom: 8,
backgroundColor: 'transparent',
}, },
dayButtonSelected: { dayButtonSelected: {
backgroundColor: Colors.light.accentGreen, backgroundColor: '#FFFFFF',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 6,
elevation: 4,
}, },
dayButtonToday: { dayButtonToday: {
borderWidth: 2, borderWidth: 2,
borderColor: Colors.light.accentGreen, borderColor: '#7a5af8',
}, },
dayContent: { dayContent: {
position: 'relative', position: 'relative',
@@ -426,142 +467,174 @@ const styles = StyleSheet.create({
}, },
dayNumber: { dayNumber: {
fontSize: 14, fontSize: 14,
color: '#999', color: '#777f8c',
fontWeight: '500', fontWeight: '600',
position: 'absolute', position: 'absolute',
top: 2, top: 2,
zIndex: 1, zIndex: 1,
}, },
dayNumberSelected: { dayNumberSelected: {
color: '#FFFFFF', color: '#192126',
fontWeight: '600', fontWeight: '700',
}, },
dayNumberToday: { dayNumberToday: {
color: Colors.light.accentGreen, color: '#7a5af8',
fontWeight: '600', fontWeight: '700',
}, },
dayNumberDisabled: { dayNumberDisabled: {
color: '#ccc', color: '#c0c4ca',
}, },
moodIconContainer: { moodIconContainer: {
position: 'absolute', position: 'absolute',
bottom: 2, bottom: 2,
width: 20, width: 22,
height: 20, height: 22,
borderRadius: 10, borderRadius: 11,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 1,
}, },
moodIcon: { moodIcon: {
width: 16, width: 18,
height: 16, height: 18,
borderRadius: 8, borderRadius: 9,
backgroundColor: 'rgba(255,255,255,0.9)', backgroundColor: 'rgba(255,255,255,0.95)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
moodEmoji: { moodEmoji: {
fontSize: 12, fontSize: 11,
}, },
defaultMoodIcon: { defaultMoodIcon: {
position: 'absolute', position: 'absolute',
bottom: 2, bottom: 2,
width: 20, width: 22,
height: 20, height: 22,
borderRadius: 10, borderRadius: 11,
borderWidth: 1, borderWidth: 1.5,
borderColor: '#ddd', borderColor: 'rgba(122,90,248,0.3)',
borderStyle: 'dashed', borderStyle: 'dashed',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(122,90,248,0.05)',
}, },
defaultMoodEmoji: { defaultMoodEmoji: {
fontSize: 10, fontSize: 10,
opacity: 0.3, opacity: 0.4,
color: '#7a5af8',
}, },
selectedDateSection: { selectedDateSection: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
marginTop: 0, marginTop: 0,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
selectedDateHeader: { selectedDateHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 16, marginBottom: 20,
}, },
selectedDateTitle: { selectedDateTitle: {
fontSize: 20, fontSize: 22,
fontWeight: '700', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
addMoodButton: { addMoodButton: {
paddingHorizontal: 16, paddingHorizontal: 20,
height: 32, height: 36,
borderRadius: 16, borderRadius: 18,
backgroundColor: Colors.light.accentGreen, backgroundColor: '#7a5af8',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
}, },
addMoodButtonText: { addMoodButtonText: {
color: '#fff', color: '#fff',
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '700',
}, },
moodRecord: { moodRecord: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
paddingVertical: 12, paddingVertical: 16,
backgroundColor: 'rgba(122,90,248,0.05)',
borderRadius: 16,
paddingHorizontal: 16,
}, },
recordIcon: { recordIcon: {
width: 48, width: 52,
height: 48, height: 52,
borderRadius: 24, borderRadius: 26,
backgroundColor: Colors.light.accentGreen, backgroundColor: '#7a5af8',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, marginRight: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 2,
}, },
recordContent: { recordContent: {
flex: 1, flex: 1,
}, },
recordMood: { recordMood: {
fontSize: 16, fontSize: 18,
color: '#333', color: '#192126',
fontWeight: '500', fontWeight: '700',
marginBottom: 4,
}, },
recordIntensity: { recordIntensity: {
fontSize: 14, fontSize: 14,
color: '#666', color: '#5d6676',
marginTop: 2, marginTop: 2,
fontWeight: '500',
}, },
recordDescription: { recordDescription: {
fontSize: 14, fontSize: 14,
color: '#666', color: '#5d6676',
marginTop: 4, marginTop: 6,
fontStyle: 'italic', fontStyle: 'italic',
lineHeight: 20,
}, },
spacer: { spacer: {
flex: 1, flex: 1,
}, },
recordTime: { recordTime: {
fontSize: 14, fontSize: 14,
color: '#999', color: '#777f8c',
fontWeight: '500',
}, },
emptyRecord: { emptyRecord: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 20, paddingVertical: 32,
}, },
emptyRecordText: { emptyRecordText: {
fontSize: 16, fontSize: 16,
color: '#666', color: '#5d6676',
marginBottom: 8, marginBottom: 8,
fontWeight: '600',
}, },
emptyRecordSubtext: { emptyRecordSubtext: {
fontSize: 12, fontSize: 13,
color: '#999', color: '#777f8c',
textAlign: 'center',
lineHeight: 18,
}, },
}); });

View File

@@ -163,11 +163,15 @@ export default function MoodEditScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient <LinearGradient
colors={backgroundGradientColors} colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
style={styles.gradientBackground} style={styles.gradientBackground}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }} end={{ x: 0, y: 1 }}
/> />
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<SafeAreaView style={styles.safeArea}> <SafeAreaView style={styles.safeArea}>
<HeaderBar <HeaderBar
title={existingMood ? '编辑心情' : '记录心情'} title={existingMood ? '编辑心情' : '记录心情'}
@@ -220,6 +224,7 @@ export default function MoodEditScreen() {
<TextInput <TextInput
style={styles.descriptionInput} style={styles.descriptionInput}
placeholder="描述一下你的心情..." placeholder="描述一下你的心情..."
placeholderTextColor="#777f8c"
value={description} value={description}
onChangeText={setDescription} onChangeText={setDescription}
multiline multiline
@@ -270,6 +275,26 @@ const styles = StyleSheet.create({
top: 0, top: 0,
bottom: 0, bottom: 0,
}, },
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#7a5af8',
opacity: 0.08,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#7a5af8',
opacity: 0.04,
},
safeArea: { safeArea: {
flex: 1, flex: 1,
}, },
@@ -278,29 +303,39 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
dateSection: { dateSection: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
alignItems: 'center', alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
dateTitle: { dateTitle: {
fontSize: 24, fontSize: 26,
fontWeight: '700', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
moodSection: { moodSection: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
marginTop: 0, marginTop: 0,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 20,
fontWeight: '600', fontWeight: '700',
color: '#333', color: '#192126',
marginBottom: 16, marginBottom: 20,
}, },
moodOptions: { moodOptions: {
flexDirection: 'row', flexDirection: 'row',
@@ -310,114 +345,163 @@ const styles = StyleSheet.create({
moodOption: { moodOption: {
width: '30%', width: '30%',
alignItems: 'center', alignItems: 'center',
paddingVertical: 16, paddingVertical: 20,
marginBottom: 16, marginBottom: 16,
borderRadius: 12, borderRadius: 16,
backgroundColor: '#f8f8f8', backgroundColor: 'rgba(122,90,248,0.05)',
borderWidth: 1,
borderColor: 'rgba(122,90,248,0.1)',
}, },
selectedMoodOption: { selectedMoodOption: {
backgroundColor: '#e8f5e8', backgroundColor: 'rgba(122,90,248,0.15)',
borderWidth: 2, borderWidth: 2,
borderColor: Colors.light.accentGreen, borderColor: '#7a5af8',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 2,
}, },
moodEmoji: { moodEmoji: {
fontSize: 24, fontSize: 28,
marginBottom: 8, marginBottom: 10,
}, },
moodLabel: { moodLabel: {
fontSize: 14, fontSize: 14,
color: '#333', color: '#192126',
fontWeight: '600',
}, },
intensitySection: { intensitySection: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
marginTop: 0, marginTop: 0,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
intensityContainer: { intensityContainer: {
alignItems: 'center', alignItems: 'center',
}, },
intensityLabel: { intensityLabel: {
fontSize: 16, fontSize: 18,
fontWeight: '600', fontWeight: '700',
color: '#333', color: '#192126',
marginBottom: 12, marginBottom: 16,
}, },
intensitySlider: { intensitySlider: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
width: '100%', width: '100%',
marginBottom: 8, marginBottom: 12,
paddingHorizontal: 10,
}, },
intensityDot: { intensityDot: {
width: 20, width: 24,
height: 20, height: 24,
borderRadius: 10, borderRadius: 12,
backgroundColor: '#ddd', backgroundColor: 'rgba(122,90,248,0.1)',
borderWidth: 2,
borderColor: 'rgba(122,90,248,0.2)',
}, },
intensityDotActive: { intensityDotActive: {
backgroundColor: Colors.light.accentGreen, backgroundColor: '#7a5af8',
borderColor: '#7a5af8',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 2,
}, },
intensityLabels: { intensityLabels: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
width: '100%', width: '100%',
paddingHorizontal: 10,
}, },
intensityLabelText: { intensityLabelText: {
fontSize: 12, fontSize: 13,
color: '#666', color: '#5d6676',
fontWeight: '500',
}, },
descriptionSection: { descriptionSection: {
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16, margin: 16,
marginTop: 0, marginTop: 0,
borderRadius: 16, borderRadius: 20,
padding: 16, padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
descriptionInput: { descriptionInput: {
borderWidth: 1, borderWidth: 1.5,
borderColor: '#ddd', borderColor: 'rgba(122,90,248,0.2)',
borderRadius: 8, borderRadius: 12,
padding: 12, padding: 16,
fontSize: 16, fontSize: 16,
minHeight: 80, minHeight: 100,
textAlignVertical: 'top', textAlignVertical: 'top',
backgroundColor: 'rgba(122,90,248,0.02)',
color: '#192126',
}, },
characterCount: { characterCount: {
fontSize: 12, fontSize: 12,
color: '#999', color: '#777f8c',
textAlign: 'right', textAlign: 'right',
marginTop: 4, marginTop: 8,
fontWeight: '500',
}, },
footer: { footer: {
padding: 16, padding: 20,
backgroundColor: '#fff', backgroundColor: 'rgba(255,255,255,0.95)',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
}, },
saveButton: { saveButton: {
backgroundColor: Colors.light.accentGreen, backgroundColor: '#7a5af8',
borderRadius: 12, borderRadius: 16,
paddingVertical: 16, paddingVertical: 18,
alignItems: 'center', alignItems: 'center',
marginTop: 8, marginTop: 12,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
}, },
deleteButton: { deleteButton: {
backgroundColor: '#F44336', backgroundColor: '#f95555',
borderRadius: 12, borderRadius: 16,
paddingVertical: 16, paddingVertical: 18,
alignItems: 'center', alignItems: 'center',
shadowColor: '#f95555',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
}, },
disabledButton: { disabledButton: {
backgroundColor: '#ccc', backgroundColor: '#c0c4ca',
shadowOpacity: 0,
elevation: 0,
}, },
saveButtonText: { saveButtonText: {
color: '#fff', color: '#fff',
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '700',
}, },
deleteButtonText: { deleteButtonText: {
color: '#fff', color: '#fff',
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '700',
}, },
}); });

View File

@@ -1,5 +1,5 @@
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors, palette } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme'; import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload'; import { useCosUpload } from '@/hooks/useCosUpload';
@@ -270,9 +270,18 @@ export default function EditProfileScreen() {
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}> <SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
<StatusBar barStyle={'dark-content'} /> <StatusBar barStyle={'dark-content'} />
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
<HeaderBar
title="编辑资料"
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
variant="elevated"
/>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}> <ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
<HeaderBar title="编辑资料" onBack={() => router.back()} withSafeTop={false} transparent />
{/* 头像(带相机蒙层,点击从相册选择) */} {/* 头像(带相机蒙层,点击从相册选择) */}
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}> <View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
@@ -561,7 +570,7 @@ const styles = StyleSheet.create({
backgroundColor: '#F1F5F9', backgroundColor: '#F1F5F9',
}, },
modalBtnPrimary: { modalBtnPrimary: {
backgroundColor: palette.primary, backgroundColor: '#7a5af8',
}, },
modalBtnText: { modalBtnText: {
color: '#334155', color: '#334155',
@@ -571,15 +580,6 @@ const styles = StyleSheet.create({
color: '#0F172A', color: '#0F172A',
fontWeight: '700', 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' },
}); });

View File

@@ -59,19 +59,24 @@ const ActivityHeatMap = () => {
console.log('generateActivityData', generateActivityData); console.log('generateActivityData', generateActivityData);
// 根据活跃度计算颜色 // 根据活跃度计算颜色 - 优化配色方案
const getActivityColor = (level: number): string => { const getActivityColor = (level: number): string => {
// 由于useColorScheme总是返回'light',我们直接使用浅色主题的颜色
switch (level) { switch (level) {
case 0: case 0:
return colors.separator; // 使用主题分隔线 // 无活动:使用主题适配的背景
return colors.separator;
case 1: case 1:
// 低活动:使用主题主色的浅色版本
return 'rgba(122, 90, 248, 0.15)'; // 浅色模式下的浅紫色
case 2: case 2:
return 'rgba(135,206,235,0.4)'; // 中等活动:使用主题主色的中等透明度
return 'rgba(122, 90, 248, 0.35)'; // 浅色模式下的中等紫色
case 3: case 3:
return 'rgba(135,206,235,0.65)'; // 高活动:使用主题主色的较高透明度
return 'rgba(122, 90, 248, 0.55)'; // 浅色模式下的较深紫色
case 4: case 4:
default: default:
// 最高活动:使用主题主色
return colors.primary; return colors.primary;
} }
}; };
@@ -143,14 +148,20 @@ const ActivityHeatMap = () => {
}, [generateActivityData]); }, [generateActivityData]);
return ( return (
<View style={[styles.container, { backgroundColor: colors.card }]}> <View style={[styles.container, {
backgroundColor: colors.card,
borderColor: colors.border,
shadowColor: 'rgba(122, 90, 248, 0.08)',
}]}>
{/* 标题和统计 */} {/* 标题和统计 */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleRow}> <View style={styles.titleRow}>
<Text style={[styles.subtitle, { color: colors.textMuted }]}> <Text style={[styles.subtitle, { color: colors.textMuted }]}>
6 {activityStats.activeDays} 6 {activityStats.activeDays}
</Text> </Text>
<View style={[styles.statsBadge, { backgroundColor: colors.ornamentPrimary }]}> <View style={[styles.statsBadge, {
backgroundColor: 'rgba(122, 90, 248, 0.1)'
}]}>
<Text style={[styles.statsText, { color: colors.primary }]}> <Text style={[styles.statsText, { color: colors.primary }]}>
{activityStats.activeRate}% {activityStats.activeRate}%
</Text> </Text>
@@ -238,13 +249,10 @@ const styles = StyleSheet.create({
borderRadius: 16, borderRadius: 16,
padding: 20, padding: 20,
marginBottom: 20, marginBottom: 20,
shadowColor: 'rgba(135,206,235,0.25)',
shadowOffset: { width: 0, height: 3 }, shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.12, shadowOpacity: 0.12,
shadowRadius: 6, shadowRadius: 6,
elevation: 3, elevation: 3,
borderWidth: 1,
borderColor: 'rgba(135,206,235,0.08)',
}, },
header: { header: {
marginBottom: 8, marginBottom: 8,

View File

@@ -24,6 +24,9 @@ const HealthDataCard: React.FC<HealthDataCardProps> = ({
style={[styles.card, style]} style={[styles.card, style]}
> >
<View style={styles.iconContainer}>
{icon}
</View>
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.title}>{title}</Text> <Text style={styles.title}>{title}</Text>
<View style={styles.valueContainer}> <View style={styles.valueContainer}>
@@ -53,14 +56,18 @@ const styles = StyleSheet.create({
}, },
iconContainer: { iconContainer: {
marginRight: 16, marginRight: 16,
alignItems: 'center',
justifyContent: 'center',
}, },
content: { content: {
flex: 1, flex: 1,
justifyContent: 'center',
}, },
title: { title: {
fontSize: 14, fontSize: 14,
color: '#666', color: '#666',
marginBottom: 4, marginBottom: 4,
fontWeight: '600',
}, },
valueContainer: { valueContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -68,14 +75,15 @@ const styles = StyleSheet.create({
}, },
value: { value: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: '800',
color: '#333', color: '#192126',
}, },
unit: { unit: {
fontSize: 14, fontSize: 14,
color: '#666', color: '#666',
marginLeft: 4, marginLeft: 4,
marginBottom: 2, marginBottom: 2,
fontWeight: '500',
}, },
}); });

View File

@@ -1,29 +1,19 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import HealthDataService from '../../services/healthData';
import HealthDataCard from './HealthDataCard'; import HealthDataCard from './HealthDataCard';
interface HeartRateCardProps { interface HeartRateCardProps {
resetToken: number; resetToken: number;
style?: object; style?: object;
heartRate?: number | null;
} }
const HeartRateCard: React.FC<HeartRateCardProps> = ({ const HeartRateCard: React.FC<HeartRateCardProps> = ({
resetToken, resetToken,
style style,
heartRate
}) => { }) => {
const [heartRate, setHeartRate] = useState<number | null>(null);
useEffect(() => {
const fetchHeartRate = async () => {
const data = await HealthDataService.getHeartRate();
setHeartRate(data);
};
fetchHeartRate();
}, [resetToken]);
const heartIcon = ( const heartIcon = (
<Ionicons name="heart" size={24} color="#EF4444" /> <Ionicons name="heart" size={24} color="#EF4444" />
); );
@@ -31,7 +21,7 @@ const HeartRateCard: React.FC<HeartRateCardProps> = ({
return ( return (
<HealthDataCard <HealthDataCard
title="心率" title="心率"
value={heartRate !== null ? heartRate.toString() : '--'} value={heartRate !== null && heartRate !== undefined ? Math.round(heartRate).toString() : '--'}
unit="bpm" unit="bpm"
icon={heartIcon} icon={heartIcon}
style={style} style={style}

View File

@@ -1,29 +1,19 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import HealthDataService from '../../services/healthData';
import HealthDataCard from './HealthDataCard'; import HealthDataCard from './HealthDataCard';
interface OxygenSaturationCardProps { interface OxygenSaturationCardProps {
resetToken: number; resetToken: number;
style?: object; style?: object;
oxygenSaturation?: number | null;
} }
const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
resetToken, resetToken,
style style,
oxygenSaturation
}) => { }) => {
const [oxygenSaturation, setOxygenSaturation] = useState<number | null>(null);
useEffect(() => {
const fetchOxygenSaturation = async () => {
const data = await HealthDataService.getOxygenSaturation();
setOxygenSaturation(data);
};
fetchOxygenSaturation();
}, [resetToken]);
const oxygenIcon = ( const oxygenIcon = (
<Ionicons name="water" size={24} color="#3B82F6" /> <Ionicons name="water" size={24} color="#3B82F6" />
); );
@@ -31,7 +21,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
return ( return (
<HealthDataCard <HealthDataCard
title="血氧饱和度" title="血氧饱和度"
value={oxygenSaturation !== null ? oxygenSaturation.toString() : '--'} value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'}
unit="%" unit="%"
icon={oxygenIcon} icon={oxygenIcon}
style={style} style={style}

View File

@@ -14,6 +14,7 @@ export type HeaderBarProps = {
showBottomBorder?: boolean; showBottomBorder?: boolean;
withSafeTop?: boolean; withSafeTop?: boolean;
transparent?: boolean; transparent?: boolean;
variant?: 'default' | 'elevated' | 'minimal';
}; };
export function HeaderBar({ export function HeaderBar({
@@ -24,34 +25,115 @@ export function HeaderBar({
showBottomBorder = false, showBottomBorder = false,
withSafeTop = true, withSafeTop = true,
transparent = true, transparent = true,
variant = 'default',
}: HeaderBarProps) { }: HeaderBarProps) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const colorScheme = useColorScheme() ?? 'light'; const colorScheme = useColorScheme() ?? 'light';
const theme = Colors[tone ?? colorScheme]; const theme = Colors[tone ?? colorScheme];
// 根据变体确定背景色和样式
const getBackgroundColor = () => {
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 ( return (
<View <View
style={[ style={[
styles.header, styles.header,
{ {
paddingTop: (withSafeTop ? insets.top : 0) + 8, paddingTop: (withSafeTop ? insets.top : 0) + 8,
backgroundColor: transparent ? 'transparent' : theme.card, backgroundColor: getBackgroundColor(),
borderBottomWidth: showBottomBorder ? 1 : 0, ...getBorderStyle(),
borderBottomColor: theme.border, ...(variant === 'elevated' && {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 3,
elevation: 2,
}),
}, },
]} ]}
> >
{onBack ? ( {onBack ? (
<TouchableOpacity accessibilityRole="button" onPress={onBack} style={[styles.backButton, { backgroundColor: `${Colors.light.accentGreen}33` }]}> <TouchableOpacity
<Ionicons name="chevron-back" size={20} color={theme.onPrimary} /> accessibilityRole="button"
onPress={onBack}
style={getBackButtonStyle()}
activeOpacity={0.7}
>
<Ionicons
name="chevron-back"
size={20}
color={getBackButtonIconColor()}
/>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<View style={{ width: 32 }} /> <View style={{ width: 32 }} />
)} )}
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View style={styles.titleContainer}>
{typeof title === 'string' ? ( {typeof title === 'string' ? (
<Text style={[styles.title, { color: theme.text }]}>{title}</Text> <Text style={[
styles.title,
{
color: theme.text,
fontWeight: variant === 'elevated' ? '700' : '800',
}
]}>
{title}
</Text>
) : ( ) : (
title title
)} )}
@@ -68,7 +150,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 10, paddingBottom: 12,
minHeight: 44,
width: '100%',
}, },
backButton: { backButton: {
width: 32, width: 32,
@@ -76,10 +160,25 @@ const styles = StyleSheet.create({
borderRadius: 16, borderRadius: 16,
alignItems: 'center', alignItems: 'center',
justifyContent: '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: { title: {
fontSize: 20, fontSize: 18,
fontWeight: '800', textAlign: 'center',
letterSpacing: -0.3,
}, },
}); });

View File

@@ -1,138 +1,212 @@
/** /**
* 应用全局配色规范(来自设计规范图)。 * 应用全局配色规范(基于设计规范图)。
* 说明:保持原有导出结构不变,同时扩展更完整的语义令牌与原子调色板 * 包含完整的语义化颜色系统:灰色、紫色、成功色、错误色、警告色和基础色
*/ */
// 原子调色板(与设计图一致) // 原子调色板(与设计图一致)
export const palette = { export const palette = {
// Primary // 灰色系统 - 中性色UI设计的基础
primary: '#7A5AF8', gray: {
ink: '#FFFFFF', 25: '#fcfcfd',
50: '#ebecee',
100: '#c0c4ca',
200: '#a2a7b0',
300: '#777f8c',
400: '#5d6676',
500: '#344054',
600: '#2f3a4c',
700: '#252d3c',
800: '#1d232e',
900: '#161b23',
},
// Secondary / Neutrals // 紫色系统 - 品牌主色,用于交互元素
neutral100: '#888F92', purple: {
neutral200: '#5E6468', 25: '#fafaff',
neutral300: '#384046', 50: '#f4f3ff',
100: '#ebe9fe',
200: '#d9d6fe',
300: '#bdb4fe',
400: '#9b8afb',
500: '#7a5af8',
600: '#6938ef',
700: '#5925dc',
800: '#4a1fb8',
900: '#3e1c96',
},
// Accents // 成功色系统 - 绿色,用于正面反馈
purple: '#A48AED', success: {
red: '#ED4747', 25: '#f6fef9',
orange: '#FCC46F', 50: '#e8f7f1',
blue: '#7A5AF8', // 更贴近logo背景的天空蓝 100: '#b8e7d2',
blueSecondary: '#4682B4', // 钢蓝色,用于选中状态 200: '#95dcbc',
green: '#9ceb87', // 温暖的绿色,用于心情日历等 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; } as const;
const primaryColor = palette.blue; // 应用主题色 // 主色调定义
const primaryColor = palette.purple[500]; // 紫色500作为主色
const tintColorLight = primaryColor; const tintColorLight = primaryColor;
const tintColorDark = '#FFFFFF'; const tintColorDark = palette.base.white;
export const Colors = { export const Colors = {
light: { light: {
// 基础文本/背景(优化对比度) // 基础文本/背景
text: '#1A2027', // 更深的文本色,提高可读性 text: palette.gray[900], // 最深灰色用于主要文本
textSecondary: '#4A5568', // 温和的次要文本 textSecondary: palette.gray[600], // 中等灰色用于次要文本
textMuted: '#718096', // 柔和的静音文本 textMuted: palette.gray[400], // 浅灰色用于静音文本
background: '#FFFFFF', background: palette.base.white,
surface: '#FFFFFF', surface: palette.base.white,
card: 'rgba(255,255,255,0.95)', // 半透明卡片,与渐变背景融合 card: palette.gray[25], // 最浅灰色用于卡片背景
buttonBackground: palette.blue,
// 品牌与可交互主色 // 品牌与可交互主色
tint: tintColorLight, tint: tintColorLight,
primary: primaryColor, primary: primaryColor,
onPrimary: palette.ink, // 主色搭配的前景色(按钮文字/图标 onPrimary: palette.base.white, // 主色上的文字/图标颜色
// 中性色与辅助 // 中性色与辅助
neutral100: palette.neutral100, neutral100: palette.gray[100],
neutral200: palette.neutral200, neutral200: palette.gray[200],
neutral300: palette.neutral300, neutral300: palette.gray[300],
// 状态/反馈色 // 状态/反馈色
success: palette.primary, success: palette.success[500],
warning: palette.orange, successLight: palette.success[100],
danger: palette.red, successDark: palette.success[700],
info: palette.blue, warning: palette.warning[500],
accentPurple: palette.purple, warningLight: palette.warning[100],
accentGreen: palette.green, // 温暖的绿色 warningDark: palette.warning[700],
accentGreenDark: palette.blueSecondary, // 深绿色,用于文本和强调 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, datePickerNormal: palette.purple[500],
datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态 datePickerSelected: palette.success[500],
// 结构色(优化后的蓝色主题) // 结构色
border: 'rgba(135,206,235,0.2)', // 蓝色调边框 border: palette.gray[200], // 浅灰色边框
separator: 'rgba(135,206,235,0.15)', // 更的分隔线 separator: palette.gray[100], // 更的分隔线
icon: '#5A6C7D', // 灰色图标 icon: palette.gray[500], // 中等灰色图标
// Tab 相关(保持兼容) // Tab 相关
tabIconDefault: '#687076', tabIconDefault: palette.gray[400],
tabIconSelected: palette.ink, // tab 激活时的文字/图标颜色(深色,在亮色背景上显示) tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见
tabBarBackground: palette.ink, // tab 栏背景色 tabBarBackground: palette.base.white,
tabBarActiveBackground: primaryColor, // tab 激活时的背景 tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景
// 页面氛围与装饰(优化后的蓝色配色方案) // 页面氛围与装饰
pageBackgroundEmphasis: '#F0F8FF', // 淡蓝色背景强调 pageBackgroundEmphasis: palette.gray[25],
heroSurfaceTint: 'rgba(135,206,235,0.12)', // 更柔和的蓝色调表面色彩 heroSurfaceTint: palette.purple[25],
ornamentPrimary: 'rgba(135,206,235,0.15)', // 与主色调和的装饰色 ornamentPrimary: palette.purple[100],
ornamentAccent: 'rgba(70,130,180,0.12)', // 钢蓝色装饰,增加层次 ornamentAccent: palette.success[100],
// 优化的背景渐变色(更柔和的蓝色过渡) // 背景渐变色
backgroundGradientStart: '#E6F3FF', // 更柔和的浅蓝色起始 backgroundGradientStart: palette.gray[25],
backgroundGradientEnd: '#FAFCFF', // 带有微蓝调的白色结束 backgroundGradientEnd: palette.base.white,
}, },
dark: { dark: {
// 基础文本/背景 // 基础文本/背景
text: '#ECEDEE', text: palette.gray[25], // 最浅灰色用于主要文本
textSecondary: palette.neutral100, textSecondary: palette.gray[100], // 浅灰色用于次要文本
textMuted: '#9BA1A6', textMuted: palette.gray[300], // 中等灰色用于静音文本
background: '#151718', background: palette.gray[900],
surface: '#1A1D1E', surface: palette.gray[800],
card: '#1A1D1E', card: palette.gray[800],
// 品牌与可交互主色 // 品牌与可交互主色
tint: tintColorDark, tint: tintColorDark,
primary: primaryColor, primary: primaryColor,
onPrimary: palette.ink, onPrimary: palette.base.white,
// 中性色与辅助 // 中性色与辅助
neutral100: palette.neutral100, neutral100: palette.gray[100],
neutral200: palette.neutral200, neutral200: palette.gray[200],
neutral300: palette.neutral300, neutral300: palette.gray[300],
// 状态/反馈色 // 状态/反馈色
success: palette.primary, success: palette.success[400], // 深色模式下使用较亮的成功色
warning: palette.orange, successLight: palette.success[200],
danger: palette.red, successDark: palette.success[600],
info: palette.blue, warning: palette.warning[400],
accentPurple: palette.purple, warningLight: palette.warning[200],
accentGreenDark: '#2D5016', // 深绿色,用于文本和强调 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, datePickerNormal: palette.purple[400],
datePickerSelected: palette.green, // 使用温暖的绿色作为选中状态 datePickerSelected: palette.success[400],
// 结构色 // 结构色
border: '#2A2F32', border: palette.gray[700],
separator: '#2A2F32', separator: palette.gray[700],
icon: '#9BA1A6', icon: palette.gray[300],
// Tab 相关(保持兼容) // Tab 相关
tabIconDefault: '#9BA1A6', tabIconDefault: palette.gray[400],
tabIconSelected: palette.ink, // 在亮色背景上使用深色文字 tabIconSelected: palette.base.white, // 选中时使用白色文字/图标,确保在紫色背景上清晰可见
tabBarBackground: palette.ink, tabBarBackground: palette.gray[800],
tabBarActiveBackground: primaryColor, tabBarActiveBackground: palette.purple[500], // 使用主色作为选中背景,在深色模式下更突出
// 页面氛围与装饰(新) // 页面氛围与装饰
pageBackgroundEmphasis: '#151718', pageBackgroundEmphasis: palette.gray[800],
heroSurfaceTint: 'rgba(187,242,70,0.12)', heroSurfaceTint: palette.purple[900],
ornamentPrimary: 'rgba(187,242,70,0.18)', ornamentPrimary: palette.purple[800],
ornamentAccent: 'rgba(164,138,237,0.14)', ornamentAccent: palette.success[800],
// 统一背景渐变色(深色模式) // 背景渐变色
backgroundGradientStart: '#0A0B0C', // 深黑色起始 backgroundGradientStart: palette.gray[900],
backgroundGradientEnd: '#151718', // 背景色结束 backgroundGradientEnd: palette.gray[800],
}, },
} as const; } as const;

View File

@@ -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
// 默认透明背景
<HeaderBar
title="页面标题"
onBack={() => navigation.goBack()}
/>
// 卡片背景
<HeaderBar
title="页面标题"
onBack={() => navigation.goBack()}
transparent={false}
showBottomBorder={true}
/>
```
### 变体使用
```tsx
// Elevated 变体 - 重要页面
<HeaderBar
title="重要操作"
onBack={() => navigation.goBack()}
variant="elevated"
showBottomBorder={true}
/>
// Minimal 变体 - 内容页面
<HeaderBar
title="内容列表"
onBack={() => navigation.goBack()}
variant="minimal"
/>
```
### 自定义右侧内容
```tsx
<HeaderBar
title="编辑页面"
onBack={() => navigation.goBack()}
right={
<TouchableOpacity onPress={handleSave}>
<Text style={{ color: theme.primary }}>保存</Text>
</TouchableOpacity>
}
variant="elevated"
/>
```
### 自定义标题
```tsx
<HeaderBar
title={
<View>
<Text style={{ color: theme.text, fontSize: 18, fontWeight: '700' }}>
主标题
</Text>
<Text style={{ color: theme.textSecondary, fontSize: 12 }}>
副标题
</Text>
</View>
}
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 组件与整个应用的设计系统完美融合,提供了优美且一致的用户体验。

View File

@@ -11,6 +11,8 @@ const PERMISSIONS: HealthKitPermissions = {
AppleHealthKit.Constants.Permissions.SleepAnalysis, AppleHealthKit.Constants.Permissions.SleepAnalysis,
AppleHealthKit.Constants.Permissions.HeartRateVariability, AppleHealthKit.Constants.Permissions.HeartRateVariability,
AppleHealthKit.Constants.Permissions.ActivitySummary, AppleHealthKit.Constants.Permissions.ActivitySummary,
AppleHealthKit.Constants.Permissions.OxygenSaturation,
AppleHealthKit.Constants.Permissions.HeartRate,
], ],
write: [ write: [
// 支持体重写入 // 支持体重写入
@@ -32,6 +34,9 @@ export type TodayHealthData = {
exerciseMinutesGoal: number; exerciseMinutesGoal: number;
standHours: number; standHours: number;
standHoursGoal: number; standHoursGoal: number;
// 新增血氧饱和度和心率数据
oxygenSaturation: number | null;
heartRate: number | null;
}; };
export async function ensureHealthPermissions(): Promise<boolean> { export async function ensureHealthPermissions(): Promise<boolean> {
@@ -74,8 +79,8 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
console.log('查询选项:', options); console.log('查询选项:', options);
// 并行获取所有健康数据包括ActivitySummary // 并行获取所有健康数据包括ActivitySummary、血氧饱和度和心率
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary] = await Promise.all([ const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate] = await Promise.all([
// 获取步数 // 获取步数
new Promise<number>((resolve) => { new Promise<number>((resolve) => {
AppleHealthKit.getStepCount({ AppleHealthKit.getStepCount({
@@ -198,10 +203,68 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
resolve(results[0]); resolve(results[0]);
}, },
); );
}),
// 获取血氧饱和度数据
new Promise<number | null>((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<number | null>((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 { return {
steps, steps,
@@ -215,7 +278,10 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
exerciseMinutes: activitySummary?.appleExerciseTime || 0, exerciseMinutes: activitySummary?.appleExerciseTime || 0,
exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30, exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30,
standHours: activitySummary?.appleStandHours || 0, standHours: activitySummary?.appleStandHours || 0,
standHoursGoal: activitySummary?.appleStandHoursGoal || 12 standHoursGoal: activitySummary?.appleStandHoursGoal || 12,
// 血氧饱和度和心率数据
oxygenSaturation,
heartRate
}; };
} }