feat(ui): 统一应用主题色为天空蓝并优化渐变背景

将应用主色调从 '#BBF246' 更改为 '#87CEEB'(天空蓝),并更新所有相关组件和页面中的颜色引用。同时为多个页面添加统一的渐变背景,提升视觉效果和用户体验。新增压力分析模态框组件,并优化压力计组件的交互与显示逻辑。更新应用图标和启动图资源。
This commit is contained in:
richarjiang
2025-08-20 09:38:25 +08:00
parent 37f8c3c78d
commit d76ba48424
35 changed files with 519 additions and 184 deletions

View File

@@ -17,7 +17,7 @@ import {
Text, Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View, View
} from 'react-native'; } from 'react-native';
import Markdown from 'react-native-markdown-display'; import Markdown from 'react-native-markdown-display';
import Animated, { FadeInDown, FadeInUp, Layout } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeInUp, Layout } from 'react-native-reanimated';
@@ -32,6 +32,7 @@ import { deleteConversation, getConversationDetail, listConversations, type AiCo
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession'; import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
import { api, getAuthToken, postTextStream } from '@/services/api'; import { api, getAuthToken, postTextStream } from '@/services/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { ActionSheet } from '../../components/ui/ActionSheet'; import { ActionSheet } from '../../components/ui/ActionSheet';
type Role = 'user' | 'assistant'; type Role = 'user' | 'assistant';
@@ -1121,7 +1122,7 @@ export default function CoachScreen() {
style={[ style={[
styles.bubble, styles.bubble,
{ {
backgroundColor: isUser ? theme.primary : 'rgba(187,242,70,0.16)', backgroundColor: theme.card, // 16% opacity
borderTopLeftRadius: isUser ? 16 : 6, borderTopLeftRadius: isUser ? 16 : 6,
borderTopRightRadius: isUser ? 6 : 16, borderTopRightRadius: isUser ? 6 : 16,
maxWidth: isUser ? '82%' : '90%', maxWidth: isUser ? '82%' : '90%',
@@ -1347,7 +1348,7 @@ export default function CoachScreen() {
</View> </View>
)} )}
{isSelected && isPending && ( {isSelected && isPending && (
<ActivityIndicator size="small" color="#2D5016" /> <ActivityIndicator size="small" color={Colors.light.accentGreenDark} />
)} )}
{isSelected && !isPending && ( {isSelected && !isPending && (
<View style={styles.selectedBadge}> <View style={styles.selectedBadge}>
@@ -1594,7 +1595,13 @@ export default function CoachScreen() {
} }
return ( return (
<View style={[styles.screen, { backgroundColor: theme.background }]}> <View style={styles.screen}>
<LinearGradient
colors={[theme.backgroundGradientStart, theme.backgroundGradientEnd]}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 顶部标题区域,显示教练名称、新建会话和历史按钮 */} {/* 顶部标题区域,显示教练名称、新建会话和历史按钮 */}
<View <View
style={[styles.header, { paddingTop: insets.top + 10 }]} style={[styles.header, { paddingTop: insets.top + 10 }]}
@@ -1608,14 +1615,14 @@ export default function CoachScreen() {
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={startNewConversation} onPress={startNewConversation}
style={[styles.headerActionButton, { backgroundColor: 'rgba(187,242,70,0.2)' }]} style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity
> >
<Ionicons name="add-outline" size={18} color={theme.onPrimary} /> <Ionicons name="add-outline" size={18} color={theme.onPrimary} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={openHistory} onPress={openHistory}
style={[styles.headerActionButton, { backgroundColor: 'rgba(187,242,70,0.2)' }]} style={[styles.headerActionButton, { backgroundColor: `${Colors.light.accentGreen}33` }]} // 20% opacity
> >
<Ionicons name="time-outline" size={18} color={theme.onPrimary} /> <Ionicons name="time-outline" size={18} color={theme.onPrimary} />
</TouchableOpacity> </TouchableOpacity>
@@ -1680,7 +1687,7 @@ 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: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.12)' }]} onPress={c.action}> <TouchableOpacity key={c.key} style={[styles.chip, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}1F` }]} onPress={c.action}>
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text> <Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
@@ -1722,11 +1729,11 @@ export default function CoachScreen() {
</ScrollView> </ScrollView>
)} )}
<View style={[styles.inputRow, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.08)' }]}> <View style={[styles.inputRow, { borderColor: `${Colors.light.accentGreen}59`, backgroundColor: `${Colors.light.accentGreen}14` }]}>
<TouchableOpacity <TouchableOpacity
accessibilityRole="button" accessibilityRole="button"
onPress={pickImages} onPress={pickImages}
style={[styles.mediaBtn, { backgroundColor: 'rgba(187,242,70,0.16)' }]} style={[styles.mediaBtn, { backgroundColor: `${Colors.light.accentGreen}28` }]}
> >
<Ionicons name="image-outline" size={18} color={'#192126'} /> <Ionicons name="image-outline" size={18} color={'#192126'} />
</TouchableOpacity> </TouchableOpacity>
@@ -1936,7 +1943,7 @@ const styles = StyleSheet.create({
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'rgba(187,242,70,0.6)' backgroundColor: `${Colors.light.accentGreen}99` // 60% opacity
}, },
dietOptionsContainer: { dietOptionsContainer: {
gap: 8, gap: 8,
@@ -1948,13 +1955,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: 'rgba(187,242,70,0.3)', borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity
}, },
dietOptionIconContainer: { dietOptionIconContainer: {
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 20, borderRadius: 20,
backgroundColor: 'rgba(187,242,70,0.2)', backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 12, marginRight: 12,
@@ -2003,7 +2010,7 @@ const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'rgba(187,242,70,0.6)', backgroundColor: `${Colors.light.accentGreen}99`, // 60% opacity
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
// markdown 基础样式承载容器的字体尺寸保持与气泡一致 // markdown 基础样式承载容器的字体尺寸保持与气泡一致
@@ -2316,17 +2323,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: 'rgba(187,242,70,0.3)', borderColor: `${Colors.light.accentGreen}4D`, // 30% opacity
borderRadius: 12, borderRadius: 12,
padding: 12, padding: 12,
}, },
choiceButtonRecommended: { choiceButtonRecommended: {
borderColor: 'rgba(187,242,70,0.6)', borderColor: `${Colors.light.accentGreen}99`, // 60% opacity
backgroundColor: 'rgba(187,242,70,0.1)', backgroundColor: `${Colors.light.accentGreen}1A`, // 10% opacity
}, },
choiceButtonSelected: { choiceButtonSelected: {
borderColor: '#2D5016', borderColor: Colors.light.accentGreenDark,
backgroundColor: 'rgba(187,242,70,0.2)', backgroundColor: `${Colors.light.accentGreen}33`, // 20% opacity
borderWidth: 2, borderWidth: 2,
}, },
choiceButtonDisabled: { choiceButtonDisabled: {
@@ -2346,10 +2353,10 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
choiceLabelRecommended: { choiceLabelRecommended: {
color: '#2D5016', color: Colors.light.accentGreenDark,
}, },
choiceLabelSelected: { choiceLabelSelected: {
color: '#2D5016', color: Colors.light.accentGreenDark,
fontWeight: '700', fontWeight: '700',
}, },
choiceLabelDisabled: { choiceLabelDisabled: {
@@ -2361,7 +2368,7 @@ const styles = StyleSheet.create({
gap: 8, gap: 8,
}, },
recommendedBadge: { recommendedBadge: {
backgroundColor: 'rgba(187,242,70,0.8)', backgroundColor: `${Colors.light.accentGreen}CC`, // 80% opacity
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 2,
@@ -2369,10 +2376,10 @@ const styles = StyleSheet.create({
recommendedText: { recommendedText: {
fontSize: 12, fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: '#2D5016', color: Colors.light.accentGreenDark,
}, },
selectedBadge: { selectedBadge: {
backgroundColor: '#2D5016', backgroundColor: Colors.light.accentGreenDark,
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 2,
@@ -2405,6 +2412,13 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#FF4444', color: '#FF4444',
}, },
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
}); });
const markdownStyles = { const markdownStyles = {

View File

@@ -9,6 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
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 { Alert, Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; import { Alert, 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';
@@ -287,11 +288,17 @@ export default function PersonalScreen() {
]; ];
return ( return (
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}> <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, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}> <SafeAreaView style={styles.safeArea}>
<ScrollView <ScrollView
style={[styles.scrollView, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]} style={styles.scrollView}
contentContainerStyle={{ paddingBottom: bottomPadding }} contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
@@ -311,6 +318,13 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
safeArea: { safeArea: {
flex: 1, flex: 1,
}, },

View File

@@ -3,6 +3,7 @@ import { BMICard } from '@/components/BMICard';
import { CircularRing } from '@/components/CircularRing'; import { CircularRing } from '@/components/CircularRing';
import { NutritionRadarCard } from '@/components/NutritionRadarCard'; import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar'; import { ProgressBar } from '@/components/ProgressBar';
import { StressAnalysisModal } from '@/components/StressAnalysisModal';
import { StressMeter } from '@/components/StressMeter'; import { StressMeter } from '@/components/StressMeter';
import { WeightHistoryCard } from '@/components/WeightHistoryCard'; import { WeightHistoryCard } from '@/components/WeightHistoryCard';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
@@ -17,6 +18,7 @@ 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 dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { import {
SafeAreaView, SafeAreaView,
@@ -85,6 +87,9 @@ export default function ExploreScreen() {
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null); const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
const [isNutritionLoading, setIsNutritionLoading] = useState(false); const [isNutritionLoading, setIsNutritionLoading] = useState(false);
// 压力分析浮窗状态
const [showStressModal, setShowStressModal] = useState(false);
// 记录最近一次请求的“日期键”,避免旧请求覆盖新结果 // 记录最近一次请求的“日期键”,避免旧请求覆盖新结果
const latestRequestKeyRef = useRef<string | null>(null); const latestRequestKeyRef = useRef<string | null>(null);
@@ -110,7 +115,7 @@ export default function ExploreScreen() {
} else { } else {
derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date(); derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date();
} }
const requestKey = getDateKey(derivedDate); const requestKey = getDateKey(derivedDate);
latestRequestKeyRef.current = requestKey; latestRequestKeyRef.current = requestKey;
@@ -204,10 +209,28 @@ export default function ExploreScreen() {
} }
}; };
// 处理压力卡片点击
const handleStressCardPress = () => {
setShowStressModal(true);
};
// 关闭压力分析浮窗
const handleCloseStressModal = () => {
setShowStressModal(false);
};
// 使用统一的渐变背景色
const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const;
return ( return (
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}> <View style={styles.container}>
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}> <LinearGradient
colors={backgroundGradientColors}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<SafeAreaView style={styles.safeArea}>
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={{ paddingBottom: bottomPadding }} contentContainerStyle={{ paddingBottom: bottomPadding }}
@@ -253,12 +276,13 @@ export default function ExploreScreen() {
<View style={styles.masonryContainer}> <View style={styles.masonryContainer}>
{/* 左列 */} {/* 左列 */}
<View style={styles.masonryColumn}> <View style={styles.masonryColumn}>
<StressMeter <StressMeter
value={hrvValue} value={hrvValue}
updateTime={hrvUpdateTime} updateTime={hrvUpdateTime}
style={styles.masonryCard} style={styles.masonryCard}
onPress={handleStressCardPress}
/> />
<View style={[styles.masonryCard, styles.caloriesCard]}> <View style={[styles.masonryCard, styles.caloriesCard]}>
<Text style={styles.cardTitleSecondary}></Text> <Text style={styles.cardTitleSecondary}></Text>
{activeCalories != null ? ( {activeCalories != null ? (
@@ -306,20 +330,20 @@ export default function ExploreScreen() {
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined} weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
height={userProfile?.height ? parseFloat(userProfile.height) : undefined} height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
style={styles.masonryCardNoBg} style={styles.masonryCardNoBg}
compact={true} // compact={true}
/> />
<View style={[styles.masonryCard, styles.trainingCard]}> <View style={[styles.masonryCard, styles.trainingCard]}>
<Text style={styles.cardTitleSecondary}></Text> <Text style={styles.cardTitleSecondary}></Text>
<View style={styles.trainingContent}> <View style={styles.trainingContent}>
<CircularRing <CircularRing
size={120} size={120}
strokeWidth={12} strokeWidth={12}
trackColor="#E2D9FD" trackColor="#E2D9FD"
progressColor="#8B74F3" progressColor="#8B74F3"
progress={trainingProgress} progress={trainingProgress}
resetToken={animToken} resetToken={animToken}
/> />
</View> </View>
</View> </View>
@@ -347,16 +371,32 @@ export default function ExploreScreen() {
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
{/* 压力分析浮窗 */}
<StressAnalysisModal
visible={showStressModal}
onClose={handleCloseStressModal}
hrvValue={hrvValue}
updateTime={hrvUpdateTime}
/>
</View> </View>
); );
} }
const primary = Colors.light.primary; const primary = Colors.light.primary;
const lightColors = Colors.light;
const darkColors = Colors.dark;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#F6F7F8', },
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
}, },
safeArea: { safeArea: {
flex: 1, flex: 1,
@@ -388,10 +428,10 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
dayPillNormal: { dayPillNormal: {
backgroundColor: '#C8F852', backgroundColor: lightColors.datePickerNormal,
}, },
dayPillSelected: { dayPillSelected: {
backgroundColor: '#192126', backgroundColor: lightColors.datePickerSelected,
}, },
dayLabel: { dayLabel: {
fontSize: 16, fontSize: 16,
@@ -414,7 +454,7 @@ const styles = StyleSheet.create({
width: 8, width: 8,
height: 8, height: 8,
borderRadius: 4, borderRadius: 4,
backgroundColor: '#192126', backgroundColor: lightColors.datePickerSelected,
marginTop: 10, marginTop: 10,
marginBottom: 4, marginBottom: 4,
alignSelf: 'center', alignSelf: 'center',

View File

@@ -341,7 +341,7 @@ function UploadTile({
> >
{uploading ? ( {uploading ? (
<View style={[styles.placeholder, { backgroundColor: '#f5f5f5' }]}> <View style={[styles.placeholder, { backgroundColor: '#f5f5f5' }]}>
<ActivityIndicator size="large" color="#BBF246" /> <ActivityIndicator size="large" color={Colors.light.accentGreen} />
<Text style={styles.placeholderTitle}>...</Text> <Text style={styles.placeholderTitle}>...</Text>
</View> </View>
) : value ? ( ) : value ? (
@@ -349,7 +349,7 @@ function UploadTile({
) : ( ) : (
<View style={styles.placeholder}> <View style={styles.placeholder}>
<View style={styles.plusBadge}> <View style={styles.plusBadge}>
<Ionicons name="camera" size={16} color="#BBF246" /> <Ionicons name="camera" size={16} color={Colors.light.accentGreen} />
</View> </View>
<Text style={styles.placeholderTitle}></Text> <Text style={styles.placeholderTitle}></Text>
<Text style={styles.placeholderDesc}></Text> <Text style={styles.placeholderDesc}></Text>
@@ -415,7 +415,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
height: 40, height: 40,
borderRadius: 12, borderRadius: 12,
backgroundColor: '#BBF246', backgroundColor: Colors.light.accentGreen,
}, },
permPrimaryText: { permPrimaryText: {
color: '#192126', color: '#192126',
@@ -518,7 +518,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderWidth: 2, borderWidth: 2,
borderColor: '#BBF246', borderColor: Colors.light.accentGreen,
}, },
placeholderTitle: { placeholderTitle: {
color: '#192126', color: '#192126',

View File

@@ -166,7 +166,7 @@ const styles = StyleSheet.create({
borderRadius: INNER_RING_SIZE / 2, borderRadius: INNER_RING_SIZE / 2,
borderWidth: 2, borderWidth: 2,
borderColor: 'rgba(187,242,70,0.65)', borderColor: 'rgba(187,242,70,0.65)',
shadowColor: '#BBF246', shadowColor: Colors.light.accentGreen,
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.35, shadowOpacity: 0.35,
shadowRadius: 24, shadowRadius: 24,
@@ -207,8 +207,8 @@ const styles = StyleSheet.create({
width: 14, width: 14,
height: 14, height: 14,
borderRadius: 7, borderRadius: 7,
backgroundColor: '#BBF246', backgroundColor: Colors.light.accentGreen,
shadowColor: '#BBF246', shadowColor: Colors.light.accentGreen,
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.4, shadowOpacity: 0.4,
shadowRadius: 16, shadowRadius: 16,

View File

@@ -1,4 +1,5 @@
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { completeDay, setCustom } from '@/store/challengeSlice'; import { completeDay, setCustom } from '@/store/challengeSlice';
import type { Exercise, ExerciseCustomConfig } from '@/utils/pilatesPlan'; import type { Exercise, ExerciseCustomConfig } from '@/utils/pilatesPlan';
@@ -158,7 +159,7 @@ const styles = StyleSheet.create({
counterValue: { minWidth: 40, textAlign: 'center', fontWeight: '700', color: '#111827' }, counterValue: { minWidth: 40, textAlign: 'center', fontWeight: '700', color: '#111827' },
setPill: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 999 }, setPill: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 999 },
setPillTodo: { backgroundColor: '#F3F4F6' }, setPillTodo: { backgroundColor: '#F3F4F6' },
setPillDone: { backgroundColor: '#BBF246' }, setPillDone: { backgroundColor: Colors.light.accentGreen },
setPillText: { fontSize: 12, fontWeight: '700' }, setPillText: { fontSize: 12, fontWeight: '700' },
setPillTextTodo: { color: '#6B7280' }, setPillTextTodo: { color: '#6B7280' },
setPillTextDone: { color: '#192126' }, setPillTextDone: { color: '#192126' },
@@ -167,7 +168,7 @@ const styles = StyleSheet.create({
tipsBox: { marginTop: 10, backgroundColor: '#F9FAFB', borderRadius: 8, padding: 10 }, tipsBox: { marginTop: 10, backgroundColor: '#F9FAFB', borderRadius: 8, padding: 10 },
tipText: { fontSize: 12, color: '#6B7280', lineHeight: 18 }, tipText: { fontSize: 12, color: '#6B7280', lineHeight: 18 },
bottomBar: { position: 'absolute', left: 0, right: 0, bottom: 0, padding: 20, backgroundColor: 'transparent' }, bottomBar: { position: 'absolute', left: 0, right: 0, bottom: 0, padding: 20, backgroundColor: 'transparent' },
finishBtn: { backgroundColor: '#BBF246', paddingVertical: 14, borderRadius: 999, alignItems: 'center' }, finishBtn: { backgroundColor: Colors.light.accentGreen, paddingVertical: 14, borderRadius: 999, alignItems: 'center' },
finishBtnText: { color: '#192126', fontWeight: '800', fontSize: 16 }, finishBtnText: { color: '#192126', fontWeight: '800', fontSize: 16 },
}); });

View File

@@ -1,4 +1,5 @@
import { HeaderBar } from '@/components/ui/HeaderBar'; import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { initChallenge } from '@/store/challengeSlice'; import { initChallenge } from '@/store/challengeSlice';
@@ -113,7 +114,7 @@ const styles = StyleSheet.create({
}, },
summaryLeft: { flexDirection: 'row', alignItems: 'center' }, summaryLeft: { flexDirection: 'row', alignItems: 'center' },
progressPill: { width: 120, height: 10, borderRadius: 999, backgroundColor: '#E5E7EB', overflow: 'hidden' }, progressPill: { width: 120, height: 10, borderRadius: 999, backgroundColor: '#E5E7EB', overflow: 'hidden' },
progressFill: { height: '100%', backgroundColor: '#BBF246' }, progressFill: { height: '100%', backgroundColor: Colors.light.accentGreen },
progressText: { marginLeft: 12, fontWeight: '700', color: '#111827' }, progressText: { marginLeft: 12, fontWeight: '700', color: '#111827' },
summaryRight: {}, summaryRight: {},
summaryItem: { fontSize: 12, color: '#6B7280' }, summaryItem: { fontSize: 12, color: '#6B7280' },
@@ -133,7 +134,7 @@ const styles = StyleSheet.create({
dayNumberLocked: { color: '#9CA3AF' }, dayNumberLocked: { color: '#9CA3AF' },
dayMinutes: { marginTop: 4, fontSize: 12, color: '#6B7280' }, dayMinutes: { marginTop: 4, fontSize: 12, color: '#6B7280' },
bottomBar: { padding: 20 }, bottomBar: { padding: 20 },
startButton: { backgroundColor: '#BBF246', paddingVertical: 14, borderRadius: 999, alignItems: 'center' }, startButton: { backgroundColor: Colors.light.accentGreen, paddingVertical: 14, borderRadius: 999, alignItems: 'center' },
startButtonText: { color: '#192126', fontWeight: '800', fontSize: 16 }, startButtonText: { color: '#192126', fontWeight: '800', fontSize: 16 },
}); });

View File

@@ -178,7 +178,7 @@ function PlanCard({ plan, onPress, onDelete, onActivate, onSchedule, isActive, i
</Pressable> </Pressable>
<Pressable style={[styles.metricItem, isActive && styles.metricActive]} onPress={onActivate} hitSlop={8}> <Pressable style={[styles.metricItem, isActive && styles.metricActive]} onPress={onActivate} hitSlop={8}>
<Ionicons name={isActive ? 'checkmark-done-circle-outline' : 'flash-outline'} size={22} color="#E6EEF2" /> <Ionicons name={isActive ? 'checkmark-done-circle-outline' : 'flash-outline'} size={22} color="#E6EEF2" />
<Text style={[styles.metricText, { color: isActive ? '#BBF246' : '#E6EEF2' }]}>{isActive ? '已激活' : '激活'}</Text> <Text style={[styles.metricText, { color: isActive ? Colors.light.accentGreen : '#E6EEF2' }]}>{isActive ? '已激活' : '激活'}</Text>
</Pressable> </Pressable>
<Pressable style={styles.metricItem} onPress={() => { <Pressable style={styles.metricItem} onPress={() => {
Alert.alert('确认删除', '确定要删除这个训练计划吗?此操作无法撤销。', [ Alert.alert('确认删除', '确定要删除这个训练计划吗?此操作无法撤销。', [

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

View File

@@ -1,20 +1,21 @@
import { Colors } from '@/constants/Colors';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { import {
BMI_CATEGORIES, BMI_CATEGORIES,
canCalculateBMI, canCalculateBMI,
getBMIResult, getBMIResult,
type BMIResult type BMIResult
} from '@/utils/bmi'; } from '@/utils/bmi';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Dimensions, Dimensions,
Modal, Modal,
Pressable, Pressable,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import Toast from 'react-native-toast-message'; import Toast from 'react-native-toast-message';
@@ -62,7 +63,7 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
if (!canCalculate) { if (!canCalculate) {
// 缺少数据的情况 // 缺少数据的情况
return ( return (
<TouchableOpacity <TouchableOpacity
style={[styles.incompleteContent, compact && styles.compactIncompleteContent]} style={[styles.incompleteContent, compact && styles.compactIncompleteContent]}
onPress={handleShowInfoModal} onPress={handleShowInfoModal}
activeOpacity={0.8} activeOpacity={0.8}
@@ -118,8 +119,8 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
// 有完整数据的情况 // 有完整数据的情况
return ( return (
<TouchableOpacity <TouchableOpacity
style={[styles.completeContent, { backgroundColor: bmiResult?.backgroundColor }, compact && styles.compactCompleteContent]} style={styles.completeContent}
onPress={handleShowInfoModal} onPress={handleShowInfoModal}
activeOpacity={0.8} activeOpacity={0.8}
> >
@@ -193,7 +194,7 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
style={styles.modalBackdrop} style={styles.modalBackdrop}
onPress={handleHideInfoModal} onPress={handleHideInfoModal}
> >
<Pressable <Pressable
style={styles.modalContainer} style={styles.modalContainer}
onPress={(e) => e.stopPropagation()} onPress={(e) => e.stopPropagation()}
> >
@@ -234,7 +235,7 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
<View style={styles.categoriesGrid}> <View style={styles.categoriesGrid}>
{BMI_CATEGORIES.map((category, index) => { {BMI_CATEGORIES.map((category, index) => {
const colors = index === 0 ? { bg: '#FEF3C7', text: '#B45309' } : const colors = index === 0 ? { bg: '#FEF3C7', text: '#B45309' } :
index === 1 ? { bg: '#E8F5E8', text: '#2D5016' } : index === 1 ? { bg: '#E8F5E8', text: Colors.light.accentGreenDark } :
index === 2 ? { bg: '#FEF3C7', text: '#B45309' } : index === 2 ? { bg: '#FEF3C7', text: '#B45309' } :
{ bg: '#FEE2E2', text: '#B91C1C' }; { bg: '#FEE2E2', text: '#B91C1C' };
@@ -289,6 +290,7 @@ const styles = StyleSheet.create({
padding: 18, padding: 18,
marginBottom: 16, marginBottom: 16,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: '#FFFFFF',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -320,7 +322,7 @@ const styles = StyleSheet.create({
// 缺少数据时的样式 // 缺少数据时的样式
incompleteContent: { incompleteContent: {
minHeight: 120, minHeight: 80,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 22, borderRadius: 22,
padding: 18, padding: 18,
@@ -345,7 +347,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#F3F4F6', backgroundColor: '#ffffff',
borderRadius: 12, borderRadius: 12,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 16, paddingHorizontal: 16,
@@ -359,7 +361,7 @@ const styles = StyleSheet.create({
// 有完整数据时的样式 // 有完整数据时的样式
completeContent: { completeContent: {
minHeight: 120, minHeight: 80,
borderRadius: 22, borderRadius: 22,
padding: 18, padding: 18,
margin: -18, margin: -18,
@@ -398,12 +400,12 @@ const styles = StyleSheet.create({
// 紧凑模式样式 // 紧凑模式样式
compactIncompleteContent: { compactIncompleteContent: {
minHeight: 110, minHeight: 80,
padding: 14, padding: 14,
margin: -14, margin: -14,
}, },
compactCompleteContent: { compactCompleteContent: {
minHeight: 110, minHeight: 80,
padding: 14, padding: 14,
margin: -14, margin: -14,
}, },
@@ -427,7 +429,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
compactBMIValue: { compactBMIValue: {
fontSize: 28, fontSize: 24,
fontWeight: '800', fontWeight: '800',
marginBottom: 4, marginBottom: 4,
}, },
@@ -491,13 +493,13 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
// 内容区域样式 // 内容区域样式
modalContent: { modalContent: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingBottom: 24, paddingBottom: 24,
}, },
// 介绍部分 // 介绍部分
introSection: { introSection: {
marginBottom: 20, marginBottom: 20,
@@ -522,7 +524,7 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
}, },
// 分类部分 // 分类部分
categoriesSection: { categoriesSection: {
marginBottom: 18, marginBottom: 18,
@@ -567,7 +569,7 @@ const styles = StyleSheet.create({
fontWeight: '500', fontWeight: '500',
opacity: 0.9, opacity: 0.9,
}, },
// 健康提示 // 健康提示
healthTips: { healthTips: {
backgroundColor: '#F9FAFB', backgroundColor: '#F9FAFB',
@@ -593,7 +595,7 @@ const styles = StyleSheet.create({
color: '#374151', color: '#374151',
lineHeight: 18, lineHeight: 18,
}, },
// 免责声明紧凑版 // 免责声明紧凑版
disclaimerCompact: { disclaimerCompact: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -1,3 +1,4 @@
import { Colors } from '@/constants/Colors';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Animated, Easing, LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native'; import { Animated, Easing, LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native';
@@ -16,7 +17,7 @@ function ProgressBarImpl({
height = 18, height = 18,
style, style,
trackColor = '#EDEDED', trackColor = '#EDEDED',
fillColor = '#BBF246', fillColor = Colors.light.accentGreen,
animated = true, animated = true,
showLabel = true, showLabel = true,
}: ProgressBarProps) { }: ProgressBarProps) {

View File

@@ -1,3 +1,4 @@
import { Colors } from '@/constants/Colors';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import Svg, * as SvgLib from 'react-native-svg'; import Svg, * as SvgLib from 'react-native-svg';
@@ -102,7 +103,7 @@ export function RadarChart({
<Svg width={actualSize} height={actualSize}> <Svg width={actualSize} height={actualSize}>
<SvgLib.Defs> <SvgLib.Defs>
<SvgLib.LinearGradient id="radarFill" x1="0" y1="0" x2="1" y2="1"> <SvgLib.LinearGradient id="radarFill" x1="0" y1="0" x2="1" y2="1">
<SvgLib.Stop offset="0%" stopColor="#BBF246" stopOpacity={0.32} /> <SvgLib.Stop offset="0%" stopColor={Colors.light.accentGreen} stopOpacity={0.32} />
<SvgLib.Stop offset="100%" stopColor="#59C6FF" stopOpacity={0.28} /> <SvgLib.Stop offset="100%" stopColor="#59C6FF" stopOpacity={0.28} />
</SvgLib.LinearGradient> </SvgLib.LinearGradient>
</SvgLib.Defs> </SvgLib.Defs>

View File

@@ -0,0 +1,267 @@
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import {
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
interface StressAnalysisModalProps {
visible: boolean;
onClose: () => void;
hrvValue: number;
updateTime: Date;
}
export function StressAnalysisModal({ visible, onClose, hrvValue, updateTime }: StressAnalysisModalProps) {
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
// 模拟30天HRV数据
const hrvData = {
goodEvents: { percentage: 26, count: 53, range: '>80毫秒' },
energetic: { percentage: 47, count: 97, range: '43-80毫秒' },
stressed: { percentage: 27, count: 56, range: '<43毫秒' },
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<LinearGradient
colors={[colors.backgroundGradientStart, colors.backgroundGradientEnd]}
style={styles.modalContainer}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* 标题 */}
<Text style={styles.title}></Text>
{/* 最近30天HRV情况 */}
<Text style={styles.sectionTitle}>30HRV情况</Text>
{/* 彩色横条图 */}
<View style={styles.chartContainer}>
<View style={styles.colorBar}>
<LinearGradient
colors={['#10B981', '#3B82F6', '#F59E0B']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.gradientBar}
/>
</View>
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#10B981' }]} />
<Text style={styles.legendText}></Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#3B82F6' }]} />
<Text style={styles.legendText}></Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#F59E0B' }]} />
<Text style={styles.legendText}></Text>
</View>
</View>
</View>
{/* 数据统计卡片 */}
<View style={styles.statsCard}>
{/* 好事发生 & 活力满满 */}
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Text style={[styles.statTitle, { color: '#10B981' }]}></Text>
<Text style={styles.statPercentage}>{hrvData.goodEvents.percentage}%</Text>
<View style={styles.statDetails}>
<Text style={styles.statRange}> {hrvData.goodEvents.range}</Text>
</View>
<Text style={styles.statCount}>{hrvData.goodEvents.count}</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statTitle, { color: '#3B82F6' }]}></Text>
<Text style={styles.statPercentage}>{hrvData.energetic.percentage}%</Text>
<View style={styles.statDetails}>
<Text style={styles.statRange}> {hrvData.energetic.range}</Text>
</View>
<Text style={styles.statCount}>{hrvData.energetic.count}</Text>
</View>
</View>
{/* 鸭梨山大 */}
<View style={styles.statItem}>
<Text style={[styles.statTitle, { color: '#F59E0B' }]}></Text>
<Text style={styles.statPercentage}>{hrvData.stressed.percentage}%</Text>
<View style={styles.statDetails}>
<Text style={styles.statRange}> {hrvData.stressed.range}</Text>
</View>
<Text style={styles.statCount}>{hrvData.stressed.count}</Text>
</View>
</View>
</ScrollView>
{/* 底部继续按钮 */}
<View style={styles.bottomContainer}>
<TouchableOpacity style={styles.continueButton} onPress={onClose}>
<View style={styles.buttonBackground}>
<Text style={styles.buttonText}></Text>
</View>
</TouchableOpacity>
<View style={styles.homeIndicator} />
</View>
</LinearGradient>
</Modal>
);
}
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 20,
},
title: {
fontSize: 24,
fontWeight: '800',
color: '#111827',
textAlign: 'center',
marginTop: 20,
marginBottom: 32,
},
sectionTitle: {
fontSize: 22,
fontWeight: '800',
color: '#111827',
marginBottom: 20,
},
chartContainer: {
marginBottom: 32,
},
colorBar: {
height: 16,
borderRadius: 8,
overflow: 'hidden',
marginBottom: 16,
},
gradientBar: {
flex: 1,
},
legend: {
flexDirection: 'row',
justifyContent: 'space-around',
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
},
legendDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 6,
},
legendText: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
},
statsCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 32,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
statsRow: {
flexDirection: 'row',
gap: 20,
marginBottom: 24,
},
statItem: {
flex: 1,
},
statTitle: {
fontSize: 16,
fontWeight: '700',
marginBottom: 8,
},
statPercentage: {
fontSize: 36,
fontWeight: '800',
color: '#111827',
marginBottom: 4,
},
statDetails: {
marginBottom: 4,
},
statRange: {
fontSize: 14,
fontWeight: '600',
color: '#DC2626',
backgroundColor: '#FEE2E2',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
alignSelf: 'flex-start',
},
statCount: {
fontSize: 16,
fontWeight: '600',
color: '#6B7280',
},
bottomContainer: {
paddingHorizontal: 20,
paddingBottom: 34,
},
continueButton: {
borderRadius: 25,
overflow: 'hidden',
marginBottom: 8,
},
buttonGradient: {
paddingVertical: 18,
alignItems: 'center',
justifyContent: 'center',
},
buttonBackground: {
backgroundColor: Colors.light.accentGreen, // 应用主色调
paddingVertical: 18,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
fontSize: 18,
fontWeight: '700',
color: '#192126', // 主色调上的文字颜色
},
homeIndicator: {
width: 134,
height: 5,
backgroundColor: '#000',
borderRadius: 3,
alignSelf: 'center',
},
});

View File

@@ -1,25 +1,27 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface StressMeterProps { interface StressMeterProps {
value: number; value: number | null;
updateTime?: Date; updateTime?: Date;
style?: any; style?: any;
onPress?: () => void;
} }
export function StressMeter({ value, updateTime, style }: StressMeterProps) { export function StressMeter({ value, updateTime, style, onPress }: StressMeterProps) {
// 将HRV值转换为压力指数0-100
// 计算进度条位置0-100% // HRV值范围30-110ms映射到压力指数100-0
// HRV值范围30-110ms对应进度条0-100% // HRV值越高压力越小HRV值越低压力越大
const progressPercentage = Math.min(100, Math.max(0, ((value - 30) / 80) * 100)); const stressIndex = value ? Math.round(Math.min(100, Math.max(0, 100 - ((value - 30) / 80) * 100))) : null;
// 根据压力指数计算状态
// 根据HRV值计算状态 const getStressStatus = () => {
const getHrvStatus = () => { if (stressIndex === null) {
if (value >= 70) { return '未知';
} else if (stressIndex <= 30) {
return '放松'; return '放松';
} else if (value >= 50) { } else if (stressIndex <= 70) {
return '正常'; return '正常';
} else { } else {
return '紧张'; return '紧张';
@@ -28,13 +30,14 @@ export function StressMeter({ value, updateTime, style }: StressMeterProps) {
// 根据状态获取表情 // 根据状态获取表情
const getStatusEmoji = () => { const getStatusEmoji = () => {
// 当HRV值为0时不展示表情 // 当HRV值为null或0时不展示表情
if (value === 0) { if (value === null || value === 0) {
return ''; return '';
} }
const status = getHrvStatus(); const status = getStressStatus();
switch (status) { switch (status) {
case '未知': return '';
case '放松': return '😌'; case '放松': return '😌';
case '正常': return '😊'; case '正常': return '😊';
case '紧张': return '😰'; case '紧张': return '😰';
@@ -42,24 +45,18 @@ export function StressMeter({ value, updateTime, style }: StressMeterProps) {
} }
}; };
// 格式化更新时间 // 计算进度条位置0-100%
const formatUpdateTime = (date?: Date) => { // 压力指数越高,进度条越满
if (!date) return ''; const progressPercentage = stressIndex === null ? 0 : stressIndex;
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
return ( return (
<View style={[styles.container, style]}> <TouchableOpacity
{/* 渐变背景 */} style={[styles.container, style]}
<LinearGradient onPress={onPress}
colors={['#F8F9FF', '#F0F4FF']} activeOpacity={0.8}
start={{ x: 0, y: 0 }} >
end={{ x: 1, y: 1 }}
style={styles.gradientBackground}
/>
{/* 头部区域 */} {/* 头部区域 */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.leftSection}> <View style={styles.leftSection}>
@@ -73,30 +70,32 @@ export function StressMeter({ value, updateTime, style }: StressMeterProps) {
{/* 数值显示区域 */} {/* 数值显示区域 */}
<View style={styles.valueSection}> <View style={styles.valueSection}>
<Text style={styles.value}>{value}</Text> <Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
<Text style={styles.unit}></Text> <Text style={styles.unit}></Text>
</View> </View>
{/* 进度条区域 */} {/* 进度条区域 */}
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
<View style={styles.progressTrack}> <View style={styles.progressTrack}>
{/* 渐变背景进度条 */} {/* 渐变背景进度条 */}
<LinearGradient <View style={[styles.progressBar, { width: `${progressPercentage}%` }]}>
colors={['#FFD700', '#87CEEB', '#98FB98']} <LinearGradient
start={{ x: 0, y: 0 }} colors={['#10B981', '#FCD34D', '#F97316']}
end={{ x: 1, y: 0 }} start={{ x: 0, y: 0 }}
style={styles.gradientTrack} end={{ x: 1, y: 0 }}
/> style={styles.gradientBar}
/>
</View>
{/* 白色圆形指示器 */} {/* 白色圆形指示器 */}
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} /> <View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} />
</View> </View>
</View> </View>
{/* 更新时间 */} {/* 更新时间
{updateTime && ( {updateTime && (
<Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text> <Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text>
)} )} */}
</View> </TouchableOpacity>
); );
} }
@@ -181,7 +180,12 @@ const styles = StyleSheet.create({
position: 'relative', position: 'relative',
overflow: 'visible', overflow: 'visible',
}, },
gradientTrack: { progressBar: {
height: '100%',
borderRadius: 4,
overflow: 'hidden',
},
gradientBar: {
height: '100%', height: '100%',
borderRadius: 4, borderRadius: 4,
}, },

View File

@@ -1,3 +1,4 @@
import { Colors } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
@@ -20,11 +21,6 @@ const CHART_WIDTH = CARD_WIDTH - 36; // 减去卡片内边距
const CHART_HEIGHT = 100; const CHART_HEIGHT = 100;
const PADDING = 10; const PADDING = 10;
type WeightHistoryItem = {
weight: string;
source: string;
createdAt: string;
};
export function WeightHistoryCard() { export function WeightHistoryCard() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -177,7 +173,7 @@ export function WeightHistoryCard() {
<Ionicons <Ionicons
name={showChart ? "chevron-up" : "chevron-down"} name={showChart ? "chevron-up" : "chevron-down"}
size={16} size={16}
color="#BBF246" color={Colors.light.primary}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@@ -185,7 +181,7 @@ export function WeightHistoryCard() {
onPress={navigateToCoach} onPress={navigateToCoach}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Ionicons name="add" size={16} color="#BBF246" /> <Ionicons name="add" size={16} color={Colors.light.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@@ -209,14 +205,6 @@ export function WeightHistoryCard() {
</Text> </Text>
</View> </View>
</View> </View>
<TouchableOpacity
style={styles.viewTrendButton}
onPress={() => setShowChart(true)}
activeOpacity={0.8}
>
<Ionicons name="trending-up" size={14} color="#BBF246" />
<Text style={styles.viewTrendText}></Text>
</TouchableOpacity>
</View> </View>
)} )}
@@ -240,7 +228,7 @@ export function WeightHistoryCard() {
{/* 折线 */} {/* 折线 */}
<Path <Path
d={singlePointPath} d={singlePointPath}
stroke="#BBF246" stroke={Colors.light.accentGreen}
strokeWidth={3} strokeWidth={3}
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
@@ -259,7 +247,7 @@ export function WeightHistoryCard() {
cx={point.x} cx={point.x}
cy={point.y} cy={point.y}
r={isLastPoint ? 6 : 4} r={isLastPoint ? 6 : 4}
fill="#BBF246" fill={Colors.light.accentGreen}
stroke="#FFFFFF" stroke="#FFFFFF"
strokeWidth={2} strokeWidth={2}
/> />
@@ -271,7 +259,7 @@ export function WeightHistoryCard() {
cy={point.y - 15} cy={point.y - 15}
r={10} r={10}
fill="rgba(255,255,255,0.9)" fill="rgba(255,255,255,0.9)"
stroke="#BBF246" stroke={Colors.light.accentGreen}
strokeWidth={1} strokeWidth={1}
/> />
<SvgText <SvgText
@@ -343,7 +331,6 @@ const styles = StyleSheet.create({
width: 30, width: 30,
height: 30, height: 30,
borderRadius: 8, borderRadius: 8,
backgroundColor: '#F0F8E0',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 10, marginRight: 10,
@@ -363,7 +350,6 @@ const styles = StyleSheet.create({
width: 28, width: 28,
height: 28, height: 28,
borderRadius: 14, borderRadius: 14,
backgroundColor: '#F0F8E0',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -371,7 +357,6 @@ const styles = StyleSheet.create({
width: 28, width: 28,
height: 28, height: 28,
borderRadius: 14, borderRadius: 14,
backgroundColor: '#F0F8E0',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -394,7 +379,7 @@ const styles = StyleSheet.create({
recordButton: { recordButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#BBF246', backgroundColor: Colors.light.accentGreen,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 10, paddingVertical: 10,
borderRadius: 20, borderRadius: 20,
@@ -437,12 +422,11 @@ const styles = StyleSheet.create({
marginTop: 8, marginTop: 8,
}, },
summaryInfo: { summaryInfo: {
paddingVertical: 12,
}, },
summaryRow: { summaryRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
marginBottom: 12, marginBottom: 6,
}, },
summaryItem: { summaryItem: {
alignItems: 'center', alignItems: 'center',
@@ -457,20 +441,4 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
color: '#192126', color: '#192126',
}, },
viewTrendButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F0F8E0',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 16,
gap: 6,
alignSelf: 'center',
},
viewTrendText: {
fontSize: 13,
fontWeight: '600',
color: '#192126',
},
}); });

View File

@@ -42,7 +42,7 @@ export function HeaderBar({
]} ]}
> >
{onBack ? ( {onBack ? (
<TouchableOpacity accessibilityRole="button" onPress={onBack} style={[styles.backButton, { backgroundColor: 'rgba(187,242,70,0.2)' }]}> <TouchableOpacity accessibilityRole="button" onPress={onBack} style={[styles.backButton, { backgroundColor: `${Colors.light.accentGreen}33` }]}>
<Ionicons name="chevron-back" size={20} color={theme.onPrimary} /> <Ionicons name="chevron-back" size={20} color={theme.onPrimary} />
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (

View File

@@ -6,7 +6,7 @@
// 原子调色板(与设计图一致) // 原子调色板(与设计图一致)
export const palette = { export const palette = {
// Primary // Primary
primary: '#BBF246', primary: '#87CEEB',
ink: '#192126', ink: '#192126',
// Secondary / Neutrals // Secondary / Neutrals
@@ -18,10 +18,11 @@ export const palette = {
purple: '#A48AED', purple: '#A48AED',
red: '#ED4747', red: '#ED4747',
orange: '#FCC46F', orange: '#FCC46F',
blue: '#95CCE3', blue: '#87CEEB', // 更贴近logo背景的天空蓝
blueSecondary: '#4682B4', // 钢蓝色,用于选中状态
} as const; } as const;
const primaryColor = palette.primary; // 应用主题色 const primaryColor = palette.blue; // 应用主题色
const tintColorLight = primaryColor; const tintColorLight = primaryColor;
const tintColorDark = '#FFFFFF'; const tintColorDark = '#FFFFFF';
@@ -35,6 +36,8 @@ export const Colors = {
surface: '#FFFFFF', surface: '#FFFFFF',
card: '#FFFFFF', card: '#FFFFFF',
buttonBackground: palette.blue,
// 品牌与可交互主色 // 品牌与可交互主色
tint: tintColorLight, tint: tintColorLight,
primary: primaryColor, primary: primaryColor,
@@ -51,6 +54,12 @@ export const Colors = {
danger: palette.red, danger: palette.red,
info: palette.blue, info: palette.blue,
accentPurple: palette.purple, accentPurple: palette.purple,
accentGreen: palette.blue,
accentGreenDark: palette.blueSecondary, // 深绿色,用于文本和强调
// 日期选择器主题色
datePickerNormal: palette.blue,
datePickerSelected: palette.blueSecondary,
// 结构色 // 结构色
border: palette.neutral100 + '33', // 20% 透明度 border: palette.neutral100 + '33', // 20% 透明度
@@ -64,10 +73,14 @@ export const Colors = {
tabBarActiveBackground: primaryColor, // tab 激活时的背景色 tabBarActiveBackground: primaryColor, // tab 激活时的背景色
// 页面氛围与装饰(新) // 页面氛围与装饰(新)
pageBackgroundEmphasis: '#F9FBF2', pageBackgroundEmphasis: '#F0F8FF', // 淡蓝色背景强调
heroSurfaceTint: 'rgba(187,242,70,0.18)', heroSurfaceTint: 'rgba(135,206,235,0.18)', // 蓝色调的表面色彩
ornamentPrimary: 'rgba(187,242,70,0.22)', ornamentPrimary: 'rgba(187,242,70,0.22)',
ornamentAccent: 'rgba(164,138,237,0.16)', ornamentAccent: 'rgba(164,138,237,0.16)',
// 统一背景渐变色(基于 StressAnalysisModal 的设计)
backgroundGradientStart: '#d3e9eeff', // 浅蓝色起始 (与logo背景匹配)
backgroundGradientEnd: '#ffffff', // 白色结束
}, },
dark: { dark: {
// 基础文本/背景 // 基础文本/背景
@@ -94,6 +107,11 @@ export const Colors = {
danger: palette.red, danger: palette.red,
info: palette.blue, info: palette.blue,
accentPurple: palette.purple, accentPurple: palette.purple,
accentGreenDark: '#2D5016', // 深绿色,用于文本和强调
// 日期选择器主题色
datePickerNormal: palette.blue,
datePickerSelected: palette.blueSecondary,
// 结构色 // 结构色
border: '#2A2F32', border: '#2A2F32',
@@ -111,5 +129,9 @@ export const Colors = {
heroSurfaceTint: 'rgba(187,242,70,0.12)', heroSurfaceTint: 'rgba(187,242,70,0.12)',
ornamentPrimary: 'rgba(187,242,70,0.18)', ornamentPrimary: 'rgba(187,242,70,0.18)',
ornamentAccent: 'rgba(164,138,237,0.14)', ornamentAccent: 'rgba(164,138,237,0.14)',
// 统一背景渐变色(深色模式)
backgroundGradientStart: '#0A0B0C', // 深黑色起始
backgroundGradientEnd: '#151718', // 背景色结束
}, },
} as const; } as const;

View File

@@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "未命名项目 (1).jpeg", "filename" : "Sealife.jpeg",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -1,17 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "未命名项目 (1).jpeg", "filename" : "Sealife.jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "未命名项目.jpeg", "filename" : "未命名项目 (5).jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "未命名项目 1.jpeg", "filename" : "未命名项目 (5) 1.jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

View File

@@ -1,17 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "未命名项目 (1).jpeg", "filename" : "Sealife.jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "未命名项目.jpeg", "filename" : "未命名项目 (5).jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "未命名项目 1.jpeg", "filename" : "未命名项目 (5) 1.jpeg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

View File

@@ -57,7 +57,7 @@ const BMI_COLORS = {
backgroundColor: '#FFF4E6', // 浅橙色背景 backgroundColor: '#FFF4E6', // 浅橙色背景
}, },
normal: { normal: {
color: '#2D5016', // 深绿色文字 color: '#2D5016', // 深绿色文字,保持硬编码因为这是工具函数的固定配色
backgroundColor: '#E8F5E8', // 浅绿色背景 backgroundColor: '#E8F5E8', // 浅绿色背景
}, },
overweight: { overweight: {