Files
digital-pilates/app/food/analysis-result.tsx
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

1274 lines
36 KiB
TypeScript

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 { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
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,
}
];
export default function FoodAnalysisResultScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
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 MEAL_TYPE_MAP = {
breakfast: t('nutritionRecords.mealTypes.breakfast'),
lunch: t('nutritionRecords.mealTypes.lunch'),
dinner: t('nutritionRecords.mealTypes.dinner'),
snack: t('nutritionRecords.mealTypes.snack'),
other: t('nutritionRecords.mealTypes.other'),
};
// 计算所有食物的总营养数据
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: t('nutritionRecords.mealTypes.breakfast'), color: '#FF6B35' },
{ key: 'lunch' as const, label: t('nutritionRecords.mealTypes.lunch'), color: '#4CAF50' },
{ key: 'dinner' as const, label: t('nutritionRecords.mealTypes.dinner'), color: '#2196F3' },
{ key: 'snack' as const, label: t('nutritionRecords.mealTypes.snack'), color: '#FF9800' },
];
if (!imageUri && !recognitionResult) {
return (
<View style={styles.container}>
<HeaderBar
title={t('foodAnalysisResult.title')}
onBack={() => router.back()}
/>
<View style={{
paddingTop: safeAreaTop
}} />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{t('foodAnalysisResult.error.notFound')}</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={t('foodAnalysisResult.title')}
onBack={() => router.back()}
transparent={true}
/>
<ScrollView style={styles.scrollContainer} contentContainerStyle={{
paddingTop: safeAreaTop
}} 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}>{t('foodAnalysisResult.placeholder')}</Text>
</View>
</View>
)}
{/* 识别信息气泡 */}
<View style={styles.descriptionBubble}>
<Text style={styles.descriptionText}>
{recognitionResult ?
t('foodAnalysisResult.confidence', { value: recognitionResult.confidence }) :
dayjs().format(t('foodAnalysisResult.dateFormats.today'))
}
</Text>
</View>
</View>
{/* 营养信息卡片 */}
<View style={styles.nutritionCard}>
{/* 卡路里 */}
<View style={styles.calorieSection}>
<Text style={styles.calorieValue}>{totalCalories}</Text>
<Text style={styles.calorieUnit}>{t('foodAnalysisResult.nutrients.caloriesUnit')}</Text>
</View>
{/* 营养圆环图 */}
<View style={styles.nutritionRings}>
<NutritionRing
label={t('foodAnalysisResult.nutrients.protein')}
value={totalProtein.toFixed(1)}
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, proteinPercentage)}
color="#4CAF50"
resetToken={animationTrigger}
/>
<NutritionRing
label={t('foodAnalysisResult.nutrients.fat')}
value={totalFat.toFixed(1)}
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, fatPercentage)}
color="#FF9800"
resetToken={animationTrigger}
/>
<NutritionRing
label={t('foodAnalysisResult.nutrients.carbs')}
value={totalCarbohydrate.toFixed(1)}
unit={t('foodAnalysisResult.nutrients.unit')}
percentage={Math.min(100, carbohydratePercentage)}
color="#2196F3"
resetToken={animationTrigger}
/>
</View>
{/* 食物摄入部分 */}
<View style={styles.foodIntakeSection}>
<Text style={styles.foodIntakeTitle}>
{recognitionResult ? t('foodAnalysisResult.sections.recognitionResult') : t('foodAnalysisResult.sections.foodIntake')}
</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}>{t('foodAnalysisResult.nonFood.title')}</Text>
<Text style={styles.nonFoodMessage}>
{recognitionResult.nonFoodMessage || recognitionResult.analysisText}
</Text>
<View style={styles.nonFoodSuggestions}>
<Text style={styles.nonFoodSuggestionsTitle}>{t('foodAnalysisResult.nonFood.suggestions.title')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item1')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item2')}</Text>
<Text style={styles.nonFoodSuggestionItem}>{t('foodAnalysisResult.nonFood.suggestions.item3')}</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}{t('foodAnalysisResult.nutrients.caloriesUnit')}</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}>{t('foodAnalysisResult.actions.retake')}</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}>{t('foodAnalysisResult.actions.record')}</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}>{t('foodAnalysisResult.mealSelector.title')}</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 ?
t('foodAnalysisResult.confidence', { value: recognitionResult.confidence }) :
dayjs().format(t('foodAnalysisResult.dateFormats.full'))
}
</Text>
</View>
)}
FooterComponent={() => (
<View style={styles.imageViewerFooter}>
<TouchableOpacity
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}>{t('foodAnalysisResult.actions.close')}</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 });
};
const { t } = useI18n();
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}>{t('foodAnalysisResult.editModal.title')}</Text>
{/* 食物名称 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.name')}</Text>
<TextInput
style={styles.editInput}
placeholder={t('foodAnalysisResult.editModal.fields.namePlaceholder')}
placeholderTextColor="#999"
value={formData.name}
onChangeText={(value) => handleFieldChange('name', value)}
autoFocus
/>
</View>
{/* 重量/数量 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.amount')}</Text>
<TextInput
style={styles.editInput}
placeholder={t('foodAnalysisResult.editModal.fields.amountPlaceholder')}
placeholderTextColor="#999"
value={formData.amount}
onChangeText={(value) => handleFieldChange('amount', value)}
keyboardType="numeric"
/>
</View>
{/* 卡路里 */}
<View style={styles.editFieldContainer}>
<Text style={styles.editFieldLabel}>{t('foodAnalysisResult.editModal.fields.calories')}</Text>
<TextInput
style={styles.editInput}
placeholder={t('foodAnalysisResult.editModal.fields.caloriesPlaceholder')}
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}>{t('foodAnalysisResult.editModal.actions.cancel')}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onSave}
style={[styles.modalSaveBtn, { backgroundColor: Colors.light.primary }]}
>
<Text style={[styles.modalSaveText, { color: Colors.light.onPrimary }]}>{t('foodAnalysisResult.editModal.actions.save')}</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',
},
});