Files
digital-pilates/components/model/food/FoodDetailModal.tsx

500 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react';
import {
Alert,
Dimensions,
Keyboard,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
// 导入统一的食物类型定义
import { Colors } from '@/constants/Colors';
import { DEFAULT_IMAGE_FOOD } from '@/constants/Image';
import type { FoodItem } from '@/types/food';
import { Image } from 'expo-image';
// 导入统一的食物类型定义
// 营养信息接口
interface NutritionInfo {
protein: number; // 蛋白质(克)
fat: number; // 脂肪(克)
carbs: number; // 碳水化合物(克)
}
// 快捷选择选项基于100克的倍数
const QUICK_SELECT_OPTIONS = [
{ id: 'small', name: '小份', amount: 80 }, // 80克
{ id: 'medium', name: '中份', amount: 120 }, // 120克
{ id: 'large', name: '大份', amount: 150 }, // 150克
];
// 食物详情弹窗属性
export interface FoodDetailModalProps {
visible: boolean;
food: FoodItem | null;
category?: { id: string; isSystem?: boolean } | null;
onClose: () => void;
onSave: (food: FoodItem, amount: number, unit: string) => void;
onDelete?: (foodId: string) => void;
}
// 获取营养数据优先使用FoodItem中的数据
const getNutritionInfo = (food: FoodItem): NutritionInfo => {
// 检查是否有任何营养数据
const hasNutritionData = food.protein !== undefined ||
food.fat !== undefined ||
food.carbohydrate !== undefined;
if (hasNutritionData) {
// 优先使用FoodItem中的营养数据
return {
protein: food.protein || 0,
fat: food.fat || 0,
carbs: food.carbohydrate || 0,
};
}
// 如果FoodItem中没有营养数据使用模拟数据作为后备
const fallbackNutritionData: 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 fallbackNutritionData[food.id] || { protein: 0, fat: 0, carbs: 0 };
};
export function FoodDetailModal({
visible,
food,
category,
onClose,
onSave,
onDelete
}: FoodDetailModalProps) {
const [amount, setAmount] = useState('100');
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');
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);
const amountNum = parseFloat(amount) || 0;
// 计算实际营养值(基于输入的克数)
const actualCalories = Math.round((food.calories * amountNum) / 100);
const actualProtein = ((nutrition.protein * amountNum) / 100).toFixed(1);
const actualFat = ((nutrition.fat * amountNum) / 100).toFixed(1);
const actualCarbs = ((nutrition.carbs * amountNum) / 100).toFixed(1);
// 快捷选择处理函数
const handleQuickSelect = (selectedAmount: number) => {
setAmount(selectedAmount.toString());
};
const handleSave = () => {
onSave(food, amountNum, '克');
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}>
<Image
style={styles.foodImage}
source={{ uri: food.imageUrl || DEFAULT_IMAGE_FOOD }}
/>
<Text style={styles.foodName}>{food.name}</Text>
</View>
<View style={styles.actionButtons}>
{/* <TouchableOpacity
onPress={() => setIsFavorite(!isFavorite)}
style={styles.favoriteButton}
>
<Ionicons
name={isFavorite ? "star" : "star-outline"}
size={18}
color={isFavorite ? "#FFD700" : "#CCC"}
/>
</TouchableOpacity> */}
{/* 删除按钮 - 仅对自定义食物显示 */}
{category && category.id === 'custom' && onDelete && (
<TouchableOpacity
onPress={() => {
Alert.alert(
'删除食物',
'确定要删除这个自定义食物吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => onDelete(food.id)
}
]
);
}}
style={styles.deleteButton}
>
<Ionicons
name="trash-outline"
size={18}
color="#FF6B6B"
/>
</TouchableOpacity>
)}
</View>
</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}>
<View style={styles.inputWithUnit}>
<TextInput
style={styles.amountInput}
value={amount}
onChangeText={setAmount}
keyboardType="numeric"
selectTextOnFocus
/>
<Text style={styles.unitLabel}></Text>
</View>
</View>
{/* 快捷选择 */}
<View style={styles.quickSelectContainer}>
{QUICK_SELECT_OPTIONS.map((option) => (
<TouchableOpacity
key={option.id}
style={[
styles.quickSelectOption,
amount === option.amount.toString() && styles.quickSelectOptionActive
]}
onPress={() => handleQuickSelect(option.amount)}
>
<Text style={[
styles.quickSelectText,
amount === option.amount.toString() && styles.quickSelectTextActive
]}>
{option.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,
},
foodImage: {
width: 40,
height: 40,
marginRight: 12,
},
foodEmoji: {
fontSize: 40,
marginRight: 12,
},
foodName: {
fontSize: 20,
fontWeight: '600',
color: '#333',
marginRight: 8,
},
actionButtons: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
favoriteButton: {
padding: 8,
},
deleteButton: {
padding: 8,
},
nutritionContainer: {
flexDirection: 'row',
paddingHorizontal: screenWidth > 400 ? 24 : 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',
},
inputWithUnit: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 16,
},
amountInput: {
fontSize: 24,
fontWeight: '600',
color: Colors.light.text,
textAlign: 'center',
minWidth: 80,
backgroundColor: 'transparent',
borderBottomWidth: 1,
borderBottomColor: 'gray',
},
unitLabel: {
fontSize: 18,
fontWeight: '500',
color: Colors.light.text,
marginLeft: 8,
},
quickSelectContainer: {
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: screenWidth > 400 ? 24 : 16,
paddingBottom: 20,
gap: screenWidth > 400 ? 16 : 12, // 根据屏幕宽度调整间距
},
quickSelectOption: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#F5F5F5',
minWidth: 60,
alignItems: 'center',
},
quickSelectOptionActive: {
backgroundColor: Colors.light.primary,
},
quickSelectText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
quickSelectTextActive: {
color: '#FFFFFF',
},
saveButton: {
backgroundColor: Colors.light.primary,
marginHorizontal: screenWidth > 400 ? 24 : 16,
marginBottom: 16,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
},
saveButtonText: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
},
});