Files
digital-pilates/app/food/camera.tsx

612 lines
16 KiB
TypeScript
Raw 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 { 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',
},
});