feat: 新增营养摄入分析卡片并优化相关页面

- 在统计页面中引入营养摄入分析卡片,展示用户的营养数据
- 更新探索页面,增加营养数据加载逻辑,确保用户体验一致性
- 移除不再使用的推荐文章逻辑,简化代码结构
- 更新路由常量,确保路径管理集中化
- 优化体重历史记录卡片,提升用户交互体验
This commit is contained in:
richarjiang
2025-08-19 10:01:26 +08:00
parent c7d7255312
commit 9aa0a692a8
7 changed files with 452 additions and 201 deletions

View File

@@ -1,4 +1,3 @@
import dayjs from 'dayjs';
import { api } from './api';
export type Article = {
@@ -10,33 +9,6 @@ export type Article = {
readCount: number;
};
const demoArticles: Article[] = [
{
id: 'intro-pilates-posture',
title: '新手入门:普拉提核心与体态的关系',
coverImage: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
publishedAt: dayjs().subtract(2, 'day').toISOString(),
readCount: 1268,
htmlContent: `
<h2>为什么核心很重要?</h2>
<p>核心是维持良好体态与动作稳定的关键。普拉提通过强调呼吸与深层肌群激活,帮助你在<em>日常站立、坐姿与训练</em>中保持更好的身体对齐。</p>
<h3>入门建议</h3>
<ol>
<li>从呼吸开始:尝试<strong>胸廓外扩</strong>而非耸肩。</li>
<li>慢而可控:注意动作过程中的连贯与专注。</li>
<li>记录变化:每周拍照或在应用中记录体态变化。</li>
</ol>
<p>更多实操可在本应用的「AI体态评估」中获取个性化建议。</p>
<img src="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/ImageCheck.jpeg" alt="pilates-illustration" />
`,
},
];
export function listRecommendedArticles(): Article[] {
// 实际项目中可替换为 API 请求
return demoArticles;
}
export async function getArticleById(id: string): Promise<Article | undefined> {
return api.get<Article>(`/articles/${id}`);
}

115
services/dietRecords.ts Normal file
View File

@@ -0,0 +1,115 @@
import { api } from '@/services/api';
export type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack' | 'other';
export type RecordSource = 'manual' | 'vision' | 'other';
export type DietRecord = {
id: number;
mealType: MealType;
foodName: string;
foodDescription?: string;
weightGrams?: number;
portionDescription?: string;
estimatedCalories?: number;
proteinGrams?: number;
carbohydrateGrams?: number;
fatGrams?: number;
fiberGrams?: number;
sugarGrams?: number;
sodiumMg?: number;
additionalNutrition?: any;
source: RecordSource;
mealTime?: string;
imageUrl?: string;
notes?: string;
createdAt: string;
updatedAt: string;
};
export type NutritionSummary = {
totalCalories: number;
totalProtein: number;
totalCarbohydrate: number;
totalFat: number;
totalFiber: number;
totalSugar: number;
totalSodium: number;
};
export async function getDietRecords({
startDate,
endDate,
}: {
startDate: string;
endDate: string;
}): Promise<{
records: DietRecord[]
total: number
page: number
limit: number
}> {
const params = startDate && endDate ? `?startDate=${startDate}&endDate=${endDate}` : '';
return await api.get<{
records: DietRecord[]
total: number
page: number
limit: number
}>(`/users/diet-records${params}`);
}
export function calculateNutritionSummary(records: DietRecord[]): NutritionSummary {
if (records?.length === 0) {
return {
totalCalories: 0,
totalProtein: 0,
totalCarbohydrate: 0,
totalFat: 0,
totalFiber: 0,
totalSugar: 0,
totalSodium: 0,
};
}
return records.reduce(
(summary, record) => ({
totalCalories: summary.totalCalories + (record.estimatedCalories || 0),
totalProtein: summary.totalProtein + (record.proteinGrams || 0),
totalCarbohydrate: summary.totalCarbohydrate + (record.carbohydrateGrams || 0),
totalFat: summary.totalFat + (record.fatGrams || 0),
totalFiber: summary.totalFiber + (record.fiberGrams || 0),
totalSugar: summary.totalSugar + (record.sugarGrams || 0),
totalSodium: summary.totalSodium + (record.sodiumMg || 0),
}),
{
totalCalories: 0,
totalProtein: 0,
totalCarbohydrate: 0,
totalFat: 0,
totalFiber: 0,
totalSugar: 0,
totalSodium: 0,
}
);
}
// 将营养数据转换为雷达图数据0-5分制
export function convertToRadarData(summary: NutritionSummary): number[] {
// 基于推荐日摄入量计算分数
const recommendations = {
calories: 2000, // 卡路里
protein: 50, // 蛋白质(g)
carbohydrate: 300, // 碳水化合物(g)
fat: 65, // 脂肪(g)
fiber: 25, // 膳食纤维(g)
sodium: 2300, // 钠(mg)
};
return [
Math.min(5, (summary.totalCalories / recommendations.calories) * 5),
Math.min(5, (summary.totalProtein / recommendations.protein) * 5),
Math.min(5, (summary.totalCarbohydrate / recommendations.carbohydrate) * 5),
Math.min(5, (summary.totalFat / recommendations.fat) * 5),
Math.min(5, (summary.totalFiber / recommendations.fiber) * 5),
Math.min(5, Math.max(0, 5 - (summary.totalSodium / recommendations.sodium) * 5)), // 钠含量越低越好
];
}