feat: Implement Food Camera Screen and Floating Food Overlay

- Added FoodCameraScreen for capturing food images with camera functionality.
- Integrated image picker for selecting images from the gallery.
- Created FloatingFoodOverlay for quick access to food library and scanning options.
- Updated NutritionRadarCard to utilize FloatingFoodOverlay for adding food.
- Enhanced ExploreScreen layout and styles for better user experience.
- Removed unused SafeAreaView from ExploreScreen.
- Updated profile edit screen to remove unnecessary state variables.
- Updated avatar image source in profile edit screen.
- Added ExpoCamera dependency for camera functionalities.
This commit is contained in:
richarjiang
2025-09-03 19:17:26 +08:00
parent 45f8415a38
commit 02883869fe
10 changed files with 931 additions and 233 deletions

View File

@@ -10,11 +10,12 @@ import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useState } from 'react';
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg';
export default function PersonalScreen() {
const dispatch = useAppDispatch();
@@ -24,7 +25,6 @@ export default function PersonalScreen() {
// 推送通知相关
const {
permissionStatus,
requestPermission,
sendNotification,
} = useNotifications();
@@ -133,35 +133,37 @@ export default function PersonalScreen() {
// 用户信息头部
const UserHeader = () => (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.cardContainer}>
<View style={styles.userInfoContainer}>
<View style={styles.avatarContainer}>
<Image
source={userProfile.avatar || DEFAULT_AVATAR_URL}
style={styles.avatar}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
</View>
<View style={styles.userDetails}>
<Text style={styles.userName}>{displayName}</Text>
</View>
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={styles.editButtonText}></Text>
</TouchableOpacity>
<View style={[styles.sectionContainer, {
marginBottom: 0
}]}>
<View style={[styles.userInfoContainer,]}>
<View style={styles.avatarContainer}>
<Image
source={userProfile.avatar || DEFAULT_AVATAR_URL}
style={styles.avatar}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
</View>
<View style={styles.userDetails}>
<Text style={styles.userName}>{displayName}</Text>
</View>
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
<Text style={styles.editButtonText}></Text>
</TouchableOpacity>
</View>
</View>
);
// 数据统计部分
const StatsSection = () => (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.cardContainer}>
<View style={[styles.cardContainer, {
backgroundColor: 'unset'
}]}>
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{formatHeight()}</Text>
@@ -224,16 +226,6 @@ export default function PersonalScreen() {
// 菜单项配置
const menuSections = [
{
title: '账户',
items: [
{
icon: 'flag-outline' as const,
title: '目标管理',
onPress: () => pushIfAuthedElseLogin('/profile/goals'),
},
],
},
{
title: '通知',
items: [
@@ -284,30 +276,45 @@ export default function PersonalScreen() {
return (
<View style={styles.container}>
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
<SafeAreaView style={styles.safeArea}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false}
>
<UserHeader />
<StatsSection />
<View style={styles.fishRecordContainer}>
{/* <Image
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: bottomPadding,
paddingHorizontal: 16,
}}
showsVerticalScrollIndicator={false}
>
<UserHeader />
<StatsSection />
<View style={styles.fishRecordContainer}>
{/* <Image
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-profile-fish.png' }}
contentFit="cover"
style={{ width: 16, height: 16, marginLeft: 6 }}
transition={200}
cachePolicy="memory-disk"
/> */}
<Text style={styles.fishRecordText}></Text>
</View>
<ActivityHeatMap />
{menuSections.map((section, index) => (
<MenuSection key={index} title={section.title} items={section.items} />
))}
</ScrollView>
</SafeAreaView>
<Text style={styles.fishRecordText}></Text>
</View>
<ActivityHeatMap />
{menuSections.map((section, index) => (
<MenuSection key={index} title={section.title} items={section.items} />
))}
</ScrollView>
</View>
);
}
@@ -315,15 +322,36 @@ export default function PersonalScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F0F9FF',
},
safeArea: {
flex: 1,
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#0EA5E9',
opacity: 0.1,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
scrollView: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 16,
},
// 部分容器
sectionContainer: {
@@ -453,3 +481,4 @@ const styles = StyleSheet.create({
marginLeft: 4,
},
});

View File

@@ -34,7 +34,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
AppState,
Image,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
@@ -366,7 +365,7 @@ export default function ExploreScreen() {
// 使用 lodash debounce 防抖的加载所有数据方法
const debouncedLoadAllData = React.useMemo(
() => debounce(executeLoadAllData, 300), // 300ms 防抖延迟
() => debounce(executeLoadAllData, 500), // 500ms 防抖延迟
[executeLoadAllData]
);
@@ -573,7 +572,7 @@ export default function ExploreScreen() {
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f8f3faff', '#b3f6f8ff', '#bcf1f7ff', '#d3edf8ff']}
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
@@ -583,157 +582,159 @@ export default function ExploreScreen() {
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<SafeAreaView style={styles.safeArea}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false}
>
{/* 顶部信息栏 */}
<View style={styles.headerContainer}>
<View style={styles.headerContent}>
{/* 左边logo */}
<Image
source={require('@/assets/images/Sealife.jpeg')}
style={styles.logoImage}
resizeMode="cover"
<ScrollView
style={styles.scrollView}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: bottomPadding,
paddingHorizontal: 20
}}
showsVerticalScrollIndicator={false}
>
{/* 顶部信息栏 */}
<View style={styles.headerContainer}>
<View style={styles.headerContent}>
{/* 左边logo */}
<Image
source={require('@/assets/images/Sealife.jpeg')}
style={styles.logoImage}
resizeMode="cover"
/>
{/* 右边文字区域 */}
<View style={styles.headerTextContainer}>
<Text style={styles.headerTitle}></Text>
</View>
</View>
</View>
{/* 日期选择器 */}
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={onSelectDate}
showMonthTitle={false}
disableFutureDates={true}
/>
{/* 营养摄入雷达图卡片 */}
<NutritionRadarCard
nutritionSummary={nutritionSummary}
nutritionGoals={nutritionGoals}
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
basalMetabolism={basalMetabolism || 0}
activeCalories={activeCalories || 0}
resetToken={animToken}
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
console.log('选择餐次:', mealType);
// 这里可以导航到营养记录页面
pushIfAuthedElseLogin('/nutrition/records');
}}
/>
<WeightHistoryCard />
{/* 真正瀑布流布局 */}
<View style={styles.masonryContainer}>
{/* 左列 */}
<View style={styles.masonryColumn}>
{/* 心情卡片 */}
<FloatingCard style={styles.masonryCard} delay={1500}>
<MoodCard
moodCheckin={currentMoodCheckin}
onPress={() => pushIfAuthedElseLogin('/mood/calendar')}
isLoading={isMoodLoading}
/>
</FloatingCard>
{/* 右边文字区域 */}
<View style={styles.headerTextContainer}>
<Text style={styles.headerTitle}></Text>
<FloatingCard style={styles.masonryCard}>
<StepsCard
stepCount={stepCount}
stepGoal={stepGoal}
hourlySteps={hourlySteps}
style={styles.stepsCardOverride}
onPress={() => pushIfAuthedElseLogin('/steps/detail')}
/>
</FloatingCard>
{/* 饮水记录卡片 */}
<FloatingCard style={styles.masonryCard} delay={500}>
<WaterIntakeCard
selectedDate={currentSelectedDateString}
style={styles.waterCardOverride}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard} delay={0}>
<StressMeter
value={hrvValue}
updateTime={hrvUpdateTime}
hrvValue={hrvValue}
/>
</FloatingCard>
{/* 心率卡片 */}
<FloatingCard style={styles.masonryCard} delay={2000}>
<HeartRateCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
heartRate={heartRate}
/>
</FloatingCard>
</View>
{/* 右列 */}
<View style={styles.masonryColumn}>
<FloatingCard style={styles.masonryCard} delay={250}>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
</View>
{sleepDuration != null ? (
<Text style={styles.sleepValue}>
{Math.floor(sleepDuration / 60)}{Math.floor(sleepDuration % 60)}
</Text>
) : (
<Text style={styles.sleepValue}></Text>
)}
</FloatingCard>
{/* 基础代谢卡片 */}
<FloatingCard style={styles.masonryCard} delay={1250}>
<BasalMetabolismCard
value={basalMetabolism}
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
/>
</FloatingCard>
{/* 血氧饱和度卡片 */}
<FloatingCard style={styles.masonryCard} delay={1750}>
<OxygenSaturationCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
oxygenSaturation={oxygenSaturation}
/>
</FloatingCard>
</View>
{/* 日期选择器 */}
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={onSelectDate}
showMonthTitle={false}
disableFutureDates={true}
/>
{/* 营养摄入雷达图卡片 */}
<NutritionRadarCard
nutritionSummary={nutritionSummary}
nutritionGoals={nutritionGoals}
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
basalMetabolism={basalMetabolism || 0}
activeCalories={activeCalories || 0}
resetToken={animToken}
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
console.log('选择餐次:', mealType);
// 这里可以导航到营养记录页面
pushIfAuthedElseLogin('/nutrition/records');
}}
/>
<WeightHistoryCard />
{/* 真正瀑布流布局 */}
<View style={styles.masonryContainer}>
{/* 左列 */}
<View style={styles.masonryColumn}>
{/* 心情卡片 */}
<FloatingCard style={styles.masonryCard} delay={1500}>
<MoodCard
moodCheckin={currentMoodCheckin}
onPress={() => pushIfAuthedElseLogin('/mood/calendar')}
isLoading={isMoodLoading}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard}>
<StepsCard
stepCount={stepCount}
stepGoal={stepGoal}
hourlySteps={hourlySteps}
style={styles.stepsCardOverride}
onPress={() => pushIfAuthedElseLogin('/steps/detail')}
/>
</FloatingCard>
{/* 饮水记录卡片 */}
<FloatingCard style={styles.masonryCard} delay={500}>
<WaterIntakeCard
selectedDate={currentSelectedDateString}
style={styles.waterCardOverride}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard} delay={0}>
<StressMeter
value={hrvValue}
updateTime={hrvUpdateTime}
hrvValue={hrvValue}
/>
</FloatingCard>
{/* 心率卡片 */}
<FloatingCard style={styles.masonryCard} delay={2000}>
<HeartRateCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
heartRate={heartRate}
/>
</FloatingCard>
</View>
{/* 右列 */}
<View style={styles.masonryColumn}>
<FloatingCard style={styles.masonryCard} delay={250}>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
{sleepDuration != null ? (
<Text style={styles.sleepValue}>
{Math.floor(sleepDuration / 60)}{Math.floor(sleepDuration % 60)}
</Text>
) : (
<Text style={styles.sleepValue}></Text>
)}
</FloatingCard>
{/* 基础代谢卡片 */}
<FloatingCard style={styles.masonryCard} delay={1250}>
<BasalMetabolismCard
value={basalMetabolism}
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
/>
</FloatingCard>
{/* 血氧饱和度卡片 */}
<FloatingCard style={styles.masonryCard} delay={1750}>
<OxygenSaturationCard
resetToken={animToken}
style={styles.basalMetabolismCardOverride}
oxygenSaturation={oxygenSaturation}
/>
</FloatingCard>
</View>
</View>
</ScrollView>
</SafeAreaView>
</View>
</ScrollView>
</View>
);
}
@@ -771,12 +772,9 @@ const styles = StyleSheet.create({
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
safeArea: {
flex: 1,
},
scrollView: {
flex: 1,
paddingHorizontal: 20,
},
headerContainer: {
marginBottom: 10,