- 新增历史记录页面,支持查看、筛选和分页加载营养成分分析记录 - 在分析页面添加历史记录入口,使用Liquid Glass效果 - 优化分析结果展示样式,采用卡片式布局和渐变效果 - 移除流式分析相关代码,简化分析流程 - 添加历史记录API接口和类型定义
232 lines
6.5 KiB
TypeScript
232 lines
6.5 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> {
|
||
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
|
||
}
|
||
};
|
||
}
|
||
}
|