feat(food): 添加拍摄指引弹窗与相册选择功能
- 在相机界面新增“拍摄示例”弹窗,展示正确/错误拍摄对比图 - 底部控制栏增加相册选择按钮与帮助按钮 - 优化控制栏布局为左右分布,提升操作便捷性 - 移除 food-recognition 中冗余的 isUploading 状态,简化上传流程
This commit is contained in:
@@ -2,12 +2,14 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
|
Modal,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -30,6 +32,7 @@ export default function FoodCameraScreen() {
|
|||||||
);
|
);
|
||||||
const [facing, setFacing] = useState<CameraType>('back');
|
const [facing, setFacing] = useState<CameraType>('back');
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
|
const [showInstructionModal, setShowInstructionModal] = useState(false);
|
||||||
|
|
||||||
// 餐次选择选项
|
// 餐次选择选项
|
||||||
const mealOptions = [
|
const mealOptions = [
|
||||||
@@ -199,13 +202,79 @@ export default function FoodCameraScreen() {
|
|||||||
{/* 底部控制栏 */}
|
{/* 底部控制栏 */}
|
||||||
<View style={styles.bottomContainer}>
|
<View style={styles.bottomContainer}>
|
||||||
<View style={styles.controlsContainer}>
|
<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}>
|
<TouchableOpacity style={styles.captureButton} onPress={takePicture}>
|
||||||
<View style={styles.captureButtonInner} />
|
<View style={styles.captureButtonInner} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 帮助按钮 */}
|
||||||
|
<TouchableOpacity style={styles.helpButton} onPress={() => setShowInstructionModal(true)}>
|
||||||
|
<Ionicons name="help-outline" size={24} color="#FFF" />
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -366,7 +435,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
controlsContainer: {
|
controlsContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 20,
|
paddingVertical: 20,
|
||||||
paddingHorizontal: 40,
|
paddingHorizontal: 40,
|
||||||
@@ -423,4 +492,111 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -29,8 +29,7 @@ export default function FoodRecognitionScreen() {
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { imageUri, mealType } = params;
|
const { imageUri, mealType } = params;
|
||||||
const { upload, uploading } = useCosUpload();
|
const { upload } = useCosUpload();
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
const [showRecognitionProcess, setShowRecognitionProcess] = useState(false);
|
const [showRecognitionProcess, setShowRecognitionProcess] = useState(false);
|
||||||
const [recognitionLogs, setRecognitionLogs] = useState<string[]>([]);
|
const [recognitionLogs, setRecognitionLogs] = useState<string[]>([]);
|
||||||
const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle');
|
const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle');
|
||||||
@@ -128,7 +127,6 @@ export default function FoodRecognitionScreen() {
|
|||||||
setShowRecognitionProcess(true);
|
setShowRecognitionProcess(true);
|
||||||
setRecognitionLogs([]);
|
setRecognitionLogs([]);
|
||||||
setCurrentStep('uploading');
|
setCurrentStep('uploading');
|
||||||
setIsUploading(true);
|
|
||||||
dispatch(setLoading(true));
|
dispatch(setLoading(true));
|
||||||
|
|
||||||
addLog('📤 正在上传图片到云端...');
|
addLog('📤 正在上传图片到云端...');
|
||||||
@@ -167,7 +165,7 @@ export default function FoodRecognitionScreen() {
|
|||||||
const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
// 保存识别结果到 Redux
|
// 保存识别结果到 Redux
|
||||||
await dispatch(saveRecognitionResult({
|
dispatch(saveRecognitionResult({
|
||||||
id: recognitionId,
|
id: recognitionId,
|
||||||
result: recognitionResult
|
result: recognitionResult
|
||||||
}));
|
}));
|
||||||
@@ -184,7 +182,6 @@ export default function FoodRecognitionScreen() {
|
|||||||
setCurrentStep('failed');
|
setCurrentStep('failed');
|
||||||
dispatch(setError('食物识别失败,请重试'));
|
dispatch(setError('食物识别失败,请重试'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
|
||||||
dispatch(setLoading(false));
|
dispatch(setLoading(false));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user