新增营养成分表拍照识别功能,用户可通过拍摄食物包装上的成分表自动解析营养信息: - 创建成分表分析页面,支持拍照/选择图片和结果展示 - 集成新的营养成分分析API,支持图片上传和流式分析 - 在营养雷达卡片中添加成分表分析入口 - 更新应用版本至1.0.19
174 lines
4.9 KiB
TypeScript
174 lines
4.9 KiB
TypeScript
import { api, postTextStream, type TextStreamCallbacks } from '@/services/api';
|
||
import type { CreateDietRecordDto, MealType } from './dietRecords';
|
||
|
||
export interface NutritionLabelData {
|
||
energy: number; // 能量 (kJ/千卡)
|
||
protein: number; // 蛋白质 (g)
|
||
fat: number; // 脂肪 (g)
|
||
carbohydrate: number; // 碳水化合物 (g)
|
||
sodium: number; // 钠 (mg)
|
||
fiber?: number; // 膳食纤维 (g)
|
||
sugar?: number; // 糖 (g)
|
||
servingSize?: string; // 每份量
|
||
}
|
||
|
||
export interface NutritionLabelAnalysisResult {
|
||
id: string;
|
||
imageUri: string;
|
||
nutritionData: NutritionLabelData;
|
||
confidence: number;
|
||
analyzedAt: string;
|
||
foodName?: string; // 识别出的食物名称
|
||
brand?: string; // 品牌
|
||
}
|
||
|
||
export interface NutritionLabelAnalysisRequest {
|
||
imageUri: string;
|
||
}
|
||
|
||
// 新API返回的营养成分项
|
||
export interface NutritionItem {
|
||
key: string;
|
||
name: string;
|
||
value: string;
|
||
analysis: string;
|
||
}
|
||
|
||
// 新API返回的响应格式
|
||
export interface NutritionAnalysisResponse {
|
||
success: boolean;
|
||
data: NutritionItem[];
|
||
message?: string; // 仅在失败时返回
|
||
}
|
||
|
||
// 新API请求参数
|
||
export interface NutritionAnalysisRequest {
|
||
imageUrl: string;
|
||
}
|
||
|
||
/**
|
||
* 分析营养成分表(非流式)
|
||
*/
|
||
export async function analyzeNutritionLabel(request: NutritionLabelAnalysisRequest): Promise<NutritionLabelAnalysisResult> {
|
||
return api.post<NutritionLabelAnalysisResult>('/ai-coach/nutrition-label-analysis', request);
|
||
}
|
||
|
||
/**
|
||
* 流式分析营养成分表
|
||
*/
|
||
export async function analyzeNutritionLabelStream(
|
||
request: NutritionLabelAnalysisRequest,
|
||
callbacks: TextStreamCallbacks
|
||
) {
|
||
const body = {
|
||
imageUri: request.imageUri,
|
||
stream: true
|
||
};
|
||
|
||
return postTextStream('/ai-coach/nutrition-label-analysis', body, callbacks, { timeoutMs: 120000 });
|
||
}
|
||
|
||
/**
|
||
* 保存成分表分析结果到饮食记录
|
||
*/
|
||
export async function saveNutritionLabelToDietRecord(
|
||
analysisResult: NutritionLabelAnalysisResult,
|
||
mealType: MealType
|
||
): Promise<any> {
|
||
const dietRecordData: CreateDietRecordDto = {
|
||
mealType,
|
||
foodName: analysisResult.foodName || '成分表分析食物',
|
||
foodDescription: `品牌: ${analysisResult.brand || '未知'}`,
|
||
portionDescription: analysisResult.nutritionData.servingSize || '100g',
|
||
estimatedCalories: analysisResult.nutritionData.energy,
|
||
proteinGrams: analysisResult.nutritionData.protein,
|
||
carbohydrateGrams: analysisResult.nutritionData.carbohydrate,
|
||
fatGrams: analysisResult.nutritionData.fat,
|
||
fiberGrams: analysisResult.nutritionData.fiber,
|
||
sugarGrams: analysisResult.nutritionData.sugar,
|
||
sodiumMg: analysisResult.nutritionData.sodium,
|
||
source: 'vision',
|
||
mealTime: new Date().toISOString(),
|
||
imageUrl: analysisResult.imageUri,
|
||
aiAnalysisResult: analysisResult,
|
||
};
|
||
|
||
return api.post('/diet-records', dietRecordData);
|
||
}
|
||
|
||
/**
|
||
* 分析营养成分表图片(新API)
|
||
* 需要先上传图片到COS获取URL,然后调用此接口
|
||
*/
|
||
export async function analyzeNutritionImage(request: NutritionAnalysisRequest): Promise<NutritionAnalysisResponse> {
|
||
return api.post<NutritionAnalysisResponse>('/diet-records/analyze-nutrition-image', request);
|
||
}
|
||
|
||
/**
|
||
* 将新API的分析结果转换为旧格式,以便与现有UI兼容
|
||
*/
|
||
export function convertNewApiResultToOldFormat(
|
||
newResult: NutritionAnalysisResponse,
|
||
imageUri: string
|
||
): NutritionLabelAnalysisResult | null {
|
||
if (!newResult.success || !newResult.data || newResult.data.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// 从新API结果中提取营养数据
|
||
const nutritionData: NutritionLabelData = {
|
||
energy: 0,
|
||
protein: 0,
|
||
fat: 0,
|
||
carbohydrate: 0,
|
||
sodium: 0,
|
||
fiber: 0,
|
||
sugar: 0,
|
||
};
|
||
|
||
// 查找各个营养素的值并转换为数字
|
||
newResult.data.forEach(item => {
|
||
const valueStr = item.value;
|
||
// 提取数字部分
|
||
const numericValue = parseFloat(valueStr.replace(/[^\d.]/g, ''));
|
||
|
||
switch (item.key) {
|
||
case 'energy_kcal':
|
||
// 如果是千焦,转换为千卡 (1千焦 ≈ 0.239千卡)
|
||
if (valueStr.includes('千焦')) {
|
||
nutritionData.energy = Math.round(numericValue * 0.239);
|
||
} else {
|
||
nutritionData.energy = numericValue;
|
||
}
|
||
break;
|
||
case 'protein':
|
||
nutritionData.protein = numericValue;
|
||
break;
|
||
case 'fat':
|
||
nutritionData.fat = numericValue;
|
||
break;
|
||
case 'carbohydrate':
|
||
nutritionData.carbohydrate = numericValue;
|
||
break;
|
||
case 'sodium':
|
||
nutritionData.sodium = numericValue;
|
||
break;
|
||
case 'fiber':
|
||
nutritionData.fiber = numericValue;
|
||
break;
|
||
case 'sugar':
|
||
nutritionData.sugar = numericValue;
|
||
break;
|
||
}
|
||
});
|
||
|
||
return {
|
||
id: Date.now().toString(),
|
||
imageUri,
|
||
nutritionData,
|
||
confidence: 0.9, // 新API没有提供置信度,使用默认值
|
||
analyzedAt: new Date().toISOString(),
|
||
foodName: '营养成分表分析',
|
||
brand: '未知',
|
||
};
|
||
} |