import { useAppSelector } from '@/hooks/redux'; import { useCosUpload } from '@/hooks/useCosUpload'; import { useI18n } from '@/hooks/useI18n'; import { Ionicons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import { Image } from 'expo-image'; import * as ImagePicker from 'expo-image-picker'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, Dimensions, Keyboard, KeyboardAvoidingView, Modal, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF']; const CTA_DISABLED_GRADIENT: [string, string] = ['#d3d7e8', '#c1c6da']; export interface CreateCustomFoodModalProps { visible: boolean; onClose: () => void; onSave: (foodData: CustomFoodData) => void; } export interface CustomFoodData { name: string; defaultAmount: number; caloriesUnit: string; calories: number; imageUrl?: string; protein?: number; fat?: number; carbohydrate?: number; } export function CreateCustomFoodModal({ visible, onClose, onSave }: CreateCustomFoodModalProps) { const { t } = useI18n(); const [foodName, setFoodName] = useState(''); const [defaultAmount, setDefaultAmount] = useState('100'); const [caloriesUnit, setCaloriesUnit] = useState(t('createCustomFood.units.kcal')); const [calories, setCalories] = useState('100'); const [imageUrl, setImageUrl] = useState(''); const [protein, setProtein] = useState('0'); const [fat, setFat] = useState('0'); 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( Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', (e) => { setKeyboardHeight(e.endCoordinates.height); } ); const keyboardDidHideListener = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => { setKeyboardHeight(0); } ); return () => { keyboardDidShowListener?.remove(); keyboardDidHideListener?.remove(); }; }, []); // 重置表单 useEffect(() => { if (visible) { setFoodName(''); setDefaultAmount('100'); setCaloriesUnit(t('createCustomFood.units.kcal')); setCalories('100'); setImageUrl(''); setProtein('0'); setFat('0'); setCarbohydrate('0'); } }, [visible]); // 选择图片 const handleSelectImage = async () => { try { const resp = await ImagePicker.requestMediaLibraryPermissionsAsync(); const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited'; if (!libGranted) { Alert.alert( t('createCustomFood.alerts.permissionDenied.title'), t('createCustomFood.alerts.permissionDenied.message') ); 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( t('createCustomFood.alerts.uploadFailed.title'), t('createCustomFood.alerts.uploadFailed.message') ); } } } catch (e) { Alert.alert( t('createCustomFood.alerts.error.title'), t('createCustomFood.alerts.error.message') ); } }; // 计算实际热量预览 const actualCalories = Math.round((parseFloat(calories) || 0) * (parseFloat(defaultAmount) || 0) / 100); // 保存自定义食物 const handleSave = () => { if (!foodName.trim()) { Alert.alert( t('createCustomFood.alerts.validation.title'), t('createCustomFood.alerts.validation.nameRequired') ); return; } if (!calories.trim() || parseFloat(calories) <= 0) { Alert.alert( t('createCustomFood.alerts.validation.title'), t('createCustomFood.alerts.validation.caloriesRequired') ); return; } const foodData: CustomFoodData = { name: foodName.trim(), defaultAmount: parseFloat(defaultAmount) || 100, caloriesUnit, calories: parseFloat(calories) || 0, imageUrl: imageUrl || undefined, protein: parseFloat(protein) || undefined, fat: parseFloat(fat) || undefined, carbohydrate: parseFloat(carbohydrate) || undefined, }; onSave(foodData); onClose(); }; const isSaveDisabled = !foodName.trim() || !calories.trim(); return ( 0 && { height: screenHeight - keyboardHeight - 60, maxHeight: screenHeight - keyboardHeight - 60, }, ]} > 0 ? 20 : 40, }} > {/* 头部 */} {t('createCustomFood.title')} {t('createCustomFood.save')} {/* 效果预览区域 */} {t('createCustomFood.preview.title')} {imageUrl ? ( ) : ( )} {foodName || t('createCustomFood.preview.defaultName')} {actualCalories} {caloriesUnit} / {defaultAmount} {t('createCustomFood.units.g')} {/* 基本信息 */} {t('createCustomFood.basicInfo.title')} * {/* 食物名称 */} {t('createCustomFood.basicInfo.name')} {/* 默认数量 */} {t('createCustomFood.basicInfo.defaultAmount')} {t('createCustomFood.units.g')} {/* 食物热量 */} {t('createCustomFood.basicInfo.calories')} {t('createCustomFood.units.kcal')} {/* 可选信息 */} {t('createCustomFood.optionalInfo.title')} {/* 照片 */} {t('createCustomFood.optionalInfo.photo')} {imageUrl ? ( ) : ( {t('createCustomFood.optionalInfo.addPhoto')} )} {uploading && ( )} {/* 蛋白质 */} {t('createCustomFood.optionalInfo.protein')} {t('createCustomFood.units.gram')} {/* 脂肪 */} {t('createCustomFood.optionalInfo.fat')} {t('createCustomFood.units.gram')} {/* 碳水化合物 */} {t('createCustomFood.optionalInfo.carbohydrate')} {t('createCustomFood.units.gram')} ); } const { height: screenHeight } = Dimensions.get('window'); const styles = StyleSheet.create({ overlay: { flex: 1, }, keyboardAvoidingView: { flex: 1, justifyContent: 'flex-end', }, dismissArea: { flex: 1, }, modalContainer: { backgroundColor: '#F1F5F9', // Slate 100 borderTopLeftRadius: 32, borderTopRightRadius: 32, height: '90%', maxHeight: '90%', shadowColor: '#000', shadowOffset: { width: 0, height: -4, }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 20, overflow: 'hidden', }, modalHeaderBar: { width: '100%', height: 24, alignItems: 'center', justifyContent: 'center', backgroundColor: '#F1F5F9', }, dragIndicator: { width: 40, height: 4, backgroundColor: '#CBD5E1', borderRadius: 2, }, scrollView: { flex: 1, backgroundColor: '#F1F5F9', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingBottom: 20, }, backButton: { padding: 4, marginLeft: -8, }, headerTitle: { fontSize: 20, fontWeight: '800', color: '#1E293B', textAlign: 'center', fontFamily: 'AliBold', }, saveButton: { borderRadius: 20, overflow: 'hidden', }, saveButtonDisabled: { opacity: 0.6, }, saveButtonGradient: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, }, saveButtonText: { fontSize: 14, color: '#FFFFFF', fontWeight: '700', fontFamily: 'AliBold', }, previewSection: { paddingHorizontal: 20, marginBottom: 24, }, previewCard: { borderRadius: 24, padding: 20, overflow: 'hidden', backgroundColor: '#FFFFFF', shadowColor: 'rgba(30, 41, 59, 0.08)', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.4, shadowRadius: 12, elevation: 4, borderWidth: 1, borderColor: 'rgba(255,255,255,0.6)', }, previewHeader: { marginBottom: 16, }, previewContent: { flexDirection: 'row', alignItems: 'center', }, imageWrapper: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 4, }, previewImage: { width: 56, height: 56, borderRadius: 16, backgroundColor: '#F8FAFC', }, previewImagePlaceholder: { width: 56, height: 56, borderRadius: 16, backgroundColor: '#F1F5F9', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#E2E8F0', }, previewInfo: { flex: 1, marginLeft: 16, justifyContent: 'center', }, previewName: { fontSize: 18, fontWeight: '700', color: '#1E293B', marginBottom: 6, fontFamily: 'AliBold', }, previewBadge: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFBEB', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, alignSelf: 'flex-start', gap: 4, }, previewCalories: { fontSize: 13, color: '#D97706', fontWeight: '600', fontFamily: 'AliRegular', }, section: { paddingHorizontal: 20, marginBottom: 24, }, sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, paddingHorizontal: 4, }, sectionTitle: { fontSize: 14, fontWeight: '700', color: '#64748B', fontFamily: 'AliBold', textTransform: 'uppercase', letterSpacing: 0.5, }, requiredIndicator: { fontSize: 14, color: '#EF4444', marginLeft: 4, fontWeight: '700', }, sectionCard: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 20, shadowColor: 'rgba(30, 41, 59, 0.05)', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.5, shadowRadius: 10, elevation: 2, }, inputRowContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, inputRowLabel: { fontSize: 15, color: '#475569', fontWeight: '600', width: 90, marginRight: 12, fontFamily: 'AliRegular', }, inputRowContent: { flex: 1, }, modernInputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 16, backgroundColor: '#F8FAFC', borderWidth: 1, borderColor: '#E2E8F0', overflow: 'hidden', }, modernNumberInput: { flex: 1, paddingHorizontal: 16, paddingVertical: 12, fontSize: 16, color: '#1E293B', textAlign: 'right', fontFamily: 'AliRegular', }, unitText: { fontSize: 14, color: '#94A3B8', paddingRight: 16, minWidth: 40, textAlign: 'center', fontWeight: '500', }, modernImageSelector: { alignSelf: 'flex-end', borderRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2, }, selectedImage: { width: 72, height: 72, borderRadius: 20, }, modernImagePlaceholder: { width: 72, height: 72, borderRadius: 20, backgroundColor: '#F8FAFC', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#E2E8F0', borderStyle: 'dashed', }, imagePlaceholderText: { fontSize: 11, color: '#94A3B8', marginTop: 4, fontWeight: '600', textAlign: 'center', }, imageLoadingOverlay: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.3)', borderRadius: 20, }, });