feat(nutrition): 优化营养成分表分析功能并移除流式显示

- 移除流式分析文本显示,简化用户界面
- 修复ImagePicker媒体类型配置,使用数组格式
- 简化API响应处理逻辑,直接使用服务端返回数据
- 移除旧格式转换函数,统一使用新的API响应格式
- 清理冗余状态变量和UI组件,提升代码可维护性
This commit is contained in:
richarjiang
2025-10-16 12:46:43 +08:00
parent 5013464a2c
commit b27099c6d9
9 changed files with 994 additions and 206 deletions

View File

@@ -5,10 +5,7 @@ import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
analyzeNutritionImage,
analyzeNutritionLabelStream,
convertNewApiResultToOldFormat,
type NutritionAnalysisResponse,
type NutritionItem,
type NutritionLabelAnalysisResult
type NutritionAnalysisResponse
} from '@/services/nutritionLabelAnalysis';
import { triggerLightHaptic } from '@/utils/haptics';
import { Ionicons } from '@expo/vector-icons';
@@ -40,8 +37,6 @@ export default function NutritionLabelAnalysisScreen() {
const [imageUri, setImageUri] = useState<string | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
const [analysisText, setAnalysisText] = useState<string>('');
const [analysisResult, setAnalysisResult] = useState<NutritionLabelAnalysisResult | null>(null);
const [newAnalysisResult, setNewAnalysisResult] = useState<NutritionAnalysisResponse | null>(null);
const [isUploading, setIsUploading] = useState(false);
@@ -94,7 +89,7 @@ export default function NutritionLabelAnalysisScreen() {
triggerLightHaptic();
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
mediaTypes: ['images'],
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
@@ -102,9 +97,7 @@ export default function NutritionLabelAnalysisScreen() {
if (!result.canceled && result.assets[0]) {
setImageUri(result.assets[0].uri);
setAnalysisText(''); // 清除之前的分析文本
setAnalysisResult(null); // 清除之前的分析结果
setNewAnalysisResult(null); // 清除新的分析结果
setNewAnalysisResult(null); // 清除之前的分析结果
}
};
@@ -113,7 +106,7 @@ export default function NutritionLabelAnalysisScreen() {
triggerLightHaptic();
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
mediaTypes: ['images'],
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
@@ -121,9 +114,7 @@ export default function NutritionLabelAnalysisScreen() {
if (!result.canceled && result.assets[0]) {
setImageUri(result.assets[0].uri);
setAnalysisText(''); // 清除之前的分析文本
setAnalysisResult(null); // 清除之前的分析结果
setNewAnalysisResult(null); // 清除新的分析结果
setNewAnalysisResult(null); // 清除之前的分析结果
}
};
@@ -140,7 +131,6 @@ export default function NutritionLabelAnalysisScreen() {
// 清理状态
setIsAnalyzing(false);
setAnalysisText('');
// 触觉反馈
triggerLightHaptic();
@@ -154,8 +144,6 @@ export default function NutritionLabelAnalysisScreen() {
if (isAnalyzing) return;
setIsAnalyzing(true);
setAnalysisText('');
setAnalysisResult(null);
// 延迟滚动到分析结果区域给UI一些时间更新
setTimeout(() => {
@@ -169,17 +157,8 @@ export default function NutritionLabelAnalysisScreen() {
{ imageUri: uri },
{
onChunk: (chunk: string) => {
setAnalysisText(prev => {
const newText = prev + chunk;
// 在接收到第一个文本块时滚动到分析结果区域
if (isFirstChunk && newText.length > 0) {
isFirstChunk = false;
setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100);
}
return newText;
});
// 流式分析暂时保留,但不再显示文本
console.log('[NUTRITION_ANALYSIS] Stream chunk:', chunk);
},
onEnd: () => {
setIsAnalyzing(false);
@@ -189,7 +168,6 @@ export default function NutritionLabelAnalysisScreen() {
onError: (error: any) => {
console.error('[NUTRITION_ANALYSIS] Analysis failed:', error);
setIsAnalyzing(false);
setAnalysisText('');
streamAbortRef.current = null;
// 如果是用户主动取消,不显示错误提示
@@ -202,7 +180,6 @@ export default function NutritionLabelAnalysisScreen() {
} catch (error) {
console.error('[NUTRITION_ANALYSIS] Analysis error:', error);
setIsAnalyzing(false);
setAnalysisText('');
Alert.alert('分析失败', '无法识别成分表,请尝试拍摄更清晰的照片');
}
}, [isAnalyzing]);
@@ -212,8 +189,6 @@ export default function NutritionLabelAnalysisScreen() {
if (isAnalyzing || isUploading) return;
setIsUploading(true);
setAnalysisText('');
setAnalysisResult(null);
setNewAnalysisResult(null);
// 延迟滚动到分析结果区域给UI一些时间更新
@@ -239,21 +214,8 @@ export default function NutritionLabelAnalysisScreen() {
console.log('[NUTRITION_ANALYSIS] API响应:', analysisResponse);
if (analysisResponse.success && analysisResponse.data) {
// 转换为旧格式以便与现有UI兼容
const oldFormatResult = convertNewApiResultToOldFormat(analysisResponse, uploadResult.url);
if (oldFormatResult) {
setAnalysisResult(oldFormatResult);
setNewAnalysisResult(analysisResponse);
// 生成分析文本
const analysisText = analysisResponse.data
.map((item: NutritionItem) => `**${item.name}**: ${item.value}\n${item.analysis}`)
.join('\n\n');
setAnalysisText(analysisText);
} else {
throw new Error('无法解析API返回结果');
}
// 直接使用服务端返回的数据,不做任何转换
setNewAnalysisResult(analysisResponse);
} else {
throw new Error(analysisResponse.message || '分析失败');
}
@@ -261,7 +223,6 @@ export default function NutritionLabelAnalysisScreen() {
console.error('[NUTRITION_ANALYSIS] 新API分析失败:', error);
setIsUploading(false);
setIsAnalyzing(false);
setAnalysisText('');
// 显示错误提示
Alert.alert(
@@ -318,7 +279,7 @@ export default function NutritionLabelAnalysisScreen() {
</TouchableOpacity>
{/* 开始分析按钮 */}
{!isAnalyzing && !isUploading && !analysisText && (
{!isAnalyzing && !isUploading && !newAnalysisResult && (
<TouchableOpacity
style={styles.analyzeButton}
onPress={() => startNewAnalysis(imageUri)}
@@ -334,8 +295,6 @@ export default function NutritionLabelAnalysisScreen() {
style={styles.deleteImageButton}
onPress={() => {
setImageUri(null);
setAnalysisText('');
setAnalysisResult(null);
setNewAnalysisResult(null);
triggerLightHaptic();
}}
@@ -373,31 +332,6 @@ export default function NutritionLabelAnalysisScreen() {
)}
</View>
{/* 流式分析文本区域 */}
{analysisText && (
<View style={styles.analysisTextCard}>
<View style={styles.analysisTextHeader}>
<Text style={styles.analysisTextTitle}></Text>
{isAnalyzing && (
<TouchableOpacity
style={styles.cancelAnalysisButton}
onPress={cancelAnalysis}
activeOpacity={0.8}
>
<Ionicons name="close-outline" size={16} color="#FF4444" />
<Text style={styles.cancelAnalysisText}></Text>
</TouchableOpacity>
)}
</View>
<Text style={styles.analysisTextContent}>{analysisText}</Text>
{isAnalyzing && (
<View style={styles.streamingIndicator}>
<ActivityIndicator size="small" color={Colors.light.primary} />
<Text style={styles.streamingText}>...</Text>
</View>
)}
</View>
)}
{/* 新API营养成分详细分析结果 */}
{newAnalysisResult && newAnalysisResult.success && newAnalysisResult.data && (
@@ -428,7 +362,7 @@ export default function NutritionLabelAnalysisScreen() {
)}
{/* 加载状态 */}
{isAnalyzing && !analysisText && !isUploading && (
{isAnalyzing && !newAnalysisResult && !isUploading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>...</Text>
@@ -725,66 +659,6 @@ const styles = StyleSheet.create({
fontWeight: '600',
marginLeft: 6,
},
// 分析文本卡片样式
analysisTextCard: {
backgroundColor: Colors.light.background,
margin: 16,
borderRadius: 16,
padding: 20,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
analysisTextHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
analysisTextTitle: {
fontSize: 18,
fontWeight: '600',
color: Colors.light.text,
},
cancelAnalysisButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: 'rgba(255,68,68,0.1)',
borderWidth: 1,
borderColor: 'rgba(255,68,68,0.3)',
},
cancelAnalysisText: {
fontSize: 12,
fontWeight: '600',
color: '#FF4444',
},
analysisTextContent: {
fontSize: 15,
lineHeight: 22,
color: Colors.light.text,
marginBottom: 12,
},
streamingIndicator: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 8,
},
streamingText: {
fontSize: 14,
color: Colors.light.textSecondary,
fontStyle: 'italic',
},
// 营养成分详细分析卡片样式
nutritionDetailsCard: {
backgroundColor: Colors.light.background,