From 3fdd2acaf2047287d4e2afe65821760bc46745e1 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 29 Aug 2025 21:03:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E9=A3=9F=E7=89=A9?= =?UTF-8?q?=E5=BA=93=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E9=A3=9F=E7=89=A9=E7=9A=84=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=B8=8E=E5=88=A0=E9=99=A4=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/statistics.tsx | 23 ++- app/food-library.tsx | 100 ++++++--- components/NutritionRadarCard.tsx | 35 +--- components/NutritionRecordCard.tsx | 39 +--- .../model/food/CreateCustomFoodModal.tsx | 195 ++++++++++-------- components/model/food/FoodDetailModal.tsx | 82 ++++++-- ios/digitalpilates.xcodeproj/project.pbxproj | 10 +- services/foodLibraryApi.ts | 30 +++ 8 files changed, 301 insertions(+), 213 deletions(-) diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index a48534c..31f85ca 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -345,14 +345,16 @@ export default function ExploreScreen() { {/* 左列 */} - - + pushIfAuthedElseLogin('/mood/calendar')} + isLoading={isMoodLoading} /> + 消耗卡路里 {activeCalories != null ? ( @@ -389,12 +391,11 @@ export default function ExploreScreen() { showLabel={false} /> - {/* 心情卡片 */} - - pushIfAuthedElseLogin('/mood/calendar')} - isLoading={isMoodLoading} + + diff --git a/app/food-library.tsx b/app/food-library.tsx index fc37003..678e4dc 100644 --- a/app/food-library.tsx +++ b/app/food-library.tsx @@ -1,9 +1,11 @@ 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 { useFoodLibrary, useFoodSearch } from '@/hooks/useFoodLibrary'; import { addDietRecord, type CreateDietRecordDto } from '@/services/dietRecords'; +import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi'; import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food'; import { Ionicons } from '@expo/vector-icons'; import { Image } from 'expo-image'; @@ -11,6 +13,7 @@ import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, + Alert, Modal, ScrollView, StyleSheet, @@ -36,7 +39,7 @@ export default function FoodLibraryScreen() { const mealType = (params.mealType as MealType) || 'breakfast'; // Redux hooks - const { categories, loading, error, clearErrors } = useFoodLibrary(); + const { categories, loading, error, clearErrors, loadFoodLibrary } = useFoodLibrary(); const { searchResults, searchLoading, search, clearResults } = useFoodSearch(); // 本地状态 @@ -129,6 +132,20 @@ export default function FoodLibraryScreen() { 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; @@ -182,32 +199,51 @@ export default function FoodLibraryScreen() { }; // 处理保存自定义食物 - const handleSaveCustomFood = (customFoodData: CustomFoodData) => { - // 创建一个临时的FoodItem对象 - const customFoodItem: FoodItem = { - id: `custom_${Date.now()}`, - name: customFoodData.name, - calories: customFoodData.calories, - unit: customFoodData.unit, - description: `自定义食物 - ${customFoodData.name}`, - imageUrl: customFoodData.imageUrl, - protein: customFoodData.protein, - fat: customFoodData.fat, - carbohydrate: customFoodData.carbohydrate, - }; + 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, + }; - // 直接添加到选择列表中 - const newSelectedItem: SelectedFoodItem = { - id: `custom_${Date.now()}`, - food: customFoodItem, - amount: customFoodData.defaultAmount, - unit: customFoodData.unit, - calories: Math.round((customFoodData.calories * customFoodData.defaultAmount) / 100) - }; + // 调用API创建自定义食物 + const createdFood = await foodLibraryApi.createCustomFood(createData); - setSelectedFoodItems(prev => [...prev, newSelectedItem]); + // 需要拉取一遍最新的食物列表 + await loadFoodLibrary(); - console.log('保存自定义食物:', customFoodData); + // 创建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('创建失败', '创建自定义食物时发生错误,请稍后重试'); + } }; // 关闭自定义食物弹窗 @@ -472,8 +508,10 @@ export default function FoodLibraryScreen() { {/* 创建自定义食物弹窗 */} @@ -497,7 +535,7 @@ const styles = StyleSheet.create({ }, customButtonText: { fontSize: 16, - color: '#4CAF50', + color: Colors.light.textSecondary, fontWeight: '500', }, searchContainer: { @@ -546,8 +584,6 @@ const styles = StyleSheet.create({ categoryContainer: { width: 100, backgroundColor: 'transparent', - borderRightWidth: 1, - borderRightColor: '#E5E5E5', }, categoryItem: { paddingVertical: 16, @@ -555,9 +591,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, categoryItemActive: { - backgroundColor: '#F0F9FF', - borderRightWidth: 2, - borderRightColor: '#4CAF50', + backgroundColor: '#F0F9FF' }, categoryText: { fontSize: 14, @@ -565,7 +599,7 @@ const styles = StyleSheet.create({ textAlign: 'center', }, categoryTextActive: { - color: '#4CAF50', + color: Colors.light.text, fontWeight: '500', }, foodContainer: { @@ -711,7 +745,7 @@ const styles = StyleSheet.create({ marginRight: 4, }, recordButton: { - backgroundColor: '#4CAF50', + backgroundColor: Colors.light.primary, paddingHorizontal: 24, paddingVertical: 10, borderRadius: 20, @@ -749,7 +783,7 @@ const styles = StyleSheet.create({ }, totalCalories: { fontSize: 14, - color: '#4CAF50', + color: Colors.light.text, fontWeight: '500', }, selectedFoodsList: { diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 49693dc..67d4be4 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -85,30 +85,6 @@ export function NutritionRadarCard({ const consumedCalories = nutritionSummary?.totalCalories || 0; const remainingCalories = burnedCalories - consumedCalories - calorieDeficit; - // 餐次数据 - const meals = [ - { - type: 'breakfast' as const, - name: '早餐', - emoji: '🥚', - }, - { - type: 'lunch' as const, - name: '午餐', - emoji: '🍔', - }, - { - type: 'dinner' as const, - name: '晚餐', - emoji: '🥣', - }, - { - type: 'snack' as const, - name: '加餐', - emoji: '🍎', - }, - ]; - const handleNavigateToRecords = () => { router.push(ROUTES.NUTRITION_RECORDS); }; @@ -124,7 +100,7 @@ export function NutritionRadarCard({ 更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')} - + @@ -142,7 +118,6 @@ export function NutritionRadarCard({ {nutritionStats.map((stat, index) => ( - {stat.label} {stat.value} @@ -267,7 +242,6 @@ const styles = StyleSheet.create({ }, // 卡路里相关样式 calorieSection: { - marginTop: 12, }, calorieTitleContainer: { @@ -341,10 +315,11 @@ const styles = StyleSheet.create({ fontSize: 24, }, addButton: { - width: 16, - height: 16, + width: 18, + height: 18, borderRadius: 8, - backgroundColor: '#10B981', + backgroundColor: '#9AA3AE', + marginLeft: 8, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx index 45737df..e85ca94 100644 --- a/components/NutritionRecordCard.tsx +++ b/components/NutritionRecordCard.tsx @@ -6,7 +6,6 @@ import dayjs from 'dayjs'; import React, { useMemo, useRef, useState } from 'react'; import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Swipeable } from 'react-native-gesture-handler'; -import Popover from 'react-native-popover-view'; export type NutritionRecordCardProps = { record: DietRecord; @@ -46,7 +45,6 @@ export function NutritionRecordCard({ const surfaceColor = useThemeColor({}, 'surface'); const textColor = useThemeColor({}, 'text'); const textSecondaryColor = useThemeColor({}, 'textSecondary'); - const primaryColor = useThemeColor({}, 'primary'); // Popover 状态管理 const [showPopover, setShowPopover] = useState(false); @@ -80,7 +78,6 @@ export function NutritionRecordCard({ }, [record]); const mealTypeColor = MEAL_TYPE_COLORS[record.mealType]; - const mealTypeIcon = MEAL_TYPE_ICONS[record.mealType]; const mealTypeLabel = MEAL_TYPE_LABELS[record.mealType]; // 处理删除操作 @@ -182,8 +179,8 @@ export function NutritionRecordCard({ {/* 餐次标签 */} - - + + {mealTypeLabel} @@ -201,29 +198,6 @@ export function NutritionRecordCard({ - {/* Popover for more options */} - setShowPopover(false)} - popoverStyle={styles.popoverContainer} - backgroundStyle={styles.popoverBackground} - > - - { - setShowPopover(false); - onDelete?.(); - }} - > - - - 删除记录 - - - - ); } @@ -245,8 +219,6 @@ const styles = StyleSheet.create({ backgroundColor: '#FFFFFF', borderRadius: 12, padding: 12, - - }, mainContent: { flex: 1, @@ -276,13 +248,13 @@ const styles = StyleSheet.create({ }, foodName: { - fontSize: 15, + fontSize: 14, fontWeight: '600', color: '#333333', - marginBottom: 2, + marginTop: 2, }, mealTime: { - fontSize: 12, + fontSize: 10, fontWeight: '400', color: '#999999', }, @@ -324,6 +296,7 @@ const styles = StyleSheet.create({ caloriesText: { fontSize: 12, color: '#473c3cff', + fontWeight: '500', }, mealTypeBadge: { paddingHorizontal: 8, diff --git a/components/model/food/CreateCustomFoodModal.tsx b/components/model/food/CreateCustomFoodModal.tsx index ef30c36..dccbc53 100644 --- a/components/model/food/CreateCustomFoodModal.tsx +++ b/components/model/food/CreateCustomFoodModal.tsx @@ -1,8 +1,9 @@ import { Ionicons } from '@expo/vector-icons'; import { Image } from 'expo-image'; import * as ImagePicker from 'expo-image-picker'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { + ActivityIndicator, Alert, Dimensions, Keyboard, @@ -16,6 +17,9 @@ import { TouchableOpacity, View } from 'react-native'; +import { useAppSelector } from '@/hooks/redux'; +import { useCosUpload } from '@/hooks/useCosUpload'; +import { Colors } from '@/constants/Colors'; export interface CreateCustomFoodModalProps { visible: boolean; @@ -25,7 +29,6 @@ export interface CreateCustomFoodModalProps { export interface CustomFoodData { name: string; - unit: string; defaultAmount: number; caloriesUnit: string; calories: number; @@ -41,7 +44,6 @@ export function CreateCustomFoodModal({ onSave }: CreateCustomFoodModalProps) { const [foodName, setFoodName] = useState(''); - const [foodUnit, setFoodUnit] = useState(''); const [defaultAmount, setDefaultAmount] = useState('100'); const [caloriesUnit, setCaloriesUnit] = useState('千卡'); const [calories, setCalories] = useState('100'); @@ -51,6 +53,20 @@ export function CreateCustomFoodModal({ const [carbohydrate, setCarbohydrate] = useState('0'); const [keyboardHeight, setKeyboardHeight] = useState(0); + // 获取用户ID和上传功能 + const accountProfile = useAppSelector((s) => (s as any)?.user?.profile as any); + const userId: string | undefined = useMemo(() => { + return ( + accountProfile?.userId || + accountProfile?.id || + accountProfile?._id || + accountProfile?.uid || + undefined + ) as string | undefined; + }, [accountProfile]); + + const { upload, uploading } = useCosUpload(); + // 键盘监听 useEffect(() => { const keyboardDidShowListener = Keyboard.addListener( @@ -76,7 +92,6 @@ export function CreateCustomFoodModal({ useEffect(() => { if (visible) { setFoodName(''); - setFoodUnit(''); setDefaultAmount('100'); setCaloriesUnit('千卡'); setCalories('100'); @@ -92,21 +107,41 @@ export function CreateCustomFoodModal({ // 选择图片 const handleSelectImage = async () => { - const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); - if (status !== 'granted') { - Alert.alert('需要相册权限', '请在设置中开启相册权限'); - return; - } - - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: 1, - }); - - if (!result.canceled && result.assets[0]) { - setImageUrl(result.assets[0].uri); + try { + const resp = await ImagePicker.requestMediaLibraryPermissionsAsync(); + const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited'; + if (!libGranted) { + Alert.alert('权限不足', '需要相册权限以选择照片'); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + allowsEditing: true, + quality: 0.9, + aspect: [1, 1], + mediaTypes: ['images'], + base64: false, + }); + + if (!result.canceled) { + const asset = result.assets?.[0]; + if (!asset?.uri) return; + + // 直接上传到 COS,成功后写入 URL + try { + const { url } = await upload( + { uri: asset.uri, name: asset.fileName || 'food.jpg', type: asset.mimeType || 'image/jpeg' }, + { prefix: 'foods/', userId } + ); + + setImageUrl(url); + } catch (e) { + console.warn('上传照片失败', e); + Alert.alert('上传失败', '照片上传失败,请重试'); + } + } + } catch (e) { + Alert.alert('发生错误', '选择照片失败,请重试'); } }; @@ -119,10 +154,7 @@ export function CreateCustomFoodModal({ Alert.alert('提示', '请输入食物名称'); return; } - if (!foodUnit.trim()) { - Alert.alert('提示', '请输入食物单位'); - return; - } + if (!calories.trim() || parseFloat(calories) <= 0) { Alert.alert('提示', '请输入有效的热量值'); return; @@ -130,7 +162,6 @@ export function CreateCustomFoodModal({ const foodData: CustomFoodData = { name: foodName.trim(), - unit: foodUnit.trim(), defaultAmount: parseFloat(defaultAmount) || 100, caloriesUnit, calories: parseFloat(calories) || 0, @@ -182,14 +213,14 @@ export function CreateCustomFoodModal({ 保存 @@ -211,7 +242,7 @@ export function CreateCustomFoodModal({ {foodName || '食物名称'} - {actualCalories}{caloriesUnit}/{defaultAmount}{foodUnit} + {actualCalories}{caloriesUnit}/{defaultAmount}g @@ -226,26 +257,18 @@ export function CreateCustomFoodModal({ {/* 食物名称和单位 */} - - - 食物名称 - - - - 食物单位 - + + 食物名称 + + + + @@ -262,17 +285,11 @@ export function CreateCustomFoodModal({ placeholder="100" placeholderTextColor="#A0A0A0" /> - {foodUnit || '单位'} + g - {/* 热量单位 */} - - 热量单位 - 千卡 - - {/* 食物热量 */} 食物热量 @@ -301,7 +318,11 @@ export function CreateCustomFoodModal({ 照片 - + {imageUrl ? ( ) : ( @@ -310,6 +331,11 @@ export function CreateCustomFoodModal({ 添加照片 )} + {uploading && ( + + + + )} @@ -369,13 +395,6 @@ export function CreateCustomFoodModal({ - - {/* 底部提示 */} - - - *为了避免历史饮食数据混乱,自定义食物创建后不支持修改。 - - @@ -384,7 +403,7 @@ export function CreateCustomFoodModal({ ); } -const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); +const { height: screenHeight } = Dimensions.get('window'); const styles = StyleSheet.create({ overlay: { @@ -410,8 +429,6 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 16, - borderBottomWidth: 1, - borderBottomColor: '#E5E5E5', }, backButton: { padding: 4, @@ -434,15 +451,14 @@ const styles = StyleSheet.create({ }, saveButtonText: { fontSize: 16, - color: '#4CAF50', + color: Colors.light.primary, fontWeight: '500', }, saveButtonTextDisabled: { - color: '#999', + color: Colors.light.textMuted, }, previewSection: { paddingHorizontal: 16, - paddingTop: 20, paddingBottom: 16, }, previewCard: { @@ -489,15 +505,16 @@ const styles = StyleSheet.create({ sectionHeader: { flexDirection: 'row', alignItems: 'center', - marginBottom: 16, + marginBottom: 4, + }, sectionTitle: { - fontSize: 18, - fontWeight: '600', + fontSize: 14, color: '#333', + marginLeft: 8 }, requiredIndicator: { - fontSize: 18, + fontSize: 16, color: '#FF4444', marginLeft: 4, }, @@ -515,16 +532,15 @@ const styles = StyleSheet.create({ inputLabel: { fontSize: 14, color: '#666', - marginBottom: 8, fontWeight: '500', }, modernTextInput: { - borderWidth: 1.5, - borderColor: '#E8E8E8', + flex: 1, borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 14, + paddingHorizontal: 12, + paddingVertical: 8, fontSize: 16, + marginLeft: 20, color: '#333', backgroundColor: '#FFFFFF', shadowColor: '#000', @@ -536,9 +552,8 @@ const styles = StyleSheet.create({ numberInputContainer: { flexDirection: 'row', alignItems: 'center', - borderWidth: 1.5, - borderColor: '#E8E8E8', borderRadius: 12, + backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, @@ -548,8 +563,8 @@ const styles = StyleSheet.create({ }, modernNumberInput: { flex: 1, - paddingHorizontal: 16, - paddingVertical: 14, + paddingHorizontal: 12, + paddingVertical: 8, fontSize: 16, color: '#333', textAlign: 'right', @@ -578,8 +593,8 @@ const styles = StyleSheet.create({ elevation: 1, }, selectButtonText: { - fontSize: 16, - color: '#333', + fontSize: 14, + color: 'gray', fontWeight: '500', }, modernImageSelector: { @@ -695,6 +710,7 @@ const styles = StyleSheet.create({ // 新增行布局样式 inputRowContainer: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, }, @@ -708,4 +724,15 @@ const styles = StyleSheet.create({ inputRowContent: { flex: 1, }, + imageLoadingOverlay: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 16, + }, }); \ No newline at end of file diff --git a/components/model/food/FoodDetailModal.tsx b/components/model/food/FoodDetailModal.tsx index 4812589..7db9792 100644 --- a/components/model/food/FoodDetailModal.tsx +++ b/components/model/food/FoodDetailModal.tsx @@ -1,6 +1,7 @@ import { Ionicons } from '@expo/vector-icons'; import React, { useEffect, useState } from 'react'; import { + Alert, Dimensions, Keyboard, KeyboardAvoidingView, @@ -14,6 +15,7 @@ import { 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'; @@ -38,8 +40,10 @@ const QUICK_SELECT_OPTIONS = [ 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中的数据 @@ -78,8 +82,10 @@ const getNutritionInfo = (food: FoodItem): NutritionInfo => { export function FoodDetailModal({ visible, food, + category, onClose, - onSave + onSave, + onDelete }: FoodDetailModalProps) { const [amount, setAmount] = useState('100'); const [isFavorite, setIsFavorite] = useState(false); @@ -189,9 +195,9 @@ export function FoodDetailModal({ - + {/* 我要纠错 - + */} {/* 食物信息 */} @@ -203,16 +209,44 @@ export function FoodDetailModal({ /> {food.name} - setIsFavorite(!isFavorite)} - style={styles.favoriteButton} - > - - + + {/* setIsFavorite(!isFavorite)} + style={styles.favoriteButton} + > + + */} + {/* 删除按钮 - 仅对自定义食物显示 */} + {category && category.id === 'custom' && onDelete && ( + { + Alert.alert( + '删除食物', + '确定要删除这个自定义食物吗?', + [ + { text: '取消', style: 'cancel' }, + { + text: '删除', + style: 'destructive', + onPress: () => onDelete(food.id) + } + ] + ); + }} + style={styles.deleteButton} + > + + + )} + {/* 营养信息 */} @@ -367,13 +401,20 @@ const styles = StyleSheet.create({ 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, - paddingVertical: 16, justifyContent: 'space-between', }, nutritionItem: { @@ -392,28 +433,29 @@ const styles = StyleSheet.create({ }, amountContainer: { alignItems: 'center', - paddingVertical: 16, }, inputWithUnit: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#E8F5E8', borderRadius: 12, paddingHorizontal: 20, paddingVertical: 16, + }, amountInput: { fontSize: 24, fontWeight: '600', - color: '#4CAF50', + color: Colors.light.text, textAlign: 'center', minWidth: 80, backgroundColor: 'transparent', + borderBottomWidth: 1, + borderBottomColor: 'gray', }, unitLabel: { fontSize: 18, fontWeight: '500', - color: '#4CAF50', + color: Colors.light.text, marginLeft: 8, }, quickSelectContainer: { @@ -432,7 +474,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, quickSelectOptionActive: { - backgroundColor: '#4CAF50', + backgroundColor: Colors.light.primary, }, quickSelectText: { fontSize: 14, @@ -443,7 +485,7 @@ const styles = StyleSheet.create({ color: '#FFFFFF', }, saveButton: { - backgroundColor: '#4CAF50', + backgroundColor: Colors.light.primary, marginHorizontal: screenWidth > 400 ? 24 : 16, marginBottom: 16, paddingVertical: 14, diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index d6a1fd1..15c13e5 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -463,7 +463,10 @@ 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"; @@ -518,7 +521,10 @@ ); 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/services/foodLibraryApi.ts b/services/foodLibraryApi.ts index e7c8512..46d336d 100644 --- a/services/foodLibraryApi.ts +++ b/services/foodLibraryApi.ts @@ -6,6 +6,20 @@ import type { } from '@/types/food'; import { api } from './api'; +export interface CreateCustomFoodDto { + name: string; + description?: string; + caloriesPer100g?: number; + proteinPer100g?: number; + carbohydratePer100g?: number; + fatPer100g?: number; + fiberPer100g?: number; + sugarPer100g?: number; + sodiumPer100g?: number; + additionalNutrition?: Record; + imageUrl?: string; +} + /** * 食物库 API 服务 */ @@ -39,6 +53,20 @@ export class FoodLibraryApi { const { id } = params; return api.get(`${this.BASE_PATH}/${id}`); } + + /** + * 创建自定义食物 + */ + static async createCustomFood(data: CreateCustomFoodDto): Promise { + return api.post(`${this.BASE_PATH}/custom`, data); + } + + /** + * 删除自定义食物 + */ + static async deleteCustomFood(id: number): Promise { + return api.delete(`${this.BASE_PATH}/custom/${id}`); + } } // 导出便捷方法 @@ -46,4 +74,6 @@ export const foodLibraryApi = { getFoodLibrary: () => FoodLibraryApi.getFoodLibrary(), searchFoods: (keyword: string) => FoodLibraryApi.searchFoods({ keyword }), getFoodById: (id: number) => FoodLibraryApi.getFoodById({ id }), + createCustomFood: (data: CreateCustomFoodDto) => FoodLibraryApi.createCustomFood(data), + deleteCustomFood: (id: number) => FoodLibraryApi.deleteCustomFood(id), }; \ No newline at end of file