feat(food): 添加拍摄指引弹窗与相册选择功能

- 在相机界面新增“拍摄示例”弹窗,展示正确/错误拍摄对比图
- 底部控制栏增加相册选择按钮与帮助按钮
- 优化控制栏布局为左右分布,提升操作便捷性
- 移除 food-recognition 中冗余的 isUploading 状态,简化上传流程
This commit is contained in:
richarjiang
2025-09-04 10:52:00 +08:00
parent 6cb0435b30
commit 4ae419754a
2 changed files with 179 additions and 6 deletions

View File

@@ -2,12 +2,14 @@ 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,
@@ -30,6 +32,7 @@ export default function FoodCameraScreen() {
);
const [facing, setFacing] = useState<CameraType>('back');
const [permission, requestPermission] = useCameraPermissions();
const [showInstructionModal, setShowInstructionModal] = useState(false);
// 餐次选择选项
const mealOptions = [
@@ -199,13 +202,79 @@ export default function FoodCameraScreen() {
{/* 底部控制栏 */}
<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>
);
}
@@ -366,7 +435,7 @@ const styles = StyleSheet.create({
},
controlsContainer: {
flexDirection: 'row',
justifyContent: 'center',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 20,
paddingHorizontal: 40,
@@ -423,4 +492,111 @@ const styles = StyleSheet.create({
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',
},
});

View File

@@ -29,8 +29,7 @@ export default function FoodRecognitionScreen() {
}>();
const { imageUri, mealType } = params;
const { upload, uploading } = useCosUpload();
const [isUploading, setIsUploading] = useState(false);
const { upload } = useCosUpload();
const [showRecognitionProcess, setShowRecognitionProcess] = useState(false);
const [recognitionLogs, setRecognitionLogs] = useState<string[]>([]);
const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle');
@@ -128,7 +127,6 @@ export default function FoodRecognitionScreen() {
setShowRecognitionProcess(true);
setRecognitionLogs([]);
setCurrentStep('uploading');
setIsUploading(true);
dispatch(setLoading(true));
addLog('📤 正在上传图片到云端...');
@@ -167,7 +165,7 @@ export default function FoodRecognitionScreen() {
const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
// 保存识别结果到 Redux
await dispatch(saveRecognitionResult({
dispatch(saveRecognitionResult({
id: recognitionId,
result: recognitionResult
}));
@@ -184,7 +182,6 @@ export default function FoodRecognitionScreen() {
setCurrentStep('failed');
dispatch(setError('食物识别失败,请重试'));
} finally {
setIsUploading(false);
dispatch(setLoading(false));
}
};