feat: 支持食物详情弹窗

This commit is contained in:
richarjiang
2025-08-28 19:24:22 +08:00
parent 6551757ca8
commit c15a9176f4
7 changed files with 664 additions and 233 deletions

View File

@@ -0,0 +1,425 @@
import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
Keyboard,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
// 食物数据类型定义
export interface FoodItem {
id: string;
name: string;
emoji: string;
calories: number;
unit: string; // 单位,如 "100克"
}
// 营养信息接口
interface NutritionInfo {
protein: number; // 蛋白质(克)
fat: number; // 脂肪(克)
carbs: number; // 碳水化合物(克)
}
// 单位选项
const UNIT_OPTIONS = [
{ id: 'gram', name: '克', ratio: 1 },
{ id: 'small', name: '小份', ratio: 0.8 },
{ id: 'medium', name: '中份', ratio: 1.2 },
{ id: 'large', name: '大份', ratio: 1.5 },
];
// 食物详情弹窗属性
export interface FoodDetailModalProps {
visible: boolean;
food: FoodItem | null;
onClose: () => void;
onSave: (food: FoodItem, amount: number, unit: string) => void;
}
// 模拟营养数据
const getNutritionInfo = (foodId: string): NutritionInfo => {
const nutritionData: Record<string, NutritionInfo> = {
'5': { protein: 0.8, fat: 0.6, carbs: 14.5 }, // 猕猴桃
'1': { protein: 0.2, fat: 0.0, carbs: 0.1 }, // 咖啡
'2': { protein: 12.8, fat: 11.1, carbs: 0.7 }, // 荷包蛋
'3': { protein: 13.3, fat: 8.8, carbs: 2.8 }, // 鸡蛋
'4': { protein: 1.4, fat: 0.2, carbs: 22.8 }, // 香蕉
'6': { protein: 0.2, fat: 0.2, carbs: 13.8 }, // 苹果
'7': { protein: 0.7, fat: 0.3, carbs: 7.7 }, // 草莓
'8': { protein: 6.6, fat: 4.6, carbs: 22.7 }, // 蛋烧麦
'9': { protein: 2.6, fat: 0.3, carbs: 25.9 }, // 米饭
'10': { protein: 4.0, fat: 1.2, carbs: 22.8 }, // 玉米
};
return nutritionData[foodId] || { protein: 0, fat: 0, carbs: 0 };
};
export function FoodDetailModal({
visible,
food,
onClose,
onSave
}: FoodDetailModalProps) {
const [amount, setAmount] = useState('100');
const [selectedUnit, setSelectedUnit] = useState(UNIT_OPTIONS[0]);
const [isFavorite, setIsFavorite] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
// 键盘监听
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
(e) => {
console.log('键盘显示,高度:', e.endCoordinates.height);
setKeyboardHeight(e.endCoordinates.height);
}
);
const keyboardDidHideListener = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
() => {
console.log('键盘隐藏');
setKeyboardHeight(0);
}
);
return () => {
keyboardDidShowListener?.remove();
keyboardDidHideListener?.remove();
};
}, []);
// 重置状态
useEffect(() => {
if (visible && food) {
console.log('FoodDetailModal: 接收到食物数据:', food);
setAmount('100');
setSelectedUnit(UNIT_OPTIONS[0]);
setIsFavorite(false);
}
}, [visible, food]);
// 调试信息
console.log('FoodDetailModal render:', {
visible,
food: food?.name,
foodId: food?.id,
hasFood: !!food
});
if (!food) {
console.log('FoodDetailModal: food为空不渲染内容');
return null;
}
const nutrition = getNutritionInfo(food.id);
const amountNum = parseFloat(amount) || 0;
const unitRatio = selectedUnit.ratio;
const finalAmount = amountNum * unitRatio;
// 计算实际营养值
const actualCalories = Math.round((food.calories * finalAmount) / 100);
const actualProtein = ((nutrition.protein * finalAmount) / 100).toFixed(1);
const actualFat = ((nutrition.fat * finalAmount) / 100).toFixed(1);
const actualCarbs = ((nutrition.carbs * finalAmount) / 100).toFixed(1);
const handleSave = () => {
onSave(food, finalAmount, selectedUnit.name);
onClose();
};
console.log('FoodDetailModal 即将渲染 Modal:', { visible, food: food.name });
return (
<Modal
visible={visible}
animationType="fade"
transparent={true}
onRequestClose={onClose}
presentationStyle="overFullScreen"
>
<View style={styles.overlay}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'position' : 'height'}
style={styles.keyboardAvoidingView}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<View style={[
styles.modalContainer,
keyboardHeight > 0 && {
height: screenHeight - keyboardHeight - 100, // 减去键盘高度和更多安全距离
maxHeight: screenHeight - keyboardHeight - 100,
minHeight: Math.min(400, screenHeight * 0.4) // 确保最小高度但不超过屏幕40%
}
]}>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{
flexGrow: 1,
paddingBottom: keyboardHeight > 0 ? 20 : 0 // 键盘弹出时增加底部间距
}}
scrollEnabled={true}
bounces={false}
>
{/* 头部 */}
<View style={styles.header}>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={24} color="#666" />
</TouchableOpacity>
<TouchableOpacity style={styles.correctButton}>
<Text style={styles.correctButtonText}></Text>
</TouchableOpacity>
</View>
{/* 食物信息 */}
<View style={styles.foodHeader}>
<View style={styles.foodInfo}>
<Text style={styles.foodEmoji}>{food.emoji}</Text>
<Text style={styles.foodName}>{food.name}</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
</View>
<TouchableOpacity
onPress={() => setIsFavorite(!isFavorite)}
style={styles.favoriteButton}
>
<Ionicons
name={isFavorite ? "star" : "star-outline"}
size={24}
color={isFavorite ? "#FFD700" : "#CCC"}
/>
</TouchableOpacity>
</View>
{/* 营养信息 */}
<View style={styles.nutritionContainer}>
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionValue}>{actualCalories}</Text>
</View>
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionValue}>{actualProtein}</Text>
</View>
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionValue}>{actualFat}</Text>
</View>
<View style={styles.nutritionItem}>
<Text style={styles.nutritionLabel}></Text>
<Text style={styles.nutritionValue}>{actualCarbs}</Text>
</View>
</View>
{/* 数量输入 */}
<View style={styles.amountContainer}>
<TextInput
style={styles.amountInput}
value={amount}
onChangeText={setAmount}
keyboardType="numeric"
selectTextOnFocus
/>
</View>
{/* 单位选择 */}
<View style={styles.unitContainer}>
{UNIT_OPTIONS.map((unit) => (
<TouchableOpacity
key={unit.id}
style={[
styles.unitOption,
selectedUnit.id === unit.id && styles.unitOptionActive
]}
onPress={() => setSelectedUnit(unit)}
>
<Text style={[
styles.unitText,
selectedUnit.id === unit.id && styles.unitTextActive
]}>
{unit.name}
</Text>
</TouchableOpacity>
))}
</View>
{/* 保存按钮 */}
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
<Text style={styles.saveButtonText}></Text>
</TouchableOpacity>
</ScrollView>
</View>
</KeyboardAvoidingView>
</View>
</Modal>
);
}
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
alignItems: 'center',
},
keyboardAvoidingView: {
width: '100%',
justifyContent: 'flex-end',
alignItems: 'center',
},
modalContainer: {
width: screenWidth,
backgroundColor: '#FFFFFF',
borderRadius: 20, // 四周都设置圆角
// maxHeight: screenHeight * 0.75, // 最大高度为屏幕高度的75%
// minHeight: screenHeight * 0.5, // 最小高度为屏幕高度的50%
shadowColor: '#000',
shadowOffset: {
width: 0,
height: -5,
},
shadowOpacity: 0.25,
shadowRadius: 20,
elevation: 10,
overflow: 'hidden', // 确保内容不会溢出
},
scrollView: {
flexGrow: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: screenWidth > 400 ? 24 : 16, // 根据屏幕宽度调整内边距
paddingTop: 16,
paddingBottom: 8,
},
closeButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
correctButton: {
paddingHorizontal: 16,
paddingVertical: 8,
},
correctButtonText: {
fontSize: 16,
color: '#4CAF50',
fontWeight: '500',
},
foodHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: screenWidth > 400 ? 24 : 16,
paddingVertical: 12,
},
foodInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
foodEmoji: {
fontSize: 40,
marginRight: 12,
},
foodName: {
fontSize: 20,
fontWeight: '600',
color: '#333',
marginRight: 8,
},
favoriteButton: {
padding: 8,
},
nutritionContainer: {
flexDirection: 'row',
paddingHorizontal: screenWidth > 400 ? 24 : 16,
paddingVertical: 16,
justifyContent: 'space-between',
},
nutritionItem: {
alignItems: 'center',
flex: 1,
},
nutritionLabel: {
fontSize: 14,
color: '#666',
marginBottom: 8,
},
nutritionValue: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
amountContainer: {
alignItems: 'center',
paddingVertical: 16,
},
amountInput: {
backgroundColor: '#E8F5E8',
borderRadius: 12,
paddingHorizontal: 24,
paddingVertical: 16,
fontSize: 24,
fontWeight: '600',
color: '#4CAF50',
textAlign: 'center',
minWidth: 120,
},
unitContainer: {
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: screenWidth > 400 ? 24 : 16,
paddingBottom: 20,
gap: screenWidth > 400 ? 16 : 12, // 根据屏幕宽度调整间距
},
unitOption: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#F5F5F5',
minWidth: 60,
alignItems: 'center',
},
unitOptionActive: {
backgroundColor: '#4CAF50',
},
unitText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
unitTextActive: {
color: '#FFFFFF',
},
saveButton: {
backgroundColor: '#4CAF50',
marginHorizontal: screenWidth > 400 ? 24 : 16,
marginBottom: 16,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
},
saveButtonText: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
},
});