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,10 +133,11 @@ 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.sectionContainer, {
marginBottom: 0
}]}>
<View style={[styles.userInfoContainer,]}>
<View style={styles.avatarContainer}>
<Image
source={userProfile.avatar || DEFAULT_AVATAR_URL}
@@ -153,15 +154,16 @@ export default function PersonalScreen() {
<Text style={styles.editButtonText}></Text>
</TouchableOpacity>
</View>
</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,10 +276,26 @@ export default function PersonalScreen() {
return (
<View style={styles.container}>
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
<SafeAreaView style={styles.safeArea}>
{/* 背景渐变 */}
<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={{ paddingBottom: bottomPadding }}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: bottomPadding,
paddingHorizontal: 16,
}}
showsVerticalScrollIndicator={false}
>
<UserHeader />
@@ -307,7 +315,6 @@ export default function PersonalScreen() {
<MenuSection key={index} title={section.title} items={section.items} />
))}
</ScrollView>
</SafeAreaView>
</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,10 +582,13 @@ export default function ExploreScreen() {
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<SafeAreaView style={styles.safeArea}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={{ paddingBottom: bottomPadding }}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: bottomPadding,
paddingHorizontal: 20
}}
showsVerticalScrollIndicator={false}
>
{/* 顶部信息栏 */}
@@ -733,7 +735,6 @@ export default function ExploreScreen() {
</View>
</View>
</ScrollView>
</SafeAreaView>
</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
View 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',
},
});

View File

@@ -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;

View 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,
},
});

View File

@@ -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>
);

View File

@@ -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,
@@ -84,7 +86,7 @@ export function HeaderBar({
<Ionicons
name="chevron-back"
size={24}
color={theme.text}
color={backColor || theme.text}
/>
</TouchableOpacity>
) : (

View File

@@ -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
View File

@@ -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",

View File

@@ -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",