diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 1d123ba..48b1ceb 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -4,9 +4,10 @@ import { NutritionSummary } from '@/services/dietRecords'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; 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 { RadarCategory, RadarChart } from './RadarChart'; +import { FoodItem, FoodLibraryModal } from './model/food/FoodLibraryModal'; export type NutritionRadarCardProps = { nutritionSummary: NutritionSummary | null; @@ -37,6 +38,8 @@ export function NutritionRadarCard({ resetToken, onMealPress }: NutritionRadarCardProps) { + const [showFoodLibrary, setShowFoodLibrary] = useState(false); + const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const radarValues = useMemo(() => { // 基于推荐日摄入量计算分数 const recommendations = { @@ -111,15 +114,24 @@ export function NutritionRadarCard({ router.push(ROUTES.NUTRITION_RECORDS); }; + const handleAddFood = () => { + setShowFoodLibrary(true); + }; + + const handleSelectFood = (food: FoodItem) => { + console.log('选择了食物:', food); + // 这里可以添加将食物添加到营养记录的逻辑 + }; + return ( 营养摄入分析 更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')} - + - + @@ -180,6 +192,14 @@ export function NutritionRadarCard({ + + {/* 食物库弹窗 */} + setShowFoodLibrary(false)} + onSelectFood={handleSelectFood} + mealType={currentMealType} + /> ); } diff --git a/components/model/food/FoodLibraryModal.tsx b/components/model/food/FoodLibraryModal.tsx new file mode 100644 index 0000000..8e1453c --- /dev/null +++ b/components/model/food/FoodLibraryModal.tsx @@ -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 ( + + + + + {/* 头部 */} + + + + + 食物库 + + 自定义 + + + + {/* 搜索框 */} + + + + + + {/* 主要内容区域 - 卡片样式 */} + + + {/* 左侧分类导航 */} + + + {FOOD_DATA.map((category) => ( + setSelectedCategoryId(category.id)} + > + + {category.name} + + + ))} + + + + {/* 右侧食物列表 */} + + + {filteredFoods.map((food) => ( + + + {food.emoji} + + {food.name} + + {food.calories}千卡/{food.unit} + + + + handleSelectFood(food)} + > + + + + ))} + + {filteredFoods.length === 0 && ( + + 暂无食物数据 + + )} + + + + + + {/* 底部餐次选择和记录按钮 */} + + + + {MEAL_TYPE_MAP[mealType]} + + + + + 记录 + + + + + ); +} + +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', + }, +}); \ No newline at end of file diff --git a/components/model/food/README.md b/components/model/food/README.md new file mode 100644 index 0000000..e4432f2 --- /dev/null +++ b/components/model/food/README.md @@ -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); + // 处理食物选择逻辑 +}; + + 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 图标库 +- 完全响应式设计,适配不同屏幕尺寸 \ No newline at end of file diff --git a/components/model/food/index.ts b/components/model/food/index.ts new file mode 100644 index 0000000..b2467a1 --- /dev/null +++ b/components/model/food/index.ts @@ -0,0 +1,2 @@ +export { FoodLibraryModal } from './FoodLibraryModal'; +export type { FoodCategory, FoodItem, FoodLibraryModalProps } from './FoodLibraryModal'; diff --git a/components/weight/WeightRecordCard.tsx b/components/weight/WeightRecordCard.tsx index 3247866..239f033 100644 --- a/components/weight/WeightRecordCard.tsx +++ b/components/weight/WeightRecordCard.tsx @@ -65,10 +65,8 @@ export const WeightRecordCard: React.FC = ({ rightThreshold={40} overshootRight={false} > - onPress?.(record)} - activeOpacity={0.7} > @@ -103,7 +101,7 @@ export const WeightRecordCard: React.FC = ({ )} - + ); }; diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index 92928d7..d6a1fd1 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -388,7 +388,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -463,10 +463,7 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -521,10 +518,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = false; diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index 2d7390d..c428f82 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.5 + 1.0.6 CFBundleSignature ???? CFBundleURLTypes