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:
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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user