612 lines
16 KiB
TypeScript
612 lines
16 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
||
import { Image } from 'expo-image';
|
||
import * as ImagePicker from 'expo-image-picker';
|
||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||
import React, { useRef, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Dimensions,
|
||
Modal,
|
||
StatusBar,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
|
||
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||
|
||
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||
|
||
export default function FoodCameraScreen() {
|
||
const router = useRouter();
|
||
const params = useLocalSearchParams<{ mealType?: string }>();
|
||
const cameraRef = useRef<CameraView>(null);
|
||
|
||
const [currentMealType, setCurrentMealType] = useState<MealType>(
|
||
(params.mealType as MealType) || 'dinner'
|
||
);
|
||
const [facing, setFacing] = useState<CameraType>('back');
|
||
const [permission, requestPermission] = useCameraPermissions();
|
||
const [showInstructionModal, setShowInstructionModal] = useState(false);
|
||
|
||
// 餐次选择选项
|
||
const mealOptions = [
|
||
{ key: 'breakfast' as const, label: '早餐', icon: '☀️' },
|
||
{ key: 'lunch' as const, label: '午餐', icon: '🌤️' },
|
||
{ key: 'dinner' as const, label: '晚餐', icon: '🌙' },
|
||
{ key: 'snack' as const, label: '加餐', icon: '🍎' },
|
||
];
|
||
|
||
if (!permission) {
|
||
// 权限仍在加载中
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<HeaderBar
|
||
title="食物拍摄"
|
||
onBack={() => router.back()}
|
||
transparent={true}
|
||
/>
|
||
<View style={styles.loadingContainer}>
|
||
<Text style={styles.loadingText}>正在加载相机...</Text>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
if (!permission.granted) {
|
||
// 没有相机权限
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<HeaderBar
|
||
title="食物拍摄"
|
||
onBack={() => router.back()}
|
||
backColor='#ffffff'
|
||
/>
|
||
<View style={styles.permissionContainer}>
|
||
<Ionicons name="camera-outline" size={64} color="#999" />
|
||
<Text style={styles.permissionTitle}>需要相机权限</Text>
|
||
<Text style={styles.permissionText}>
|
||
为了拍摄食物,需要访问您的相机
|
||
</Text>
|
||
<TouchableOpacity
|
||
style={styles.permissionButton}
|
||
onPress={requestPermission}
|
||
>
|
||
<Text style={styles.permissionButtonText}>授权访问</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
// 切换相机前后摄像头
|
||
function toggleCameraFacing() {
|
||
setFacing(current => (current === 'back' ? 'front' : 'back'));
|
||
}
|
||
|
||
// 拍摄照片
|
||
const takePicture = async () => {
|
||
if (cameraRef.current) {
|
||
try {
|
||
const photo = await cameraRef.current.takePictureAsync({
|
||
quality: 0.8,
|
||
base64: false,
|
||
});
|
||
|
||
if (photo) {
|
||
// 跳转到食物识别页面
|
||
console.log('照片拍摄成功:', photo.uri);
|
||
router.replace(`/food/food-recognition?imageUri=${encodeURIComponent(photo.uri)}&mealType=${currentMealType}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('拍照失败:', error);
|
||
Alert.alert('拍照失败', '请重试');
|
||
}
|
||
}
|
||
};
|
||
|
||
// 从相册选择照片
|
||
const pickImageFromGallery = async () => {
|
||
try {
|
||
const result = await ImagePicker.launchImageLibraryAsync({
|
||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||
allowsEditing: true,
|
||
aspect: [4, 3],
|
||
quality: 0.8,
|
||
});
|
||
|
||
if (!result.canceled && result.assets[0]) {
|
||
const imageUri = result.assets[0].uri;
|
||
console.log('从相册选择的照片:', imageUri);
|
||
router.push(`/food/food-recognition?imageUri=${encodeURIComponent(imageUri)}&mealType=${currentMealType}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('选择照片失败:', error);
|
||
Alert.alert('选择失败', '请重试');
|
||
}
|
||
};
|
||
|
||
// AR功能(暂时显示提示)
|
||
const handleARPress = () => {
|
||
Alert.alert('AR功能', 'AR食物识别功能即将推出');
|
||
};
|
||
|
||
// 餐次选择
|
||
const handleMealTypeChange = (mealType: MealType) => {
|
||
setCurrentMealType(mealType);
|
||
};
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||
|
||
{/* 头部导航 */}
|
||
<HeaderBar
|
||
title=""
|
||
onBack={() => router.back()}
|
||
transparent={true}
|
||
backColor={'#fff'}
|
||
/>
|
||
|
||
{/* 主要内容区域 */}
|
||
<View style={styles.contentContainer}>
|
||
{/* 取景框容器 */}
|
||
<View style={styles.cameraFrameContainer}>
|
||
<Text style={styles.hintText}>确保食物在取景框内</Text>
|
||
|
||
{/* 相机取景框包装器 */}
|
||
<View style={styles.cameraWrapper}>
|
||
{/* 相机取景框 */}
|
||
<View style={styles.cameraFrame}>
|
||
<CameraView
|
||
ref={cameraRef}
|
||
style={styles.cameraView}
|
||
facing={facing}
|
||
/>
|
||
</View>
|
||
{/* 取景框装饰 - 放在外层避免被截断 */}
|
||
<View style={styles.viewfinderOverlay}>
|
||
<View style={[styles.corner, styles.topLeft]} />
|
||
<View style={[styles.corner, styles.topRight]} />
|
||
<View style={[styles.corner, styles.bottomLeft]} />
|
||
<View style={[styles.corner, styles.bottomRight]} />
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 餐次选择器 */}
|
||
<View style={styles.mealTypeContainer}>
|
||
{mealOptions.map((option) => (
|
||
<TouchableOpacity
|
||
key={option.key}
|
||
style={[
|
||
styles.mealTypeButton,
|
||
currentMealType === option.key && styles.mealTypeButtonActive
|
||
]}
|
||
onPress={() => handleMealTypeChange(option.key)}
|
||
>
|
||
<Text style={styles.mealTypeIcon}>{option.icon}</Text>
|
||
<Text style={[
|
||
styles.mealTypeText,
|
||
currentMealType === option.key && styles.mealTypeTextActive
|
||
]}>
|
||
{option.label}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
|
||
{/* 底部控制栏 */}
|
||
<View style={styles.bottomContainer}>
|
||
<View style={styles.controlsContainer}>
|
||
{/* 相册选择按钮 */}
|
||
<TouchableOpacity style={styles.galleryButton} onPress={pickImageFromGallery}>
|
||
<Ionicons name="images-outline" size={24} color="#FFF" />
|
||
</TouchableOpacity>
|
||
|
||
{/* 拍照按钮 */}
|
||
<TouchableOpacity style={styles.captureButton} onPress={takePicture}>
|
||
<View style={styles.captureButtonInner} />
|
||
</TouchableOpacity>
|
||
|
||
{/* 帮助按钮 */}
|
||
<TouchableOpacity style={styles.helpButton} onPress={() => setShowInstructionModal(true)}>
|
||
<Ionicons name="help-outline" size={24} color="#FFF" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 拍摄说明弹窗 */}
|
||
<Modal
|
||
visible={showInstructionModal}
|
||
animationType="fade"
|
||
transparent={true}
|
||
onRequestClose={() => setShowInstructionModal(false)}
|
||
>
|
||
<View style={styles.modalOverlay}>
|
||
<View style={styles.instructionModal}>
|
||
<Text style={styles.instructionTitle}>拍摄示例</Text>
|
||
|
||
<View style={styles.exampleContainer}>
|
||
{/* 好的示例 */}
|
||
<View style={styles.exampleItem}>
|
||
<View style={styles.exampleImagePlaceholder}>
|
||
<View style={styles.checkmarkContainer}>
|
||
<Ionicons name="checkmark" size={32} color="#FFF" />
|
||
</View>
|
||
{/* 这里可以放置好的示例图片 */}
|
||
<Image
|
||
style={styles.exampleImage}
|
||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/function/food-right.jpeg' }}
|
||
cachePolicy={'memory-disk'}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 不好的示例 */}
|
||
<View style={styles.exampleItem}>
|
||
<View style={styles.exampleImagePlaceholder}>
|
||
<View style={styles.crossContainer}>
|
||
<Ionicons name="close" size={32} color="#FFF" />
|
||
</View>
|
||
<Image
|
||
style={styles.exampleImage}
|
||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/function/food-wrong.jpeg' }}
|
||
cachePolicy={'memory-disk'}
|
||
/>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<Text style={styles.instructionDescription}>
|
||
请上传或拍摄如左图所示的食物照片
|
||
</Text>
|
||
|
||
<TouchableOpacity
|
||
style={styles.knowButton}
|
||
onPress={() => setShowInstructionModal(false)}
|
||
>
|
||
<Text style={styles.knowButtonText}>知道了</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#000',
|
||
},
|
||
contentContainer: {
|
||
flex: 1,
|
||
paddingTop: 100,
|
||
},
|
||
cameraFrameContainer: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 20,
|
||
},
|
||
cameraWrapper: {
|
||
width: 300,
|
||
height: 300,
|
||
position: 'relative',
|
||
},
|
||
cameraFrame: {
|
||
width: 300,
|
||
height: 300,
|
||
borderRadius: 20,
|
||
overflow: 'hidden',
|
||
backgroundColor: '#000',
|
||
},
|
||
cameraView: {
|
||
flex: 1,
|
||
},
|
||
viewfinderOverlay: {
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
zIndex: 1,
|
||
pointerEvents: 'none',
|
||
},
|
||
camera: {
|
||
flex: 1,
|
||
},
|
||
loadingContainer: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
backgroundColor: '#000',
|
||
},
|
||
loadingText: {
|
||
color: '#FFF',
|
||
fontSize: 16,
|
||
},
|
||
permissionContainer: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
backgroundColor: '#000',
|
||
paddingHorizontal: 40,
|
||
},
|
||
permissionTitle: {
|
||
color: '#FFF',
|
||
fontSize: 20,
|
||
fontWeight: '600',
|
||
marginTop: 20,
|
||
marginBottom: 10,
|
||
},
|
||
permissionText: {
|
||
color: '#CCC',
|
||
fontSize: 16,
|
||
textAlign: 'center',
|
||
marginBottom: 30,
|
||
lineHeight: 22,
|
||
},
|
||
permissionButton: {
|
||
backgroundColor: Colors.light.primary,
|
||
paddingHorizontal: 24,
|
||
paddingVertical: 12,
|
||
borderRadius: 24,
|
||
},
|
||
permissionButtonText: {
|
||
color: '#FFF',
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
},
|
||
header: {
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
zIndex: 10,
|
||
},
|
||
hintText: {
|
||
color: '#FFF',
|
||
fontSize: 16,
|
||
fontWeight: '500',
|
||
marginBottom: 20,
|
||
textAlign: 'center',
|
||
},
|
||
corner: {
|
||
position: 'absolute',
|
||
width: 30,
|
||
height: 30,
|
||
borderColor: '#FFF',
|
||
borderWidth: 3,
|
||
},
|
||
topLeft: {
|
||
top: 0,
|
||
left: 0,
|
||
borderRightWidth: 0,
|
||
borderBottomWidth: 0,
|
||
},
|
||
topRight: {
|
||
top: 0,
|
||
right: 0,
|
||
borderLeftWidth: 0,
|
||
borderBottomWidth: 0,
|
||
},
|
||
bottomLeft: {
|
||
bottom: 0,
|
||
left: 0,
|
||
borderRightWidth: 0,
|
||
borderTopWidth: 0,
|
||
},
|
||
bottomRight: {
|
||
bottom: 0,
|
||
right: 0,
|
||
borderLeftWidth: 0,
|
||
borderTopWidth: 0,
|
||
},
|
||
mealTypeContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'center',
|
||
paddingHorizontal: 20,
|
||
marginVertical: 20,
|
||
},
|
||
mealTypeButton: {
|
||
alignItems: 'center',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 8,
|
||
marginHorizontal: 8,
|
||
borderRadius: 20,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
minWidth: 70,
|
||
},
|
||
mealTypeButtonActive: {
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
},
|
||
mealTypeIcon: {
|
||
fontSize: 20,
|
||
marginBottom: 2,
|
||
},
|
||
mealTypeText: {
|
||
color: '#FFF',
|
||
fontSize: 12,
|
||
fontWeight: '500',
|
||
},
|
||
mealTypeTextActive: {
|
||
color: '#333',
|
||
},
|
||
bottomContainer: {
|
||
paddingBottom: 40,
|
||
},
|
||
controlsContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingVertical: 20,
|
||
paddingHorizontal: 40,
|
||
},
|
||
controlButton: {
|
||
alignItems: 'center',
|
||
},
|
||
controlButtonText: {
|
||
color: '#FFF',
|
||
fontSize: 12,
|
||
marginTop: 8,
|
||
fontWeight: '500',
|
||
},
|
||
albumButton: {
|
||
width: 50,
|
||
height: 50,
|
||
borderRadius: 10,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
borderWidth: 2,
|
||
borderColor: '#FFF',
|
||
},
|
||
captureButton: {
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: 40,
|
||
backgroundColor: '#FFF',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
borderWidth: 4,
|
||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||
},
|
||
captureButtonInner: {
|
||
width: 60,
|
||
height: 60,
|
||
borderRadius: 30,
|
||
backgroundColor: '#FFF',
|
||
borderWidth: 2,
|
||
borderColor: '#333',
|
||
},
|
||
arButton: {
|
||
width: 50,
|
||
height: 50,
|
||
borderRadius: 25,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
borderWidth: 2,
|
||
borderColor: '#FFF',
|
||
},
|
||
arButtonText: {
|
||
color: '#FFF',
|
||
fontSize: 14,
|
||
fontWeight: 'bold',
|
||
},
|
||
galleryButton: {
|
||
width: 50,
|
||
height: 50,
|
||
borderRadius: 25,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
borderWidth: 2,
|
||
borderColor: '#FFF',
|
||
},
|
||
helpButton: {
|
||
width: 50,
|
||
height: 50,
|
||
borderRadius: 25,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
borderWidth: 2,
|
||
borderColor: '#FFF',
|
||
},
|
||
modalOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
instructionModal: {
|
||
backgroundColor: '#FFF',
|
||
borderTopLeftRadius: 20,
|
||
borderTopRightRadius: 20,
|
||
paddingHorizontal: 24,
|
||
paddingVertical: 32,
|
||
minHeight: 400,
|
||
},
|
||
instructionTitle: {
|
||
fontSize: 24,
|
||
fontWeight: 'bold',
|
||
textAlign: 'center',
|
||
marginBottom: 32,
|
||
color: '#333',
|
||
},
|
||
exampleContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 24,
|
||
paddingHorizontal: 16,
|
||
},
|
||
exampleItem: {
|
||
flex: 1,
|
||
marginHorizontal: 8,
|
||
},
|
||
exampleImagePlaceholder: {
|
||
width: '100%',
|
||
aspectRatio: 3 / 4,
|
||
backgroundColor: '#F0F0F0',
|
||
borderRadius: 16,
|
||
position: 'relative',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
checkmarkContainer: {
|
||
position: 'absolute',
|
||
top: 12,
|
||
right: 12,
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: '#4CAF50',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
zIndex: 10,
|
||
},
|
||
crossContainer: {
|
||
position: 'absolute',
|
||
top: 12,
|
||
right: 12,
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: '#F44336',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
zIndex: 10,
|
||
},
|
||
exampleImage: {
|
||
width: '100%',
|
||
height: '100%',
|
||
},
|
||
instructionDescription: {
|
||
fontSize: 16,
|
||
textAlign: 'center',
|
||
color: '#666',
|
||
marginBottom: 32,
|
||
lineHeight: 24,
|
||
paddingHorizontal: 16,
|
||
},
|
||
knowButton: {
|
||
backgroundColor: '#000',
|
||
borderRadius: 25,
|
||
paddingVertical: 16,
|
||
marginHorizontal: 16,
|
||
},
|
||
knowButtonText: {
|
||
color: '#FFF',
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
textAlign: 'center',
|
||
},
|
||
}); |