Files
digital-pilates/app/food/analysis-result.tsx
richarjiang acb3907344 Refactor: Remove background task management and related hooks
- Deleted `useBackgroundTasks.ts` hook and its associated logic for managing background tasks.
- Removed `backgroundTaskManager.ts` service and all related task definitions and registrations.
- Cleaned up `Podfile.lock` and `package.json` to remove unused dependencies related to background tasks.
- Updated iOS project files to eliminate references to removed background task components.
- Added new background fetch identifier in `Info.plist` for future use.
2025-09-05 09:47:49 +08:00

1262 lines
34 KiB
TypeScript
Raw Permalink 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 { CircularRing } from '@/components/CircularRing';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes';
import { useAppSelector } from '@/hooks/redux';
import { addDietRecord, type CreateDietRecordDto, type MealType } from '@/services/dietRecords';
import { selectFoodRecognitionResult } from '@/store/foodRecognitionSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
BackHandler,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import ImageViewing from 'react-native-image-viewing';
// 模拟食物摄入列表数据
const mockFoodItems = [
{
id: '1',
name: '每日豆奶',
emoji: '🥛',
amount: 190,
unit: 'g',
calories: 80,
protein: 4.6,
carbohydrate: 4.0,
fat: 4.2,
},
{
id: '2',
name: '全麦面包',
emoji: '🍞',
amount: 50,
unit: 'g',
calories: 120,
protein: 4.0,
carbohydrate: 22.0,
fat: 2.0,
},
{
id: '3',
name: '香蕉',
emoji: '🍌',
amount: 100,
unit: 'g',
calories: 89,
protein: 1.1,
carbohydrate: 23.0,
fat: 0.3,
}
];
// 餐次映射
const MEAL_TYPE_MAP = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐'
};
export default function FoodAnalysisResultScreen() {
const router = useRouter();
const params = useLocalSearchParams<{
imageUri?: string;
mealType?: string;
recognitionId?: string;
hideRecordBar?: string;
}>();
const [foodItems, setFoodItems] = useState(mockFoodItems);
const [currentMealType, setCurrentMealType] = useState<MealType>((params.mealType as MealType) || 'breakfast');
const [showMealSelector, setShowMealSelector] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
const [animationTrigger, setAnimationTrigger] = useState(0);
const [editingFood, setEditingFood] = useState<string | null>(null);
const [editFormData, setEditFormData] = useState({ name: '', amount: '', calories: '' });
const { imageUri, recognitionId, hideRecordBar } = params;
// 判断是否隐藏记录栏(默认显示)
const shouldHideRecordBar = hideRecordBar === 'true';
// 从 Redux 获取识别结果
const recognitionResult = useAppSelector(recognitionId ? selectFoodRecognitionResult(recognitionId) : () => null);
// 处理Android返回键关闭图片预览
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
if (showImagePreview) {
setShowImagePreview(false);
return true; // 阻止默认返回行为
}
return false;
});
return () => backHandler.remove();
}, [showImagePreview]);
// 处理识别结果数据
useEffect(() => {
if (recognitionResult) {
// 检查是否识别到食物
if (!recognitionResult.isFoodDetected) {
// 非食物检测情况,清空食物列表
setFoodItems([]);
return;
}
// 将识别结果转换为食物项格式
const convertedItems = recognitionResult.items.map((item, index) => ({
id: item.id || `item-${index}`,
name: item.foodName,
emoji: getRandomFoodEmoji(), // 使用随机emoji
amount: parseInt(item.portion.match(/\d+/)?.[0] || '100'),
unit: item.portion.includes('g') ? 'g' : item.portion.includes('ml') ? 'ml' : 'g',
calories: item.calories,
protein: item.nutritionData.proteinGrams || 0,
carbohydrate: item.nutritionData.carbohydrateGrams || 0,
fat: item.nutritionData.fatGrams || 0,
}));
setFoodItems(convertedItems);
// 如果识别结果中有餐次信息,更新餐次
if (recognitionResult.items.length > 0 && recognitionResult.items[0].mealType) {
const mealTypeFromApi = recognitionResult.items[0].mealType as MealType;
if (['breakfast', 'lunch', 'dinner', 'snack'].includes(mealTypeFromApi)) {
setCurrentMealType(mealTypeFromApi);
}
}
// 触发营养圆环动画
setAnimationTrigger(prev => prev + 1);
}
}, [recognitionResult]);
// 当食物项发生变化时也触发动画
useEffect(() => {
if (foodItems.length > 0) {
setAnimationTrigger(prev => prev + 1);
}
}, [foodItems]);
const handleSaveToDiary = async () => {
if (isRecording || foodItems.length === 0) return;
setIsRecording(true);
try {
// 逐个记录所有食物
for (const item of foodItems) {
const dietRecordData: CreateDietRecordDto = {
mealType: currentMealType,
foodName: item.name,
foodDescription: `${item.amount}${item.unit}`,
portionDescription: `${item.amount}${item.unit}`,
estimatedCalories: item.calories,
proteinGrams: item.protein,
carbohydrateGrams: item.carbohydrate,
fatGrams: item.fat,
source: 'vision',
mealTime: new Date().toISOString(),
imageUrl: imageUri,
};
await addDietRecord(dietRecordData);
}
router.replace(ROUTES.TAB_STATISTICS)
} catch (error) {
console.error('记录饮食失败:', error);
} finally {
setIsRecording(false);
}
};
// 计算所有食物的总营养数据
const totalCalories = foodItems.reduce((sum, item) => sum + item.calories, 0);
const totalProtein = foodItems.reduce((sum, item) => sum + item.protein, 0);
const totalCarbohydrate = foodItems.reduce((sum, item) => sum + item.carbohydrate, 0);
const totalFat = foodItems.reduce((sum, item) => sum + item.fat, 0);
// 计算营养比例(基于推荐日摄入量)
const proteinPercentage = Math.round((totalProtein / 50) * 100); // 推荐50g
const fatPercentage = Math.round((totalFat / 65) * 100); // 推荐65g
const carbohydratePercentage = Math.round((totalCarbohydrate / 300) * 100); // 推荐300g
// 删除食物项
const handleRemoveFood = (itemId: string) => {
setFoodItems(prev => prev.filter(item => item.id !== itemId));
};
// 打开编辑弹窗
const handleEditFood = (item: typeof mockFoodItems[0]) => {
setEditFormData({
name: item.name,
amount: item.amount.toString(),
calories: item.calories.toString(),
});
setEditingFood(item.id);
};
// 保存编辑
const handleSaveEdit = () => {
if (!editingFood) return;
const amount = parseFloat(editFormData.amount) || 0;
const calories = parseFloat(editFormData.calories) || 0;
setFoodItems(prev => prev.map(item =>
item.id === editingFood
? { ...item, name: editFormData.name, amount, calories }
: item
));
setEditingFood(null);
setEditFormData({ name: '', amount: '', calories: '' });
};
// 关闭编辑弹窗
const handleCloseEdit = () => {
setEditingFood(null);
setEditFormData({ name: '', amount: '', calories: '' });
};
// 获取随机食物emoji
function getRandomFoodEmoji(): string {
const foodEmojis = ['🍎', '🍌', '🍞', '🥛', '🥗', '🍗', '🍖', '🥕', '🥦', '🥬', '🍅', '🥒', '🍇', '🥝', '🍓'];
return foodEmojis[Math.floor(Math.random() * foodEmojis.length)];
}
// 处理餐次选择
const handleMealTypeSelect = (selectedMealType: MealType) => {
setCurrentMealType(selectedMealType);
setShowMealSelector(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' },
];
if (!imageUri && !recognitionResult) {
return (
<View style={styles.container}>
<HeaderBar
title="分析结果"
onBack={() => router.back()}
/>
<View style={styles.errorContainer}>
<Text style={styles.errorText}></Text>
</View>
</View>
);
}
return (
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#ffffffff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<HeaderBar
title="分析结果"
onBack={() => router.back()}
transparent={true}
/>
<ScrollView style={styles.scrollContainer} showsVerticalScrollIndicator={false}>
{/* 食物主图 */}
<View style={styles.imageContainer}>
{imageUri ? (
<TouchableOpacity
onPress={() => setShowImagePreview(true)}
activeOpacity={0.9}
>
<Image
source={{ uri: imageUri }}
style={styles.foodImage}
cachePolicy={'memory-disk'}
/>
{/* 预览提示图标 */}
<View style={styles.previewHint}>
<Ionicons name="expand-outline" size={20} color="#FFF" />
</View>
</TouchableOpacity>
) : (
<View style={styles.placeholderContainer}>
<View style={styles.placeholderContent}>
<Ionicons name="restaurant-outline" size={48} color="#666" />
<Text style={styles.placeholderText}></Text>
</View>
</View>
)}
{/* 识别信息气泡 */}
<View style={styles.descriptionBubble}>
<Text style={styles.descriptionText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
dayjs().format('YYYY年M月D日')
}
</Text>
</View>
</View>
{/* 营养信息卡片 */}
<View style={styles.nutritionCard}>
{/* 卡路里 */}
<View style={styles.calorieSection}>
<Text style={styles.calorieValue}>{totalCalories}</Text>
<Text style={styles.calorieUnit}></Text>
</View>
{/* 营养圆环图 */}
<View style={styles.nutritionRings}>
<NutritionRing
label="蛋白质"
value={totalProtein.toFixed(1)}
unit="克"
percentage={Math.min(100, proteinPercentage)}
color="#4CAF50"
resetToken={animationTrigger}
/>
<NutritionRing
label="脂肪"
value={totalFat.toFixed(1)}
unit="克"
percentage={Math.min(100, fatPercentage)}
color="#FF9800"
resetToken={animationTrigger}
/>
<NutritionRing
label="碳水"
value={totalCarbohydrate.toFixed(1)}
unit="克"
percentage={Math.min(100, carbohydratePercentage)}
color="#2196F3"
resetToken={animationTrigger}
/>
</View>
{/* 食物摄入部分 */}
<View style={styles.foodIntakeSection}>
<Text style={styles.foodIntakeTitle}>
{recognitionResult ? '识别结果' : '食物摄入'}
</Text>
{recognitionResult && recognitionResult.analysisText && (
<Text style={styles.analysisText}>{recognitionResult.analysisText}</Text>
)}
{/* 非食物检测提示 */}
{recognitionResult && !recognitionResult.isFoodDetected && (
<View style={styles.nonFoodContainer}>
<View style={styles.nonFoodIcon}>
<Ionicons name="alert-circle-outline" size={48} color="#FF9800" />
</View>
<Text style={styles.nonFoodTitle}></Text>
<Text style={styles.nonFoodMessage}>
{recognitionResult.nonFoodMessage || recognitionResult.analysisText}
</Text>
<View style={styles.nonFoodSuggestions}>
<Text style={styles.nonFoodSuggestionsTitle}></Text>
<Text style={styles.nonFoodSuggestionItem}> </Text>
<Text style={styles.nonFoodSuggestionItem}> </Text>
<Text style={styles.nonFoodSuggestionItem}> 线</Text>
</View>
</View>
)}
{/* 食物列表 */}
{foodItems.length > 0 && foodItems.map((item, index) => (
<View key={item.id} style={[styles.foodIntakeRow, index > 0 && { marginTop: 8 }]}>
<View style={styles.foodIntakeInfo}>
<View style={styles.foodIntakeIcon}>
<Text style={styles.foodIconEmoji}>{item.emoji}</Text>
</View>
<View style={styles.foodIntakeDetails}>
<Text style={styles.foodIntakeName}>{item.name}</Text>
<Text style={styles.foodIntakeAmount}>{item.amount}{item.unit}</Text>
</View>
</View>
<View style={styles.foodIntakeCalories}>
<Text style={styles.foodIntakeCaloriesValue}>{item.calories}</Text>
{shouldHideRecordBar ? null : <TouchableOpacity
style={styles.editButton}
onPress={() => handleEditFood(item)}
>
<Ionicons name="create-outline" size={16} color="#666" />
</TouchableOpacity>}
{shouldHideRecordBar ? null : <TouchableOpacity
style={styles.deleteButton}
onPress={() => handleRemoveFood(item.id)}
>
<Ionicons name="trash-outline" size={16} color="#666" />
</TouchableOpacity>}
</View>
</View>
))}
</View>
</View>
</ScrollView>
{/* 底部餐次选择和记录按钮 */}
{!shouldHideRecordBar && (
recognitionResult && !recognitionResult.isFoodDetected ? (
// 非食物检测情况显示重新拍照按钮
<View style={styles.bottomContainer}>
<TouchableOpacity
style={styles.retakePhotoButton}
onPress={() => router.back()}
activeOpacity={0.8}
>
<Ionicons name="camera-outline" size={20} color={Colors.light.onPrimary} style={{ marginRight: 8 }} />
<Text style={styles.retakePhotoButtonText}></Text>
</TouchableOpacity>
</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 as keyof typeof MEAL_TYPE_MAP]}</Text>
<Ionicons name="chevron-down" size={16} color="#333" />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.recordButton,
(isRecording || foodItems.length === 0) && styles.recordButtonDisabled
]}
disabled={isRecording || foodItems.length === 0}
onPress={handleSaveToDiary}
>
{isRecording ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
<Text style={styles.recordButtonText}></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>
{/* 图片预览 */}
<ImageViewing
images={imageUri ? [{ uri: imageUri }] : []}
imageIndex={0}
visible={showImagePreview}
onRequestClose={() => {
console.log('ImageViewing onRequestClose called');
setShowImagePreview(false);
}}
swipeToCloseEnabled={true}
doubleTapToZoomEnabled={true}
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
dayjs().format('YYYY年M月D日 HH:mm')
}
</Text>
</View>
)}
FooterComponent={() => (
<View style={styles.imageViewerFooter}>
<TouchableOpacity
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
</TouchableOpacity>
</View>
)}
/>
{/* 编辑食物弹窗 */}
<FoodEditModal
visible={!!editingFood}
formData={editFormData}
onFormDataChange={setEditFormData}
onClose={handleCloseEdit}
onSave={handleSaveEdit}
/>
</View>
);
}
// 编辑食物弹窗组件
function FoodEditModal({
visible,
formData,
onFormDataChange,
onClose,
onSave
}: {
visible: boolean;
formData: { name: string; amount: string; calories: string };
onFormDataChange: (data: { name: string; amount: string; calories: string }) => void;
onClose: () => void;
onSave: () => void;
}) {
const handleFieldChange = (field: string, value: string) => {
onFormDataChange({ ...formData, [field]: value });
};
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onClose}
>
<Pressable style={styles.modalBackdrop} onPress={onClose} />
<View style={styles.editModalSheet}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}></Text>
{/* 食物名称 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}></Text>
<TextInput
style={styles.editInput}
placeholder="输入食物名称"
placeholderTextColor="#999"
value={formData.name}
onChangeText={(value) => handleFieldChange('name', value)}
autoFocus
/>
</View>
{/* 重量/数量 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}> ()</Text>
<TextInput
style={styles.editInput}
placeholder="输入重量"
placeholderTextColor="#999"
value={formData.amount}
onChangeText={(value) => handleFieldChange('amount', value)}
keyboardType="numeric"
/>
</View>
{/* 卡路里 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}> ()</Text>
<TextInput
style={styles.editInput}
placeholder="输入卡路里"
placeholderTextColor="#999"
value={formData.calories}
onChangeText={(value) => handleFieldChange('calories', value)}
keyboardType="numeric"
/>
</View>
{/* 按钮区域 */}
<View style={styles.modalButtons}>
<TouchableOpacity
onPress={onClose}
style={styles.modalCancelBtn}
>
<Text style={styles.modalCancelText}></Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onSave}
style={[styles.modalSaveBtn, { backgroundColor: Colors.light.primary }]}
>
<Text style={[styles.modalSaveText, { color: Colors.light.onPrimary }]}></Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
// 营养圆环组件
function NutritionRing({
label,
value,
unit,
percentage,
color,
resetToken
}: {
label: string;
value: string | number;
unit?: string;
percentage: number;
color: string;
resetToken?: unknown;
}) {
return (
<View style={styles.nutritionRingContainer}>
<View style={styles.ringWrapper}>
<CircularRing
size={60}
strokeWidth={6}
trackColor="#E2E8F0"
progressColor={color}
progress={percentage / 100}
showCenterText={false}
resetToken={resetToken}
durationMs={1200}
/>
<View style={styles.ringCenter}>
<Text style={styles.ringCenterText}>{percentage}%</Text>
</View>
</View>
<Text style={styles.nutritionRingLabel}>{label}</Text>
<Text style={styles.nutritionRingValue}>
{value}{unit}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5e5fbff',
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 120,
right: 30,
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#0EA5E9',
opacity: 0.08,
},
decorativeCircle2: {
position: 'absolute',
top: 250,
left: -20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#8B74F3',
opacity: 0.06,
},
decorativeCircle3: {
position: 'absolute',
bottom: 200,
right: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
scrollContainer: {
flex: 1,
},
imageContainer: {
position: 'relative',
height: 300,
},
foodImage: {
width: '100%',
height: '100%',
},
descriptionBubble: {
position: 'absolute',
top: 20,
left: 20,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 16,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
descriptionText: {
fontSize: 14,
color: Colors.light.text,
fontWeight: '500',
},
nutritionCard: {
backgroundColor: Colors.light.background,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
height: '100%',
marginTop: -24,
paddingTop: 24,
paddingHorizontal: 20,
paddingBottom: 40,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: -4,
},
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 8,
},
calorieSection: {
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 8,
},
calorieValue: {
fontSize: 32,
fontWeight: '700',
lineHeight: 32,
},
calorieUnit: {
fontSize: 16,
color: Colors.light.textSecondary,
marginLeft: 8,
},
foodName: {
fontSize: 20,
fontWeight: '600',
color: Colors.light.text,
marginBottom: 24,
},
nutritionHeader: {
marginBottom: 16,
},
nutritionTitle: {
fontSize: 18,
fontWeight: '600',
color: Colors.light.text,
textAlign: 'center',
},
nutritionRings: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 32,
paddingVertical: 20,
},
nutritionRingContainer: {
alignItems: 'center',
},
ringWrapper: {
position: 'relative',
marginBottom: 8,
},
ringCenter: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
},
ringCenterText: {
fontSize: 14,
fontWeight: '600',
color: Colors.light.text,
},
nutritionRingLabel: {
fontSize: 14,
color: Colors.light.textSecondary,
fontWeight: '500',
marginBottom: 2,
},
nutritionRingValue: {
fontSize: 12,
color: Colors.light.text,
fontWeight: '600',
},
foodIntakeSection: {
marginBottom: 20,
},
foodIntakeTitle: {
fontSize: 18,
fontWeight: '600',
color: Colors.light.text,
marginBottom: 12,
},
foodIntakeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#F8F9FA',
borderRadius: 12,
},
foodIntakeInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
foodIntakeIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#FFF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
foodIconEmoji: {
fontSize: 20,
},
foodIntakeDetails: {
flex: 1,
},
foodIntakeName: {
fontSize: 16,
fontWeight: '500',
color: Colors.light.text,
marginBottom: 2,
},
foodIntakeAmount: {
fontSize: 14,
color: Colors.light.textSecondary,
},
foodIntakeCalories: {
flexDirection: 'row',
alignItems: 'center',
},
foodIntakeCaloriesValue: {
fontSize: 16,
fontWeight: '600',
color: Colors.light.text,
marginRight: 8,
},
editButton: {
padding: 4,
marginRight: 4,
},
deleteButton: {
padding: 4,
},
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',
},
// 餐次选择弹窗样式
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',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
fontSize: 16,
color: Colors.light.textMuted,
textAlign: 'center',
},
analysisText: {
fontSize: 14,
color: Colors.light.textSecondary,
marginBottom: 12,
lineHeight: 20,
},
// 非食物检测样式
nonFoodContainer: {
backgroundColor: '#FFF8E1',
borderRadius: 16,
padding: 24,
alignItems: 'center',
marginVertical: 16,
borderWidth: 1,
borderColor: '#FFE0B2',
},
nonFoodIcon: {
marginBottom: 16,
},
nonFoodTitle: {
fontSize: 18,
fontWeight: '600',
color: '#E65100',
marginBottom: 8,
textAlign: 'center',
},
nonFoodMessage: {
fontSize: 16,
color: Colors.light.text,
textAlign: 'center',
lineHeight: 24,
marginBottom: 16,
},
nonFoodSuggestions: {
alignSelf: 'stretch',
},
nonFoodSuggestionsTitle: {
fontSize: 16,
fontWeight: '600',
color: Colors.light.text,
marginBottom: 8,
},
nonFoodSuggestionItem: {
fontSize: 14,
color: Colors.light.textSecondary,
marginBottom: 4,
lineHeight: 20,
},
retakePhotoButton: {
backgroundColor: Colors.light.primary,
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 28,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
flex: 1,
shadowColor: Colors.light.primary,
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
},
retakePhotoButtonText: {
color: Colors.light.onPrimary,
fontSize: 16,
fontWeight: '600',
letterSpacing: 0.5,
},
placeholderContainer: {
width: '100%',
height: '100%',
backgroundColor: '#F5F5F5',
alignItems: 'center',
justifyContent: 'center',
},
placeholderContent: {
alignItems: 'center',
},
placeholderText: {
fontSize: 16,
color: '#666',
fontWeight: '500',
marginTop: 8,
},
// 预览提示图标样式
previewHint: {
position: 'absolute',
top: 16,
right: 16,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 20,
padding: 8,
},
// ImageViewing 组件样式
imageViewerHeader: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
zIndex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
imageViewerCloseButton: {
padding: 4,
},
imageViewerHeaderText: {
color: '#FFF',
fontSize: 14,
fontWeight: '500',
flex: 1,
textAlign: 'center',
marginLeft: -28, // 补偿关闭按钮的宽度,保持文字居中
},
imageViewerFooter: {
position: 'absolute',
bottom: 60,
left: 20,
right: 20,
alignItems: 'center',
zIndex: 1,
},
imageViewerFooterButton: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 20,
},
imageViewerFooterButtonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '500',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
// 编辑弹窗样式
editModalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 20,
paddingBottom: 40,
paddingTop: 20,
},
modalHandle: {
width: 36,
height: 4,
backgroundColor: '#E0E0E0',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: '600',
color: '#333333',
marginBottom: 24,
textAlign: 'center',
},
editFieldContainer: {
marginBottom: 20,
},
editFieldLabel: {
fontSize: 16,
fontWeight: '500',
color: '#333333',
marginBottom: 8,
},
editInput: {
height: 50,
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 12,
paddingHorizontal: 16,
fontSize: 16,
backgroundColor: '#FFFFFF',
color: '#333333',
},
modalButtons: {
flexDirection: 'row',
gap: 12,
marginTop: 8,
},
modalCancelBtn: {
flex: 1,
height: 50,
backgroundColor: '#F0F0F0',
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
modalCancelText: {
fontSize: 16,
fontWeight: '600',
color: '#666666',
},
modalSaveBtn: {
flex: 1,
height: 50,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
modalSaveText: {
fontSize: 16,
fontWeight: '600',
},
});