feat:
This commit is contained in:
@@ -4,9 +4,10 @@ import { NutritionSummary } from '@/services/dietRecords';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { RadarCategory, RadarChart } from './RadarChart';
|
import { RadarCategory, RadarChart } from './RadarChart';
|
||||||
|
import { FoodItem, FoodLibraryModal } from './model/food/FoodLibraryModal';
|
||||||
|
|
||||||
export type NutritionRadarCardProps = {
|
export type NutritionRadarCardProps = {
|
||||||
nutritionSummary: NutritionSummary | null;
|
nutritionSummary: NutritionSummary | null;
|
||||||
@@ -37,6 +38,8 @@ export function NutritionRadarCard({
|
|||||||
resetToken,
|
resetToken,
|
||||||
onMealPress
|
onMealPress
|
||||||
}: NutritionRadarCardProps) {
|
}: NutritionRadarCardProps) {
|
||||||
|
const [showFoodLibrary, setShowFoodLibrary] = useState(false);
|
||||||
|
const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||||
const radarValues = useMemo(() => {
|
const radarValues = useMemo(() => {
|
||||||
// 基于推荐日摄入量计算分数
|
// 基于推荐日摄入量计算分数
|
||||||
const recommendations = {
|
const recommendations = {
|
||||||
@@ -111,15 +114,24 @@ export function NutritionRadarCard({
|
|||||||
router.push(ROUTES.NUTRITION_RECORDS);
|
router.push(ROUTES.NUTRITION_RECORDS);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddFood = () => {
|
||||||
|
setShowFoodLibrary(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFood = (food: FoodItem) => {
|
||||||
|
console.log('选择了食物:', food);
|
||||||
|
// 这里可以添加将食物添加到营养记录的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<Text style={styles.cardTitle}>营养摄入分析</Text>
|
<Text style={styles.cardTitle}>营养摄入分析</Text>
|
||||||
<View style={styles.cardRightContainer}>
|
<View style={styles.cardRightContainer}>
|
||||||
<Text style={styles.cardSubtitle}>更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text>
|
<Text style={styles.cardSubtitle}>更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text>
|
||||||
<View style={styles.addButton}>
|
<TouchableOpacity style={styles.addButton} onPress={handleAddFood}>
|
||||||
<Ionicons name="add" size={12} color="#FFFFFF" />
|
<Ionicons name="add" size={12} color="#FFFFFF" />
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -180,6 +192,14 @@ export function NutritionRadarCard({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 食物库弹窗 */}
|
||||||
|
<FoodLibraryModal
|
||||||
|
visible={showFoodLibrary}
|
||||||
|
onClose={() => setShowFoodLibrary(false)}
|
||||||
|
onSelectFood={handleSelectFood}
|
||||||
|
mealType={currentMealType}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
445
components/model/food/FoodLibraryModal.tsx
Normal file
445
components/model/food/FoodLibraryModal.tsx
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
StatusBar,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
// 食物数据类型
|
||||||
|
export interface FoodItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
emoji: string;
|
||||||
|
calories: number;
|
||||||
|
unit: string; // 单位,如 "100克"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 食物分类类型
|
||||||
|
export interface FoodCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
foods: FoodItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 食物库弹窗属性
|
||||||
|
export interface FoodLibraryModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelectFood: (food: FoodItem) => void;
|
||||||
|
mealType?: 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟食物数据
|
||||||
|
const FOOD_DATA: FoodCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'common',
|
||||||
|
name: '常见',
|
||||||
|
foods: [
|
||||||
|
{ id: '1', name: '无糖美式咖啡', emoji: '☕', calories: 1, unit: '100克' },
|
||||||
|
{ id: '2', name: '荷包蛋(油煎)', emoji: '🍳', calories: 195, unit: '100克' },
|
||||||
|
{ id: '3', name: '鸡蛋', emoji: '🥚', calories: 139, unit: '100克' },
|
||||||
|
{ id: '4', name: '香蕉', emoji: '🍌', calories: 93, unit: '100克' },
|
||||||
|
{ id: '5', name: '猕猴桃', emoji: '🥝', calories: 61, unit: '100克' },
|
||||||
|
{ id: '6', name: '苹果', emoji: '🍎', calories: 53, unit: '100克' },
|
||||||
|
{ id: '7', name: '草莓', emoji: '🍓', calories: 32, unit: '100克' },
|
||||||
|
{ id: '8', name: '蛋烧麦', emoji: '🥟', calories: 157, unit: '100克' },
|
||||||
|
{ id: '9', name: '米饭', emoji: '🍚', calories: 116, unit: '100克' },
|
||||||
|
{ id: '10', name: '鲜玉米', emoji: '🌽', calories: 112, unit: '100克' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom',
|
||||||
|
name: '自定义',
|
||||||
|
foods: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'favorite',
|
||||||
|
name: '收藏',
|
||||||
|
foods: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fruits',
|
||||||
|
name: '水果蔬菜',
|
||||||
|
foods: [
|
||||||
|
{ id: '11', name: '苹果', emoji: '🍎', calories: 53, unit: '100克' },
|
||||||
|
{ id: '12', name: '香蕉', emoji: '🍌', calories: 93, unit: '100克' },
|
||||||
|
{ id: '13', name: '草莓', emoji: '🍓', calories: 32, unit: '100克' },
|
||||||
|
{ id: '14', name: '猕猴桃', emoji: '🥝', calories: 61, unit: '100克' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meat',
|
||||||
|
name: '肉蛋奶',
|
||||||
|
foods: [
|
||||||
|
{ id: '15', name: '鸡蛋', emoji: '🥚', calories: 139, unit: '100克' },
|
||||||
|
{ id: '16', name: '荷包蛋(油煎)', emoji: '🍳', calories: 195, unit: '100克' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'beans',
|
||||||
|
name: '豆类坚果',
|
||||||
|
foods: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'drinks',
|
||||||
|
name: '零食饮料',
|
||||||
|
foods: [
|
||||||
|
{ id: '17', name: '无糖美式咖啡', emoji: '☕', calories: 1, unit: '100克' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'staple',
|
||||||
|
name: '主食',
|
||||||
|
foods: [
|
||||||
|
{ id: '18', name: '米饭', emoji: '🍚', calories: 116, unit: '100克' },
|
||||||
|
{ id: '19', name: '鲜玉米', emoji: '🌽', calories: 112, unit: '100克' },
|
||||||
|
{ id: '20', name: '蛋烧麦', emoji: '🥟', calories: 157, unit: '100克' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vegetables',
|
||||||
|
name: '菜肴',
|
||||||
|
foods: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 餐次映射
|
||||||
|
const MEAL_TYPE_MAP = {
|
||||||
|
breakfast: '早餐',
|
||||||
|
lunch: '午餐',
|
||||||
|
dinner: '晚餐',
|
||||||
|
snack: '加餐'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FoodLibraryModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onSelectFood,
|
||||||
|
mealType = 'breakfast'
|
||||||
|
}: FoodLibraryModalProps) {
|
||||||
|
const [selectedCategoryId, setSelectedCategoryId] = useState('common');
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
|
// 获取当前选中的分类
|
||||||
|
const selectedCategory = FOOD_DATA.find(cat => cat.id === selectedCategoryId);
|
||||||
|
|
||||||
|
// 过滤食物列表
|
||||||
|
const filteredFoods = selectedCategory?.foods.filter(food =>
|
||||||
|
food.name.toLowerCase().includes(searchText.toLowerCase())
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
// 处理食物选择
|
||||||
|
const handleSelectFood = (food: FoodItem) => {
|
||||||
|
onSelectFood(food);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
presentationStyle="pageSheet"
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar barStyle="dark-content" backgroundColor="#F8F9FA" />
|
||||||
|
|
||||||
|
{/* 头部 */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.backButton}>
|
||||||
|
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>食物库</Text>
|
||||||
|
<TouchableOpacity style={styles.customButton}>
|
||||||
|
<Text style={styles.customButtonText}>自定义</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<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}>
|
||||||
|
<View style={styles.mainContent}>
|
||||||
|
{/* 左侧分类导航 */}
|
||||||
|
<View style={styles.categoryContainer}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
{FOOD_DATA.map((category) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={category.id}
|
||||||
|
style={[
|
||||||
|
styles.categoryItem,
|
||||||
|
selectedCategoryId === category.id && styles.categoryItemActive
|
||||||
|
]}
|
||||||
|
onPress={() => setSelectedCategoryId(category.id)}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.categoryText,
|
||||||
|
selectedCategoryId === category.id && styles.categoryTextActive
|
||||||
|
]}>
|
||||||
|
{category.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧食物列表 */}
|
||||||
|
<View style={styles.foodContainer}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
{filteredFoods.map((food) => (
|
||||||
|
<View key={food.id} style={styles.foodItem}>
|
||||||
|
<View style={styles.foodInfo}>
|
||||||
|
<Text style={styles.foodEmoji}>{food.emoji}</Text>
|
||||||
|
<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 && (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<Text style={styles.emptyText}>暂无食物数据</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部餐次选择和记录按钮 */}
|
||||||
|
<View style={styles.bottomContainer}>
|
||||||
|
<View style={styles.mealSelector}>
|
||||||
|
<View style={styles.mealIndicator} />
|
||||||
|
<Text style={styles.mealText}>{MEAL_TYPE_MAP[mealType]}</Text>
|
||||||
|
<Ionicons name="chevron-down" size={16} color="#333" />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.recordButton}>
|
||||||
|
<Text style={styles.recordButtonText}>记录</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E5E5',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
customButton: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
customButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#4CAF50',
|
||||||
|
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',
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#E5E5E5',
|
||||||
|
},
|
||||||
|
categoryItem: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
categoryItemActive: {
|
||||||
|
backgroundColor: '#F0F9FF',
|
||||||
|
borderRightWidth: 2,
|
||||||
|
borderRightColor: '#4CAF50',
|
||||||
|
},
|
||||||
|
categoryText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#666',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
categoryTextActive: {
|
||||||
|
color: '#4CAF50',
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
foodEmoji: {
|
||||||
|
fontSize: 32,
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
foodDetails: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
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: '#4CAF50',
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
recordButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#FFF',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
109
components/model/food/README.md
Normal file
109
components/model/food/README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 食物库弹窗组件
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
食物库弹窗组件 (`FoodLibraryModal`) 是一个完整的食物选择界面,用户可以通过分类浏览和搜索来选择食物。
|
||||||
|
|
||||||
|
## 主要特性
|
||||||
|
|
||||||
|
- ✅ 完全按照设计图还原的UI界面
|
||||||
|
- ✅ 左侧分类导航,右侧食物列表
|
||||||
|
- ✅ 搜索功能
|
||||||
|
- ✅ 食物信息显示(名称、卡路里、单位)
|
||||||
|
- ✅ 餐次选择(早餐、午餐、晚餐、加餐)
|
||||||
|
- ✅ 自定义和收藏功能预留
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 1. 在营养卡片中使用
|
||||||
|
|
||||||
|
在 `statistics.tsx` 页面的营养摄入分析卡片右上角,点击绿色的加号按钮即可打开食物库弹窗。
|
||||||
|
|
||||||
|
### 2. 组件集成
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { FoodLibraryModal, FoodItem } from '@/components/model/food/FoodLibraryModal';
|
||||||
|
|
||||||
|
// 在组件中使用
|
||||||
|
const [showFoodLibrary, setShowFoodLibrary] = useState(false);
|
||||||
|
|
||||||
|
const handleSelectFood = (food: FoodItem) => {
|
||||||
|
console.log('选择了食物:', food);
|
||||||
|
// 处理食物选择逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
<FoodLibraryModal
|
||||||
|
visible={showFoodLibrary}
|
||||||
|
onClose={() => setShowFoodLibrary(false)}
|
||||||
|
onSelectFood={handleSelectFood}
|
||||||
|
mealType="breakfast"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
model/
|
||||||
|
food/
|
||||||
|
├── FoodLibraryModal.tsx # 主要弹窗组件
|
||||||
|
└── index.ts # 导出文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
### FoodItem
|
||||||
|
```tsx
|
||||||
|
interface FoodItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
emoji: string;
|
||||||
|
calories: number;
|
||||||
|
unit: string; // 如 "100克"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FoodCategory
|
||||||
|
```tsx
|
||||||
|
interface FoodCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
foods: FoodItem[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 食物分类
|
||||||
|
|
||||||
|
- 常见
|
||||||
|
- 自定义
|
||||||
|
- 收藏
|
||||||
|
- 水果蔬菜
|
||||||
|
- 肉蛋奶
|
||||||
|
- 豆类坚果
|
||||||
|
- 零食饮料
|
||||||
|
- 主食
|
||||||
|
- 菜肴
|
||||||
|
|
||||||
|
## 已实现的功能
|
||||||
|
|
||||||
|
1. **分类浏览** - 左侧导航可切换不同食物分类
|
||||||
|
2. **搜索功能** - 顶部搜索框支持食物名称搜索
|
||||||
|
3. **食物选择** - 点击右侧加号按钮选择食物
|
||||||
|
4. **餐次指示** - 底部显示当前餐次(早餐、午餐、晚餐、加餐)
|
||||||
|
5. **UI动画** - 模态弹窗的滑入滑出动画
|
||||||
|
|
||||||
|
## 待扩展功能
|
||||||
|
|
||||||
|
1. **自定义食物** - 用户可以添加自定义食物
|
||||||
|
2. **收藏功能** - 用户可以收藏常用食物
|
||||||
|
3. **营养详情** - 显示更详细的营养成分信息
|
||||||
|
4. **数据持久化** - 将选择的食物保存到营养记录中
|
||||||
|
5. **餐次切换** - 底部餐次选择器的交互功能
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
- 使用 React Native Modal 实现全屏弹窗
|
||||||
|
- 使用 ScrollView 实现分类和食物列表的滚动
|
||||||
|
- 使用 TouchableOpacity 实现交互反馈
|
||||||
|
- 使用 Ionicons 图标库
|
||||||
|
- 完全响应式设计,适配不同屏幕尺寸
|
||||||
2
components/model/food/index.ts
Normal file
2
components/model/food/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { FoodLibraryModal } from './FoodLibraryModal';
|
||||||
|
export type { FoodCategory, FoodItem, FoodLibraryModalProps } from './FoodLibraryModal';
|
||||||
@@ -65,10 +65,8 @@ export const WeightRecordCard: React.FC<WeightRecordCardProps> = ({
|
|||||||
rightThreshold={40}
|
rightThreshold={40}
|
||||||
overshootRight={false}
|
overshootRight={false}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<View
|
||||||
style={styles.recordCard}
|
style={styles.recordCard}
|
||||||
onPress={() => onPress?.(record)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
>
|
||||||
<View style={styles.recordHeader}>
|
<View style={styles.recordHeader}>
|
||||||
<Text style={styles.recordDateTime}>
|
<Text style={styles.recordDateTime}>
|
||||||
@@ -103,7 +101,7 @@ export const WeightRecordCard: React.FC<WeightRecordCardProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
</Swipeable>
|
</Swipeable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -350,7 +350,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -388,7 +388,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -463,10 +463,7 @@
|
|||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
@@ -521,10 +518,7 @@
|
|||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = false;
|
USE_HERMES = false;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.5</string>
|
<string>1.0.6</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user