Files
digital-pilates/components/model/food/FoodDetailModal.tsx
2025-08-28 19:24:22 +08:00

425 lines
12 KiB
TypeScript
Raw 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 {
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',
},
});