feat(ui): 统一应用主题色为天空蓝并优化渐变背景
将应用主色调从 '#BBF246' 更改为 '#87CEEB'(天空蓝),并更新所有相关组件和页面中的颜色引用。同时为多个页面添加统一的渐变背景,提升视觉效果和用户体验。新增压力分析模态框组件,并优化压力计组件的交互与显示逻辑。更新应用图标和启动图资源。
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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('确认删除', '确定要删除这个训练计划吗?此操作无法撤销。', [
|
||||||
|
|||||||
BIN
assets/images/Sealife@2x.jpeg
Normal file
|
After Width: | Height: | Size: 744 KiB |
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
267
components/StressAnalysisModal.tsx
Normal 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}>最近30天HRV情况</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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "未命名项目 (1).jpeg",
|
"filename" : "Sealife.jpeg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 304 KiB |
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/Sealife.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 304 KiB |
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/未命名项目 (5) 1.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/未命名项目 (5).jpeg
vendored
Normal file
|
After Width: | Height: | Size: 744 KiB |
|
Before Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 745 KiB |
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/Sealife.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 304 KiB |
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/未命名项目 (5) 1.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
ios/digitalpilates/Images.xcassets/logo.imageset/未命名项目 (5).jpeg
vendored
Normal file
|
After Width: | Height: | Size: 744 KiB |
|
Before Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 745 KiB |
@@ -57,7 +57,7 @@ const BMI_COLORS = {
|
|||||||
backgroundColor: '#FFF4E6', // 浅橙色背景
|
backgroundColor: '#FFF4E6', // 浅橙色背景
|
||||||
},
|
},
|
||||||
normal: {
|
normal: {
|
||||||
color: '#2D5016', // 深绿色文字
|
color: '#2D5016', // 深绿色文字,保持硬编码因为这是工具函数的固定配色
|
||||||
backgroundColor: '#E8F5E8', // 浅绿色背景
|
backgroundColor: '#E8F5E8', // 浅绿色背景
|
||||||
},
|
},
|
||||||
overweight: {
|
overweight: {
|
||||||
|
|||||||