934 lines
27 KiB
TypeScript
934 lines
27 KiB
TypeScript
import { CreateCustomFoodModal, type CustomFoodData } from '@/components/model/food/CreateCustomFoodModal';
|
||
import { FoodDetailModal } from '@/components/model/food/FoodDetailModal';
|
||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { DEFAULT_IMAGE_FOOD } from '@/constants/Image';
|
||
import { useAppDispatch } from '@/hooks/redux';
|
||
import { useFoodLibrary, useFoodSearch } from '@/hooks/useFoodLibrary';
|
||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||
import { addDietRecord, type CreateDietRecordDto } from '@/services/dietRecords';
|
||
import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi';
|
||
import { fetchDailyNutritionData } from '@/store/nutritionSlice';
|
||
import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food';
|
||
import { saveNutritionToHealthKit } from '@/utils/health';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { Image } from 'expo-image';
|
||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||
import React, { useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
Modal,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
|
||
// 餐次映射保持不变
|
||
|
||
// 餐次映射
|
||
const MEAL_TYPE_MAP = {
|
||
breakfast: '早餐',
|
||
lunch: '午餐',
|
||
dinner: '晚餐',
|
||
snack: '加餐'
|
||
};
|
||
|
||
export default function FoodLibraryScreen() {
|
||
const safeAreaTop = useSafeAreaTop()
|
||
const router = useRouter();
|
||
const params = useLocalSearchParams<{ mealType?: string }>();
|
||
const mealType = (params.mealType as MealType) || 'breakfast';
|
||
|
||
// Redux hooks
|
||
const dispatch = useAppDispatch();
|
||
const { categories, loading, error, clearErrors, loadFoodLibrary } = useFoodLibrary();
|
||
const { searchResults, searchLoading, search, clearResults } = useFoodSearch();
|
||
|
||
// 本地状态
|
||
const [selectedCategoryId, setSelectedCategoryId] = useState('common');
|
||
const [searchText, setSearchText] = useState('');
|
||
const [selectedFood, setSelectedFood] = useState<FoodItem | null>(null);
|
||
const [showFoodDetail, setShowFoodDetail] = useState(false);
|
||
const [selectedFoodItems, setSelectedFoodItems] = useState<SelectedFoodItem[]>([]);
|
||
const [showMealSelector, setShowMealSelector] = useState(false);
|
||
const [currentMealType, setCurrentMealType] = useState<MealType>(mealType);
|
||
const [isRecording, setIsRecording] = useState(false);
|
||
const [showCreateCustomFood, setShowCreateCustomFood] = useState(false);
|
||
|
||
// 获取当前选中的分类
|
||
const selectedCategory = categories.find(cat => cat.id === selectedCategoryId);
|
||
|
||
|
||
// 过滤食物列表 - 优先显示搜索结果
|
||
const filteredFoods = useMemo(() => {
|
||
if (searchText.trim() && searchResults.length > 0) {
|
||
return searchResults;
|
||
}
|
||
|
||
if (selectedCategory) {
|
||
return selectedCategory.foods
|
||
}
|
||
|
||
return [];
|
||
}, [searchText, searchResults, selectedCategory]);
|
||
|
||
|
||
// 处理搜索
|
||
useEffect(() => {
|
||
const timeoutId = setTimeout(() => {
|
||
if (searchText.trim()) {
|
||
search(searchText);
|
||
} else {
|
||
clearResults();
|
||
}
|
||
}, 300); // 防抖
|
||
|
||
return () => clearTimeout(timeoutId);
|
||
}, [searchText, search, clearResults]);
|
||
|
||
// 处理食物选择 - 显示详情弹窗
|
||
const handleSelectFood = (food: FoodItem) => {
|
||
console.log('选择食物:', food);
|
||
setSelectedFood(food);
|
||
setShowFoodDetail(true);
|
||
console.log('设置弹窗状态:', {
|
||
showFoodDetail: true,
|
||
selectedFood: food,
|
||
foodName: food.name,
|
||
foodId: food.id
|
||
});
|
||
};
|
||
|
||
// 处理食物保存
|
||
const handleSaveFood = (food: FoodItem, amount: number, unit: string) => {
|
||
// 计算实际热量
|
||
const actualCalories = Math.round((food.calories * amount) / 100);
|
||
|
||
// 创建新的选择项目
|
||
const newSelectedItem: SelectedFoodItem = {
|
||
id: `${food.id}_${Date.now()}`, // 使用时间戳确保唯一性
|
||
food,
|
||
amount,
|
||
unit,
|
||
calories: actualCalories
|
||
};
|
||
|
||
// 添加到已选择列表
|
||
setSelectedFoodItems(prev => [...prev, newSelectedItem]);
|
||
|
||
console.log('保存食物:', food, amount, unit, '热量:', actualCalories);
|
||
setShowFoodDetail(false);
|
||
};
|
||
|
||
// 移除已选择的食物
|
||
const handleRemoveSelectedFood = (itemId: string) => {
|
||
setSelectedFoodItems(prev => prev.filter(item => item.id !== itemId));
|
||
};
|
||
|
||
// 计算总热量
|
||
const totalCalories = selectedFoodItems.reduce((sum, item) => sum + item.calories, 0);
|
||
|
||
// 关闭详情弹窗
|
||
const handleCloseFoodDetail = () => {
|
||
setShowFoodDetail(false);
|
||
setSelectedFood(null);
|
||
};
|
||
|
||
// 处理删除自定义食物
|
||
const handleDeleteFood = async (foodId: string) => {
|
||
try {
|
||
await foodLibraryApi.deleteCustomFood(Number(foodId));
|
||
// 删除成功后重新加载食物库数据
|
||
await loadFoodLibrary();
|
||
// 关闭弹窗
|
||
handleCloseFoodDetail();
|
||
} catch (error) {
|
||
console.error('删除食物失败:', error);
|
||
Alert.alert('删除失败', '删除食物时发生错误,请稍后重试');
|
||
}
|
||
};
|
||
|
||
// 处理饮食记录
|
||
const handleRecordDiet = async () => {
|
||
if (selectedFoodItems.length === 0) return;
|
||
|
||
setIsRecording(true);
|
||
|
||
try {
|
||
// 逐个记录选中的食物
|
||
for (const item of selectedFoodItems) {
|
||
const dietRecordData: CreateDietRecordDto = {
|
||
mealType: currentMealType,
|
||
foodName: item.food.name,
|
||
foodDescription: item.food.description,
|
||
portionDescription: `${item.amount}${item.unit}`,
|
||
estimatedCalories: item.calories,
|
||
proteinGrams: item.food.protein ? Number(((item.food.protein * item.amount) / 100).toFixed(2)) : undefined,
|
||
carbohydrateGrams: item.food.carbohydrate ? Number(((item.food.carbohydrate * item.amount) / 100).toFixed(2)) : undefined,
|
||
fatGrams: item.food.fat ? Number(((item.food.fat * item.amount) / 100).toFixed(2)) : undefined,
|
||
fiberGrams: item.food.fiber ? Number(((item.food.fiber * item.amount) / 100).toFixed(2)) : undefined,
|
||
sugarGrams: item.food.sugar ? Number(((item.food.sugar * item.amount) / 100).toFixed(2)) : undefined,
|
||
sodiumMg: item.food.sodium ? Number(((item.food.sodium * item.amount) / 100).toFixed(2)) : undefined,
|
||
additionalNutrition: item.food.additionalNutrition,
|
||
source: 'manual',
|
||
mealTime: new Date().toISOString(),
|
||
imageUrl: item.food.imageUrl,
|
||
};
|
||
|
||
// 先保存到后端
|
||
await addDietRecord(dietRecordData);
|
||
|
||
// 然后尝试同步到 HealthKit(非阻塞)
|
||
// 提取蛋白质、脂肪和碳水化合物数据
|
||
const { proteinGrams, fatGrams, carbohydrateGrams, mealTime } = dietRecordData;
|
||
|
||
if (proteinGrams !== undefined || fatGrams !== undefined || carbohydrateGrams !== undefined) {
|
||
// 使用 catch 确保 HealthKit 同步失败不影响后端记录
|
||
saveNutritionToHealthKit(
|
||
{
|
||
proteinGrams: proteinGrams || undefined,
|
||
fatGrams: fatGrams || undefined,
|
||
carbohydrateGrams: carbohydrateGrams || undefined
|
||
},
|
||
mealTime
|
||
).catch(error => {
|
||
// HealthKit 同步失败只记录日志,不影响用户体验
|
||
console.error('HealthKit 营养数据同步失败(不影响记录):', error);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 记录成功后,刷新当天的营养数据
|
||
const today = new Date();
|
||
await dispatch(fetchDailyNutritionData(today));
|
||
|
||
// 清空选择列表并返回
|
||
setSelectedFoodItems([]);
|
||
router.back();
|
||
} catch (error) {
|
||
console.error('记录饮食失败:', error);
|
||
// 这里可以显示错误提示
|
||
} finally {
|
||
setIsRecording(false);
|
||
}
|
||
};
|
||
|
||
// 处理餐次选择
|
||
const handleMealTypeSelect = (selectedMealType: MealType) => {
|
||
setCurrentMealType(selectedMealType);
|
||
setShowMealSelector(false);
|
||
};
|
||
|
||
// 处理创建自定义食物
|
||
const handleCreateCustomFood = () => {
|
||
setShowCreateCustomFood(true);
|
||
};
|
||
|
||
// 处理保存自定义食物
|
||
const handleSaveCustomFood = async (customFoodData: CustomFoodData) => {
|
||
try {
|
||
// 转换数据格式以匹配API要求
|
||
const createData: CreateCustomFoodDto = {
|
||
name: customFoodData.name,
|
||
caloriesPer100g: customFoodData.calories,
|
||
proteinPer100g: customFoodData.protein,
|
||
carbohydratePer100g: customFoodData.carbohydrate,
|
||
fatPer100g: customFoodData.fat,
|
||
imageUrl: customFoodData.imageUrl,
|
||
};
|
||
|
||
// 调用API创建自定义食物
|
||
const createdFood = await foodLibraryApi.createCustomFood(createData);
|
||
|
||
// 需要拉取一遍最新的食物列表
|
||
await loadFoodLibrary();
|
||
|
||
// 创建FoodItem对象
|
||
const customFoodItem: FoodItem = {
|
||
id: createdFood.id.toString(),
|
||
name: createdFood.name,
|
||
calories: createdFood.caloriesPer100g || 0,
|
||
unit: 'g',
|
||
description: createdFood.description || `自定义食物 - ${createdFood.name}`,
|
||
imageUrl: createdFood.imageUrl,
|
||
protein: createdFood.proteinPer100g,
|
||
fat: createdFood.fatPer100g,
|
||
carbohydrate: createdFood.carbohydratePer100g,
|
||
};
|
||
|
||
// 添加到选择列表中
|
||
const newSelectedItem: SelectedFoodItem = {
|
||
id: createdFood.id.toString(),
|
||
food: customFoodItem,
|
||
amount: customFoodData.defaultAmount,
|
||
unit: 'g',
|
||
calories: Math.round((customFoodItem.calories * customFoodData.defaultAmount) / 100)
|
||
};
|
||
|
||
setSelectedFoodItems(prev => [...prev, newSelectedItem]);
|
||
} catch (error) {
|
||
console.error('创建自定义食物失败:', error);
|
||
Alert.alert('创建失败', '创建自定义食物时发生错误,请稍后重试');
|
||
}
|
||
};
|
||
|
||
// 关闭自定义食物弹窗
|
||
const handleCloseCreateCustomFood = () => {
|
||
setShowCreateCustomFood(false);
|
||
};
|
||
|
||
// 餐次选择选项
|
||
const mealOptions = [
|
||
{ key: 'breakfast' as const, label: '早餐', color: '#FF6B35' },
|
||
{ key: 'lunch' as const, label: '午餐', color: '#4CAF50' },
|
||
{ key: 'dinner' as const, label: '晚餐', color: '#2196F3' },
|
||
{ key: 'snack' as const, label: '加餐', color: '#FF9800' },
|
||
];
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 头部 */}
|
||
<HeaderBar
|
||
title="食物库"
|
||
onBack={() => router.back()}
|
||
variant="elevated"
|
||
right={
|
||
<TouchableOpacity style={styles.customButton} onPress={handleCreateCustomFood}>
|
||
<Text style={styles.customButtonText}>自定义</Text>
|
||
</TouchableOpacity>
|
||
}
|
||
/>
|
||
|
||
<View style={{
|
||
paddingTop: safeAreaTop
|
||
}} />
|
||
|
||
{/* 搜索框 */}
|
||
<View style={styles.searchContainer}>
|
||
<Ionicons name="search" size={20} color="#999" style={styles.searchIcon} />
|
||
<TextInput
|
||
style={styles.searchInput}
|
||
placeholder="搜索食物"
|
||
value={searchText}
|
||
onChangeText={setSearchText}
|
||
placeholderTextColor="#999"
|
||
/>
|
||
</View>
|
||
|
||
{/* 主要内容区域 - 卡片样式 */}
|
||
<View style={styles.mainContentCard}>
|
||
{loading && categories.length === 0 ? (
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color="#4CAF50" />
|
||
<Text style={styles.loadingText}>加载食物库中...</Text>
|
||
</View>
|
||
) : error ? (
|
||
<View style={styles.errorContainer}>
|
||
<Text style={styles.errorText}>{error}</Text>
|
||
<TouchableOpacity
|
||
style={styles.retryButton}
|
||
onPress={() => {
|
||
clearErrors();
|
||
// 这里可以重新加载数据
|
||
}}
|
||
>
|
||
<Text style={styles.retryButtonText}>重试</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
) : (
|
||
<View style={styles.mainContent}>
|
||
{/* 左侧分类导航 */}
|
||
<View style={styles.categoryContainer}>
|
||
<ScrollView showsVerticalScrollIndicator={false}>
|
||
{categories.map((category) => (
|
||
<TouchableOpacity
|
||
key={category.id}
|
||
style={[
|
||
styles.categoryItem,
|
||
selectedCategoryId === category.id && styles.categoryItemActive
|
||
]}
|
||
onPress={() => {
|
||
setSelectedCategoryId(category.id);
|
||
// 切换分类时清除搜索
|
||
if (searchText) {
|
||
setSearchText('');
|
||
clearResults();
|
||
}
|
||
}}
|
||
>
|
||
<Text style={[
|
||
styles.categoryText,
|
||
selectedCategoryId === category.id && styles.categoryTextActive
|
||
]}>
|
||
{category.name}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* 右侧食物列表 */}
|
||
<View style={styles.foodContainer}>
|
||
{searchLoading ? (
|
||
<View style={styles.searchLoadingContainer}>
|
||
<ActivityIndicator size="small" color="#4CAF50" />
|
||
<Text style={styles.searchLoadingText}>搜索中...</Text>
|
||
</View>
|
||
) : (
|
||
<ScrollView showsVerticalScrollIndicator={false}>
|
||
{filteredFoods.map((food) => (
|
||
<View key={food.id} style={styles.foodItem}>
|
||
<View style={styles.foodInfo}>
|
||
<Image
|
||
style={styles.foodImage}
|
||
source={{ uri: food.imageUrl || DEFAULT_IMAGE_FOOD }}
|
||
cachePolicy={'memory-disk'}
|
||
/>
|
||
<View style={styles.foodDetails}>
|
||
<Text style={styles.foodName}>{food.name}</Text>
|
||
<Text style={styles.foodCalories}>
|
||
{food.calories}千卡/{food.unit}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<TouchableOpacity
|
||
style={styles.addButton}
|
||
onPress={() => handleSelectFood(food)}
|
||
>
|
||
<Ionicons name="add" size={20} color="#666" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
))}
|
||
|
||
{filteredFoods.length === 0 && !searchLoading && (
|
||
<View style={styles.emptyContainer}>
|
||
<Text style={styles.emptyText}>
|
||
{searchText ? '未找到相关食物' : '暂无食物数据'}
|
||
</Text>
|
||
{searchText && (
|
||
<Text style={styles.emptySubText}>
|
||
尝试使用其他关键词搜索
|
||
</Text>
|
||
)}
|
||
</View>
|
||
)}
|
||
</ScrollView>
|
||
)}
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 已选择食物列表 */}
|
||
{selectedFoodItems.length > 0 && (
|
||
<View style={styles.selectedFoodsContainer}>
|
||
<View style={styles.selectedFoodsHeader}>
|
||
<Text style={styles.selectedFoodsTitle}>已选择食物 ({selectedFoodItems.length})</Text>
|
||
<Text style={styles.totalCalories}>总热量: {totalCalories}千卡</Text>
|
||
</View>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
style={styles.selectedFoodsList}
|
||
contentContainerStyle={styles.selectedFoodsContent}
|
||
>
|
||
{selectedFoodItems.map((item) => (
|
||
<View key={item.id} style={styles.selectedFoodItem}>
|
||
<TouchableOpacity
|
||
style={styles.removeButton}
|
||
onPress={() => handleRemoveSelectedFood(item.id)}
|
||
>
|
||
<Ionicons name="close-circle" size={16} color="#FF4444" />
|
||
</TouchableOpacity>
|
||
|
||
{item.food.imageUrl ? <Image
|
||
style={styles.selectedFoodImage}
|
||
source={{ uri: item.food.imageUrl }}
|
||
/> : <Text style={styles.selectedFoodEmoji}>{item.food.emoji}</Text>}
|
||
<Text style={styles.selectedFoodName} numberOfLines={1}>{item.food.name}</Text>
|
||
<Text style={styles.selectedFoodAmount}>{item.amount}{item.unit}</Text>
|
||
<Text style={styles.selectedFoodCalories}>{item.calories}千卡</Text>
|
||
</View>
|
||
))}
|
||
</ScrollView>
|
||
</View>
|
||
)}
|
||
|
||
{/* 底部餐次选择和记录按钮 */}
|
||
<View style={styles.bottomContainer}>
|
||
<TouchableOpacity
|
||
style={styles.mealSelector}
|
||
onPress={() => setShowMealSelector(true)}
|
||
>
|
||
<View style={[
|
||
styles.mealIndicator,
|
||
{ backgroundColor: mealOptions.find(option => option.key === currentMealType)?.color || '#FF6B35' }
|
||
]} />
|
||
<Text style={styles.mealText}>{MEAL_TYPE_MAP[currentMealType]}</Text>
|
||
<Ionicons name="chevron-down" size={16} color="#333" />
|
||
</TouchableOpacity>
|
||
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.recordButton,
|
||
(selectedFoodItems.length === 0 || isRecording) && styles.recordButtonDisabled
|
||
]}
|
||
disabled={selectedFoodItems.length === 0 || isRecording}
|
||
onPress={handleRecordDiet}
|
||
>
|
||
{isRecording ? (
|
||
<ActivityIndicator size="small" color="#FFF" />
|
||
) : (
|
||
<Text style={[
|
||
styles.recordButtonText,
|
||
selectedFoodItems.length === 0 && styles.recordButtonTextDisabled
|
||
]}>记录</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* 餐次选择弹窗 */}
|
||
<Modal
|
||
visible={showMealSelector}
|
||
transparent={true}
|
||
animationType="fade"
|
||
onRequestClose={() => setShowMealSelector(false)}
|
||
>
|
||
<View style={styles.mealSelectorOverlay}>
|
||
<TouchableOpacity
|
||
style={styles.mealSelectorBackdrop}
|
||
onPress={() => setShowMealSelector(false)}
|
||
/>
|
||
<View style={styles.mealSelectorModal}>
|
||
<View style={styles.mealSelectorHeader}>
|
||
<Text style={styles.mealSelectorTitle}>选择餐次</Text>
|
||
<TouchableOpacity onPress={() => setShowMealSelector(false)}>
|
||
<Ionicons name="close" size={24} color="#666" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
<View style={styles.mealOptionsContainer}>
|
||
{mealOptions.map((option) => (
|
||
<TouchableOpacity
|
||
key={option.key}
|
||
style={[
|
||
styles.mealOption,
|
||
currentMealType === option.key && styles.mealOptionActive
|
||
]}
|
||
onPress={() => handleMealTypeSelect(option.key)}
|
||
>
|
||
<View style={[styles.mealOptionIndicator, { backgroundColor: option.color }]} />
|
||
<Text style={[
|
||
styles.mealOptionText,
|
||
currentMealType === option.key && styles.mealOptionTextActive
|
||
]}>
|
||
{option.label}
|
||
</Text>
|
||
{currentMealType === option.key && (
|
||
<Ionicons name="checkmark" size={20} color="#4CAF50" />
|
||
)}
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
|
||
{/* 食物详情弹窗 */}
|
||
<FoodDetailModal
|
||
visible={showFoodDetail}
|
||
food={selectedFood}
|
||
category={selectedCategory}
|
||
onClose={handleCloseFoodDetail}
|
||
onSave={handleSaveFood}
|
||
onDelete={handleDeleteFood}
|
||
/>
|
||
|
||
{/* 创建自定义食物弹窗 */}
|
||
<CreateCustomFoodModal
|
||
visible={showCreateCustomFood}
|
||
onClose={handleCloseCreateCustomFood}
|
||
onSave={handleSaveCustomFood}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: Colors.light.pageBackgroundEmphasis,
|
||
},
|
||
customButton: {
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4,
|
||
},
|
||
customButtonText: {
|
||
fontSize: 16,
|
||
color: Colors.light.textSecondary,
|
||
fontWeight: '500',
|
||
},
|
||
searchContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: '#FFF',
|
||
marginHorizontal: 16,
|
||
marginVertical: 12,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
borderRadius: 12,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 1 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 2,
|
||
elevation: 2,
|
||
},
|
||
searchIcon: {
|
||
marginRight: 8,
|
||
},
|
||
searchInput: {
|
||
flex: 1,
|
||
fontSize: 16,
|
||
color: '#333',
|
||
},
|
||
mainContentCard: {
|
||
flex: 1,
|
||
marginHorizontal: 16,
|
||
marginBottom: 16,
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 2,
|
||
},
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 8,
|
||
elevation: 4,
|
||
overflow: 'hidden',
|
||
},
|
||
mainContent: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
},
|
||
categoryContainer: {
|
||
width: 100,
|
||
backgroundColor: 'transparent',
|
||
},
|
||
categoryItem: {
|
||
paddingVertical: 16,
|
||
paddingHorizontal: 12,
|
||
alignItems: 'center',
|
||
},
|
||
categoryItemActive: {
|
||
backgroundColor: '#F0F9FF'
|
||
},
|
||
categoryText: {
|
||
fontSize: 14,
|
||
color: '#666',
|
||
textAlign: 'center',
|
||
},
|
||
categoryTextActive: {
|
||
color: Colors.light.text,
|
||
fontWeight: '500',
|
||
},
|
||
foodContainer: {
|
||
flex: 1,
|
||
backgroundColor: 'transparent',
|
||
},
|
||
foodItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
},
|
||
foodInfo: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flex: 1,
|
||
},
|
||
foodImage: {
|
||
width: 32,
|
||
height: 32,
|
||
},
|
||
foodEmoji: {
|
||
fontSize: 32,
|
||
marginRight: 12,
|
||
},
|
||
foodDetails: {
|
||
flex: 1,
|
||
marginLeft: 12
|
||
},
|
||
foodName: {
|
||
fontSize: 16,
|
||
color: '#333',
|
||
fontWeight: '500',
|
||
marginBottom: 2,
|
||
|
||
},
|
||
foodCalories: {
|
||
fontSize: 14,
|
||
color: '#999',
|
||
},
|
||
addButton: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: '#F5F5F5',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
emptyContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 40,
|
||
},
|
||
emptyText: {
|
||
fontSize: 16,
|
||
color: '#999',
|
||
},
|
||
emptySubText: {
|
||
fontSize: 14,
|
||
color: '#CCC',
|
||
marginTop: 8,
|
||
textAlign: 'center',
|
||
},
|
||
// 加载状态样式
|
||
loadingContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 60,
|
||
},
|
||
loadingText: {
|
||
fontSize: 16,
|
||
color: '#666',
|
||
marginTop: 12,
|
||
},
|
||
// 错误状态样式
|
||
errorContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 60,
|
||
paddingHorizontal: 20,
|
||
},
|
||
errorText: {
|
||
fontSize: 16,
|
||
color: '#FF4444',
|
||
textAlign: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
retryButton: {
|
||
backgroundColor: '#4CAF50',
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 10,
|
||
borderRadius: 20,
|
||
},
|
||
retryButtonText: {
|
||
fontSize: 14,
|
||
color: '#FFF',
|
||
fontWeight: '500',
|
||
},
|
||
// 搜索加载状态样式
|
||
searchLoadingContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 20,
|
||
},
|
||
searchLoadingText: {
|
||
fontSize: 14,
|
||
color: '#666',
|
||
marginLeft: 8,
|
||
},
|
||
bottomContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
backgroundColor: '#FFF',
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#E5E5E5',
|
||
},
|
||
mealSelector: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
backgroundColor: '#F8F9FA',
|
||
borderRadius: 20,
|
||
},
|
||
mealIndicator: {
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
backgroundColor: '#FF6B35',
|
||
marginRight: 8,
|
||
},
|
||
mealText: {
|
||
fontSize: 14,
|
||
color: '#333',
|
||
marginRight: 4,
|
||
},
|
||
recordButton: {
|
||
backgroundColor: Colors.light.primary,
|
||
paddingHorizontal: 24,
|
||
paddingVertical: 10,
|
||
borderRadius: 20,
|
||
},
|
||
recordButtonText: {
|
||
fontSize: 16,
|
||
color: '#FFF',
|
||
fontWeight: '500',
|
||
},
|
||
recordButtonDisabled: {
|
||
backgroundColor: '#CCC',
|
||
},
|
||
recordButtonTextDisabled: {
|
||
color: '#999',
|
||
},
|
||
// 已选择食物列表样式
|
||
selectedFoodsContainer: {
|
||
backgroundColor: '#FFF',
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#E5E5E5',
|
||
paddingVertical: 12,
|
||
maxHeight: 140, // 限制最大高度
|
||
},
|
||
selectedFoodsHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 16,
|
||
marginBottom: 8,
|
||
},
|
||
selectedFoodsTitle: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#333',
|
||
},
|
||
totalCalories: {
|
||
fontSize: 14,
|
||
color: Colors.light.text,
|
||
fontWeight: '500',
|
||
},
|
||
selectedFoodsList: {
|
||
paddingHorizontal: 16,
|
||
},
|
||
selectedFoodsContent: {
|
||
paddingRight: 16,
|
||
},
|
||
selectedFoodItem: {
|
||
backgroundColor: '#F8F9FA',
|
||
borderRadius: 12,
|
||
padding: 8,
|
||
marginRight: 8,
|
||
minWidth: 80,
|
||
alignItems: 'center',
|
||
position: 'relative',
|
||
},
|
||
removeButton: {
|
||
position: 'absolute',
|
||
top: 4,
|
||
right: 4,
|
||
backgroundColor: '#FFF',
|
||
borderRadius: 8,
|
||
zIndex: 1,
|
||
},
|
||
selectedFoodImage: {
|
||
width: 20,
|
||
height: 20,
|
||
marginBottom: 4,
|
||
},
|
||
selectedFoodEmoji: {
|
||
fontSize: 20,
|
||
marginBottom: 4,
|
||
},
|
||
selectedFoodName: {
|
||
fontSize: 12,
|
||
color: '#333',
|
||
fontWeight: '500',
|
||
textAlign: 'center',
|
||
marginBottom: 2,
|
||
maxWidth: 64,
|
||
},
|
||
selectedFoodAmount: {
|
||
fontSize: 11,
|
||
color: '#666',
|
||
marginBottom: 2,
|
||
},
|
||
selectedFoodCalories: {
|
||
fontSize: 11,
|
||
color: '#4CAF50',
|
||
fontWeight: '500',
|
||
},
|
||
// 餐次选择弹窗样式
|
||
mealSelectorOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
mealSelectorBackdrop: {
|
||
flex: 1,
|
||
},
|
||
mealSelectorModal: {
|
||
backgroundColor: '#FFF',
|
||
borderTopLeftRadius: 20,
|
||
borderTopRightRadius: 20,
|
||
paddingBottom: 34, // 为底部安全区域留出空间
|
||
},
|
||
mealSelectorHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 16,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#E5E5E5',
|
||
},
|
||
mealSelectorTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
color: '#333',
|
||
},
|
||
mealOptionsContainer: {
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 16,
|
||
},
|
||
mealOption: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: 16,
|
||
paddingHorizontal: 16,
|
||
borderRadius: 12,
|
||
marginBottom: 8,
|
||
backgroundColor: '#F8F9FA',
|
||
},
|
||
mealOptionActive: {
|
||
backgroundColor: '#E8F5E8',
|
||
borderWidth: 1,
|
||
borderColor: '#4CAF50',
|
||
},
|
||
mealOptionIndicator: {
|
||
width: 12,
|
||
height: 12,
|
||
borderRadius: 6,
|
||
marginRight: 12,
|
||
},
|
||
mealOptionText: {
|
||
flex: 1,
|
||
fontSize: 16,
|
||
color: '#333',
|
||
fontWeight: '500',
|
||
},
|
||
mealOptionTextActive: {
|
||
color: '#4CAF50',
|
||
},
|
||
}); |