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:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
436
app/food-camera.tsx
Normal file
436
app/food-camera.tsx
Normal file
@@ -0,0 +1,436 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||
|
||||
export default function FoodCameraScreen() {
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams<{ mealType?: string }>();
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
|
||||
const [currentMealType, setCurrentMealType] = useState<MealType>(
|
||||
(params.mealType as MealType) || 'dinner'
|
||||
);
|
||||
const [facing, setFacing] = useState<CameraType>('back');
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
|
||||
// 餐次选择选项
|
||||
const mealOptions = [
|
||||
{ key: 'breakfast' as const, label: '早餐', icon: '☀️' },
|
||||
{ key: 'lunch' as const, label: '午餐', icon: '🌤️' },
|
||||
{ key: 'dinner' as const, label: '晚餐', icon: '🌙' },
|
||||
{ key: 'snack' as const, label: '加餐', icon: '🍎' },
|
||||
];
|
||||
|
||||
if (!permission) {
|
||||
// 权限仍在加载中
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<HeaderBar
|
||||
title="食物拍摄"
|
||||
onBack={() => router.back()}
|
||||
transparent={true}
|
||||
/>
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>正在加载相机...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission.granted) {
|
||||
// 没有相机权限
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<HeaderBar
|
||||
title="食物拍摄"
|
||||
onBack={() => router.back()}
|
||||
transparent={true}
|
||||
/>
|
||||
<View style={styles.permissionContainer}>
|
||||
<Ionicons name="camera-outline" size={64} color="#999" />
|
||||
<Text style={styles.permissionTitle}>需要相机权限</Text>
|
||||
<Text style={styles.permissionText}>
|
||||
为了拍摄食物,需要访问您的相机
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.permissionButton}
|
||||
onPress={requestPermission}
|
||||
>
|
||||
<Text style={styles.permissionButtonText}>授权访问</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// 切换相机前后摄像头
|
||||
function toggleCameraFacing() {
|
||||
setFacing(current => (current === 'back' ? 'front' : 'back'));
|
||||
}
|
||||
|
||||
// 拍摄照片
|
||||
const takePicture = async () => {
|
||||
if (cameraRef.current) {
|
||||
try {
|
||||
const photo = await cameraRef.current.takePictureAsync({
|
||||
quality: 0.8,
|
||||
base64: false,
|
||||
});
|
||||
|
||||
if (photo) {
|
||||
// TODO: 处理拍摄的照片,可以传递到下一个页面进行食物识别
|
||||
console.log('照片拍摄成功:', photo.uri);
|
||||
Alert.alert('拍摄成功', '照片已保存,后续会添加食物识别功能');
|
||||
// router.push(`/food-recognition?imageUri=${photo.uri}&mealType=${currentMealType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('拍照失败:', error);
|
||||
Alert.alert('拍照失败', '请重试');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 从相册选择照片
|
||||
const pickImageFromGallery = async () => {
|
||||
try {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
allowsEditing: true,
|
||||
aspect: [4, 3],
|
||||
quality: 0.8,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
const imageUri = result.assets[0].uri;
|
||||
console.log('从相册选择的照片:', imageUri);
|
||||
Alert.alert('选择成功', '照片已选择,后续会添加食物识别功能');
|
||||
// router.push(`/food-recognition?imageUri=${imageUri}&mealType=${currentMealType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择照片失败:', error);
|
||||
Alert.alert('选择失败', '请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// AR功能(暂时显示提示)
|
||||
const handleARPress = () => {
|
||||
Alert.alert('AR功能', 'AR食物识别功能即将推出');
|
||||
};
|
||||
|
||||
// 餐次选择
|
||||
const handleMealTypeChange = (mealType: MealType) => {
|
||||
setCurrentMealType(mealType);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
|
||||
{/* 相机视图 */}
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={styles.camera}
|
||||
facing={facing}
|
||||
ratio="16:9"
|
||||
>
|
||||
{/* 头部导航 */}
|
||||
<HeaderBar
|
||||
title=""
|
||||
onBack={() => router.back()}
|
||||
transparent={true}
|
||||
backColor={'#fff'}
|
||||
/>
|
||||
|
||||
{/* 取景框和提示文本 */}
|
||||
<View style={styles.overlayContainer}>
|
||||
<Text style={styles.hintText}>确保食物在辅助框内</Text>
|
||||
|
||||
{/* 取景框 */}
|
||||
<View style={styles.viewfinderContainer}>
|
||||
<View style={styles.viewfinder}>
|
||||
{/* 四个角的装饰 */}
|
||||
<View style={[styles.corner, styles.topLeft]} />
|
||||
<View style={[styles.corner, styles.topRight]} />
|
||||
<View style={[styles.corner, styles.bottomLeft]} />
|
||||
<View style={[styles.corner, styles.bottomRight]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 餐次选择器 */}
|
||||
<View style={styles.mealTypeContainer}>
|
||||
{mealOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option.key}
|
||||
style={[
|
||||
styles.mealTypeButton,
|
||||
currentMealType === option.key && styles.mealTypeButtonActive
|
||||
]}
|
||||
onPress={() => handleMealTypeChange(option.key)}
|
||||
>
|
||||
<Text style={styles.mealTypeIcon}>{option.icon}</Text>
|
||||
<Text style={[
|
||||
styles.mealTypeText,
|
||||
currentMealType === option.key && styles.mealTypeTextActive
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部控制栏 */}
|
||||
<SafeAreaView style={styles.bottomContainer}>
|
||||
<View style={styles.controlsContainer}>
|
||||
{/* 相册按钮 */}
|
||||
{/* <TouchableOpacity style={styles.controlButton} onPress={pickImageFromGallery}>
|
||||
<View style={styles.albumButton}>
|
||||
<Ionicons name="images-outline" size={24} color="#FFF" />
|
||||
</View>
|
||||
<Text style={styles.controlButtonText}>相册</Text>
|
||||
</TouchableOpacity> */}
|
||||
|
||||
{/* 拍照按钮 */}
|
||||
<TouchableOpacity style={styles.captureButton} onPress={takePicture}>
|
||||
<View style={styles.captureButtonInner} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* AR按钮 */}
|
||||
{/* <TouchableOpacity style={styles.controlButton} onPress={handleARPress}>
|
||||
<View style={styles.arButton}>
|
||||
<Text style={styles.arButtonText}>AR</Text>
|
||||
</View>
|
||||
<Text style={styles.controlButtonText}>AR</Text>
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</CameraView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
},
|
||||
permissionContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
permissionTitle: {
|
||||
color: '#FFF',
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
permissionText: {
|
||||
color: '#CCC',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
lineHeight: 22,
|
||||
},
|
||||
permissionButton: {
|
||||
backgroundColor: Colors.light.primary,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 24,
|
||||
},
|
||||
permissionButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
header: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
},
|
||||
overlayContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: 100,
|
||||
},
|
||||
hintText: {
|
||||
color: '#FFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 30,
|
||||
textAlign: 'center',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 3,
|
||||
},
|
||||
viewfinderContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
viewfinder: {
|
||||
width: 280,
|
||||
height: 280,
|
||||
position: 'relative',
|
||||
},
|
||||
corner: {
|
||||
position: 'absolute',
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 3,
|
||||
},
|
||||
topLeft: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderRightWidth: 0,
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
topRight: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
bottomLeft: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
borderRightWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
bottomRight: {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
mealTypeContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
mealTypeButton: {
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
minWidth: 70,
|
||||
},
|
||||
mealTypeButtonActive: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
},
|
||||
mealTypeIcon: {
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
mealTypeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
mealTypeTextActive: {
|
||||
color: '#333',
|
||||
},
|
||||
bottomContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
controlsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
controlButton: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
controlButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
fontWeight: '500',
|
||||
},
|
||||
albumButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
},
|
||||
captureButton: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: '#FFF',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 4,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
captureButtonInner: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#FFF',
|
||||
borderWidth: 2,
|
||||
borderColor: '#333',
|
||||
},
|
||||
arButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
},
|
||||
arButtonText: {
|
||||
color: '#FFF',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
@@ -70,9 +70,6 @@ export default function EditProfileScreen() {
|
||||
activityLevel: undefined,
|
||||
});
|
||||
|
||||
const [weightInput, setWeightInput] = useState<string>('');
|
||||
const [heightInput, setHeightInput] = useState<string>('');
|
||||
|
||||
// 出生日期选择器
|
||||
const [datePickerVisible, setDatePickerVisible] = useState(false);
|
||||
const [pickerDate, setPickerDate] = useState<Date>(new Date());
|
||||
@@ -115,8 +112,7 @@ export default function EditProfileScreen() {
|
||||
}
|
||||
console.log('loadLocalProfile', next);
|
||||
setProfile((prev) => ({ ...next, avatarUri: prev.avatarUri ?? next.avatarUri ?? null }));
|
||||
setWeightInput(next.weight != null ? String(round(next.weight, 1)) : '');
|
||||
setHeightInput(next.height != null ? String(Math.round(next.height)) : '');
|
||||
|
||||
} catch (e) {
|
||||
console.warn('读取资料失败', e);
|
||||
}
|
||||
@@ -276,7 +272,7 @@ export default function EditProfileScreen() {
|
||||
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 32 }}>
|
||||
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary} disabled={uploading}>
|
||||
<View style={styles.avatarCircle}>
|
||||
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg' }} style={styles.avatarImage} />
|
||||
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg' }} style={styles.avatarImage} />
|
||||
<View style={styles.avatarOverlay}>
|
||||
<Ionicons name="camera" size={22} color="#192126" />
|
||||
</View>
|
||||
@@ -394,11 +390,11 @@ export default function EditProfileScreen() {
|
||||
} else if (field === 'height') {
|
||||
updatedProfile.height = parseFloat(value) || undefined;
|
||||
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
|
||||
setHeightInput(value);
|
||||
|
||||
} else if (field === 'weight') {
|
||||
updatedProfile.weight = parseFloat(value) || undefined;
|
||||
setProfile(p => ({ ...p, weight: parseFloat(value) || undefined }));
|
||||
setWeightInput(value);
|
||||
|
||||
} else if (field === 'activity') {
|
||||
const activityLevel = parseInt(value) as number;
|
||||
updatedProfile.activityLevel = activityLevel;
|
||||
|
||||
192
components/FloatingFoodOverlay.tsx
Normal file
192
components/FloatingFoodOverlay.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
interface FloatingFoodOverlayProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
mealType?: string;
|
||||
}
|
||||
|
||||
export function FloatingFoodOverlay({ visible, onClose, mealType = 'dinner' }: FloatingFoodOverlayProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleFoodLibrary = () => {
|
||||
onClose();
|
||||
router.push(`${ROUTES.FOOD_LIBRARY}?mealType=${mealType}`);
|
||||
};
|
||||
|
||||
const handlePhotoRecognition = () => {
|
||||
onClose();
|
||||
router.push(`/food-camera?mealType=${mealType}`);
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'scan',
|
||||
title: '扫描',
|
||||
icon: '📷',
|
||||
backgroundColor: '#4FC3F7',
|
||||
onPress: handlePhotoRecognition,
|
||||
},
|
||||
{
|
||||
id: 'food-library',
|
||||
title: '食物库',
|
||||
icon: '🍎',
|
||||
backgroundColor: '#FF9500',
|
||||
onPress: handleFoodLibrary,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<TouchableOpacity
|
||||
style={styles.backdrop}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
/>
|
||||
|
||||
<View style={styles.container}>
|
||||
<BlurView intensity={80} tint="light" style={styles.blurContainer}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>日常记录</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.menuGrid}>
|
||||
{menuItems.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.menuItem}
|
||||
onPress={item.onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconContainer, { backgroundColor: item.backgroundColor }]}>
|
||||
<Text style={styles.iconText}>{item.icon}</Text>
|
||||
</View>
|
||||
<Text style={styles.menuText}>{item.title}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.closeButtonInner}>
|
||||
<Ionicons name="close" size={24} color="#666" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backdrop: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
blurContainer: {
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
minWidth: 340,
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 16,
|
||||
minHeight: 100
|
||||
},
|
||||
header: {
|
||||
paddingBottom: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
menuGrid: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
gap: 32,
|
||||
},
|
||||
menuItem: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
iconText: {
|
||||
fontSize: 22,
|
||||
},
|
||||
menuText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: 20,
|
||||
},
|
||||
closeButtonInner: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { FloatingFoodOverlay } from '@/components/FloatingFoodOverlay';
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { NutritionSummary } from '@/services/dietRecords';
|
||||
import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition';
|
||||
@@ -48,6 +49,7 @@ export function NutritionRadarCard({
|
||||
onMealPress
|
||||
}: NutritionRadarCardProps) {
|
||||
const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||
const [showFoodOverlay, setShowFoodOverlay] = useState(false);
|
||||
const radarValues = useMemo(() => {
|
||||
// 基于动态计算的营养目标或默认推荐值
|
||||
const recommendations = {
|
||||
@@ -108,7 +110,7 @@ export function NutritionRadarCard({
|
||||
};
|
||||
|
||||
const handleAddFood = () => {
|
||||
router.push(`/food-library?mealType=${currentMealType}`);
|
||||
setShowFoodOverlay(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -192,6 +194,12 @@ export function NutritionRadarCard({
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 食物添加悬浮窗 */}
|
||||
<FloatingFoodOverlay
|
||||
visible={showFoodOverlay}
|
||||
onClose={() => setShowFoodOverlay(false)}
|
||||
mealType={currentMealType}
|
||||
/>
|
||||
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
export type HeaderBarProps = {
|
||||
title: string | React.ReactNode;
|
||||
onBack?: () => void;
|
||||
backColor?: string;
|
||||
right?: React.ReactNode;
|
||||
tone?: 'light' | 'dark';
|
||||
showBottomBorder?: boolean;
|
||||
@@ -20,6 +21,7 @@ export type HeaderBarProps = {
|
||||
export function HeaderBar({
|
||||
title,
|
||||
onBack,
|
||||
backColor,
|
||||
right,
|
||||
tone,
|
||||
showBottomBorder = false,
|
||||
@@ -34,7 +36,7 @@ export function HeaderBar({
|
||||
// 根据变体确定背景色和样式
|
||||
const getBackgroundColor = () => {
|
||||
if (transparent) return 'transparent';
|
||||
|
||||
|
||||
switch (variant) {
|
||||
case 'elevated':
|
||||
return theme.background;
|
||||
@@ -47,11 +49,11 @@ export function HeaderBar({
|
||||
|
||||
const getBorderStyle = () => {
|
||||
if (!showBottomBorder) return {};
|
||||
|
||||
|
||||
return {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: variant === 'elevated'
|
||||
? theme.border
|
||||
borderBottomColor: variant === 'elevated'
|
||||
? theme.border
|
||||
: `${theme.border}40`, // 40% 透明度
|
||||
};
|
||||
};
|
||||
@@ -75,16 +77,16 @@ export function HeaderBar({
|
||||
]}
|
||||
>
|
||||
{onBack ? (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={onBack}
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={onBack}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={24}
|
||||
color={theme.text}
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={24}
|
||||
color={backColor || theme.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
@@ -94,8 +96,8 @@ export function HeaderBar({
|
||||
<View style={styles.titleContainer}>
|
||||
{typeof title === 'string' ? (
|
||||
<Text style={[
|
||||
styles.title,
|
||||
{
|
||||
styles.title,
|
||||
{
|
||||
color: theme.text,
|
||||
fontWeight: variant === 'elevated' ? '700' : '800',
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoBlur (14.1.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoCamera (16.1.11):
|
||||
- ExpoModulesCore
|
||||
- ZXingObjC/OneD
|
||||
- ZXingObjC/PDF417
|
||||
- ExpoFileSystem (18.1.11):
|
||||
- ExpoModulesCore
|
||||
- ExpoFont (13.3.2):
|
||||
@@ -1985,6 +1989,11 @@ PODS:
|
||||
- SocketRocket (0.7.1)
|
||||
- UMAppLoader (5.1.3)
|
||||
- Yoga (0.0.0)
|
||||
- ZXingObjC/Core (3.6.9)
|
||||
- ZXingObjC/OneD (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
- ZXingObjC/PDF417 (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
@@ -1999,6 +2008,7 @@ DEPENDENCIES:
|
||||
- ExpoBackgroundFetch (from `../node_modules/expo-background-fetch/ios`)
|
||||
- ExpoBackgroundTask (from `../node_modules/expo-background-task/ios`)
|
||||
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
||||
- ExpoCamera (from `../node_modules/expo-camera/ios`)
|
||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
|
||||
@@ -2119,6 +2129,7 @@ SPEC REPOS:
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
- ZXingObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
@@ -2145,6 +2156,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-background-task/ios"
|
||||
ExpoBlur:
|
||||
:path: "../node_modules/expo-blur/ios"
|
||||
ExpoCamera:
|
||||
:path: "../node_modules/expo-camera/ios"
|
||||
ExpoFileSystem:
|
||||
:path: "../node_modules/expo-file-system/ios"
|
||||
ExpoFont:
|
||||
@@ -2359,6 +2372,7 @@ SPEC CHECKSUMS:
|
||||
ExpoBackgroundFetch: 6dcade705c90ae5b7e2d0836b9145cae8f5f3070
|
||||
ExpoBackgroundTask: 6c1990438e45b5c4bbbc7d75aa6b688d53602fe8
|
||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||
ExpoCamera: e1879906d41184e84b57d7643119f8509414e318
|
||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
||||
ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500
|
||||
@@ -2474,6 +2488,7 @@ SPEC CHECKSUMS:
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
|
||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||
|
||||
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
||||
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"expo-background-fetch": "^13.1.6",
|
||||
"expo-background-task": "~0.2.8",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-camera": "^16.1.11",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-font": "~13.3.2",
|
||||
"expo-haptics": "~14.1.4",
|
||||
@@ -7109,6 +7110,26 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-camera": {
|
||||
"version": "16.1.11",
|
||||
"resolved": "https://mirrors.tencent.com/npm/expo-camera/-/expo-camera-16.1.11.tgz",
|
||||
"integrity": "sha512-etA5ZKoC6nPBnWWqiTmlX//zoFZ6cWQCCIdmpUHTGHAKd4qZNCkhPvBWbi8o32pDe57lix1V4+TPFgEcvPwsaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-web": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-web": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-constants": {
|
||||
"version": "17.1.7",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"expo-background-fetch": "^13.1.6",
|
||||
"expo-background-task": "~0.2.8",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-camera": "^16.1.11",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-font": "~13.3.2",
|
||||
"expo-haptics": "~14.1.4",
|
||||
|
||||
Reference in New Issue
Block a user