Files
digital-pilates/services/nutritionLabelAnalysis.ts
richarjiang c6084fe702 feat(nutrition): 添加营养分析历史记录删除和图片预览功能
- 新增删除营养分析记录功能,支持本地状态更新和API调用
- 添加图片全屏预览功能,支持缩放和手势操作
- 实现Liquid Glass风格的删除按钮,包含兼容性处理
- 优化历史记录页面布局和交互体验
- 更新Memory Bank文档,添加Liquid Glass按钮实现指南
2025-10-16 16:43:45 +08:00

245 lines
6.9 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 { 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> {
try {
const response = await api.post<any>('/diet-records/analyze-nutrition-image', request);
// 处理不同的响应格式
if (Array.isArray(response)) {
// 如果直接返回数组,包装成标准格式
return {
success: true,
data: response as NutritionItem[]
};
} else if (response && typeof response === 'object') {
// 如果是对象,检查是否已经是标准格式
if (response.success !== undefined && response.data) {
return response as NutritionAnalysisResponse;
} else if (Array.isArray(response.data)) {
// 如果有data字段且是数组包装成标准格式
return {
success: true,
data: response.data as NutritionItem[]
};
}
}
// 如果都不匹配,返回错误
throw new Error('无法解析API返回结果');
} catch (error) {
console.error('[NUTRITION_ANALYSIS] API调用失败:', error);
return {
success: false,
data: [],
message: error instanceof Error ? error.message : '分析失败'
};
}
}
// 营养成分分析记录的接口定义
export interface NutritionAnalysisRecord {
id: number;
userId: string;
imageUrl: string;
analysisResult: {
data: NutritionItem[];
success: boolean;
message?: string;
};
status: 'success' | 'failed' | 'processing';
message: string;
aiProvider: string;
aiModel: string;
nutritionCount: number;
createdAt: string;
updatedAt: string;
}
// 获取历史记录的请求参数
export interface GetNutritionRecordsParams {
startDate?: string;
endDate?: string;
status?: string;
page?: number;
limit?: number;
}
// 获取历史记录的响应格式
export interface GetNutritionRecordsResponse {
code: number;
message: string;
data: {
records: NutritionAnalysisRecord[];
total: number;
page: number;
limit: number;
totalPages: number;
};
}
/**
* 获取营养成分分析记录列表
*/
export async function getNutritionAnalysisRecords(params?: GetNutritionRecordsParams): Promise<GetNutritionRecordsResponse> {
try {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.append(key, String(value));
}
});
}
const queryString = searchParams.toString() ? `?${searchParams.toString()}` : '';
// 使用 api.get 方法,但需要特殊处理响应格式
const response = await api.get<any>(`/diet-records/nutrition-analysis-records${queryString}`);
// 检查响应是否已经是标准格式
if (response && typeof response === 'object' && 'code' in response) {
return response as GetNutritionRecordsResponse;
}
// 如果不是标准格式,包装成标准格式
return {
code: 0,
message: '获取成功',
data: {
records: response.records || [],
total: response.total || 0,
page: response.page || 1,
limit: response.limit || 20,
totalPages: response.totalPages || Math.ceil((response.total || 0) / (response.limit || 20))
}
};
} catch (error) {
console.error('[NUTRITION_RECORDS] 获取历史记录失败:', error);
// 返回错误格式的响应
return {
code: 1,
message: error instanceof Error ? error.message : '获取营养成分分析记录失败,请稍后重试',
data: {
records: [],
total: 0,
page: 1,
limit: 20,
totalPages: 0
}
};
}
}
/**
* 删除营养成分分析记录
*/
export async function deleteNutritionAnalysisRecord(recordId: number): Promise<any> {
try {
const response = await api.delete(`/diet-records/nutrition-analysis-records/${recordId}`);
return response;
} catch (error) {
console.error('[NUTRITION_RECORDS] 删除记录失败:', error);
throw error;
}
}