diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx
index e091f1f..ed909e9 100644
--- a/app/(tabs)/coach.tsx
+++ b/app/(tabs)/coach.tsx
@@ -109,6 +109,7 @@ const CardType = {
WEIGHT_INPUT: '__WEIGHT_INPUT_CARD__',
DIET_INPUT: '__DIET_INPUT_CARD__',
DIET_TEXT_INPUT: '__DIET_TEXT_INPUT__',
+ DIET_PLAN: '__DIET_PLAN_CARD__',
} as const;
type CardType = typeof CardType[keyof typeof CardType];
@@ -279,6 +280,7 @@ export default function CoachScreen() {
// { key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
+ { key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
], [router, planDraft, checkin]);
const scrollToEnd = useCallback(() => {
@@ -1281,6 +1283,167 @@ export default function CoachScreen() {
);
}
+ if (item.content?.startsWith(CardType.DIET_PLAN)) {
+ const cardId = item.content.split('\n')?.[1] || '';
+
+ // 获取用户数据
+ const weight = userProfile?.weight ? Number(userProfile.weight) : 58;
+ const height = userProfile?.height ? Number(userProfile.height) : 160;
+
+ // 计算年龄
+ const calculateAge = (birthday?: string): number => {
+ if (!birthday) return 25; // 默认年龄
+ try {
+ const birthDate = new Date(birthday);
+ const today = new Date();
+ let age = today.getFullYear() - birthDate.getFullYear();
+ const monthDiff = today.getMonth() - birthDate.getMonth();
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
+ age--;
+ }
+ return age > 0 ? age : 25;
+ } catch {
+ return 25;
+ }
+ };
+
+ const age = calculateAge(userProfile?.birthDate);
+ const gender = userProfile?.gender || 'female';
+ const name = userProfile?.name || '用户';
+
+ // 计算相关数据
+ const bmi = calculateBMI(weight, height);
+ const bmiStatus = getBMIStatus(bmi);
+ const dailyCalories = calculateDailyCalories(weight, height, age, gender);
+ const nutrition = calculateNutritionDistribution(dailyCalories);
+
+ return (
+
+ {/* 标题部分 */}
+
+
+
+ 我的饮食方案
+
+ MY DIET PLAN
+
+
+ {/* 我的档案数据 */}
+
+ 我的档案数据
+
+
+
+ {name.charAt(0)}
+
+
+
+
+ {age}
+ 年龄/岁
+
+
+ {height.toFixed(1)}
+ 身高/CM
+
+
+ {weight.toFixed(1)}
+ 体重/KG
+
+
+
+
+
+ {/* BMI部分 */}
+
+
+ 当前BMI
+
+ {bmiStatus.status}
+
+
+ {bmi.toFixed(1)}
+
+
+
+
+
+
+
+ 偏瘦
+ 正常
+ 偏胖
+ 肥胖
+
+
+
+ {/* 饮食目标 */}
+
+
+ 饮食目标
+
+
+
+
+ {/* 目标体重 */}
+
+
+ 目标体重
+
+
+
+
+ {/* 每日推荐摄入 */}
+
+
+ 每日推荐摄入
+ {dailyCalories}千卡
+
+
+
+
+ {nutrition.carbs}g
+
+
+ 碳水
+
+
+
+ {nutrition.protein}g
+
+
+ 蛋白质
+
+
+
+ {nutrition.fat}g
+
+
+ 脂肪
+
+
+
+
+
+ 根据您的基础代谢率,这到理想体重所需要的热量缺口计算得出每日的热量推荐值。
+
+
+
+ {/* 底部按钮 */}
+ {
+ // 这里可以添加跳转到详细饮食方案页面的逻辑
+ console.log('跳转到饮食方案详情');
+ }}
+ >
+
+ 饮食方案
+
+
+ );
+ }
+
// 在流式回复过程中显示取消按钮
if (isStreaming && pendingAssistantIdRef.current === item.id && item.content?.trim()) {
return (
@@ -1380,6 +1543,58 @@ export default function CoachScreen() {
setTimeout(scrollToEnd, 100);
}
+ function insertDietPlanCard() {
+ const id = `dpcard_${Date.now()}`;
+ const payload = `${CardType.DIET_PLAN}\n${id}`;
+ setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
+ setTimeout(scrollToEnd, 100);
+ }
+
+ // 计算BMI
+ function calculateBMI(weight: number, height: number): number {
+ if (!weight || !height || weight <= 0 || height <= 0) return 0;
+ const heightInMeters = height / 100;
+ return Number((weight / (heightInMeters * heightInMeters)).toFixed(1));
+ }
+
+ // 获取BMI状态
+ function getBMIStatus(bmi: number): { status: string; color: string } {
+ if (bmi < 18.5) return { status: '偏瘦', color: '#87CEEB' };
+ if (bmi < 24) return { status: '正常', color: '#90EE90' };
+ if (bmi < 28) return { status: '偏胖', color: '#FFD700' };
+ return { status: '肥胖', color: '#FFA07A' };
+ }
+
+ // 计算每日推荐摄入热量
+ function calculateDailyCalories(weight: number, height: number, age: number, gender: string = 'female'): number {
+ if (!weight || !height || !age) return 1376; // 默认值
+
+ // 使用Harris-Benedict公式计算基础代谢率
+ let bmr: number;
+ if (gender === 'male') {
+ bmr = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age);
+ } else {
+ bmr = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age);
+ }
+
+ // 考虑活动水平,这里使用轻度活动的系数1.375
+ return Math.round(bmr * 1.375);
+ }
+
+ // 计算营养素分配
+ function calculateNutritionDistribution(calories: number) {
+ // 碳水化合物 50%,蛋白质 15%,脂肪 35%
+ const carbCalories = calories * 0.5;
+ const proteinCalories = calories * 0.15;
+ const fatCalories = calories * 0.35;
+
+ return {
+ carbs: Math.round(carbCalories / 4), // 1g碳水 = 4卡路里
+ protein: Math.round(proteinCalories / 4), // 1g蛋白质 = 4卡路里
+ fat: Math.round(fatCalories / 9), // 1g脂肪 = 9卡路里
+ };
+ }
+
async function handleSubmitWeight(text?: string, cardId?: string) {
const val = parseFloat(String(text ?? '').trim());
if (isNaN(val) || val <= 0 || val > 500) {
@@ -2419,6 +2634,181 @@ const styles = StyleSheet.create({
top: 0,
bottom: 0,
},
+ // 饮食方案卡片样式
+ dietPlanContainer: {
+ backgroundColor: 'rgba(255,255,255,0.95)',
+ borderRadius: 16,
+ padding: 16,
+ gap: 16,
+ borderWidth: 1,
+ borderColor: `${Colors.light.accentGreen}33`, // 20% opacity
+ },
+ dietPlanHeader: {
+ gap: 4,
+ },
+ dietPlanTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ dietPlanTitle: {
+ fontSize: 18,
+ fontWeight: '800',
+ color: '#192126',
+ },
+ dietPlanSubtitle: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: '#687076',
+ letterSpacing: 1,
+ },
+ profileSection: {
+ gap: 12,
+ },
+ sectionTitle: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: '#192126',
+ },
+ profileDataRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 16,
+ },
+ avatarContainer: {
+ alignItems: 'center',
+ },
+ profileStats: {
+ flexDirection: 'row',
+ flex: 1,
+ justifyContent: 'space-around',
+ },
+ statItem: {
+ alignItems: 'center',
+ gap: 4,
+ },
+ statValue: {
+ fontSize: 20,
+ fontWeight: '800',
+ color: '#192126',
+ },
+ statLabel: {
+ fontSize: 12,
+ color: '#687076',
+ },
+ bmiSection: {
+ gap: 12,
+ },
+ bmiHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ bmiStatusBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ bmiStatusText: {
+ fontSize: 12,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ },
+ bmiValue: {
+ fontSize: 32,
+ fontWeight: '800',
+ color: '#192126',
+ textAlign: 'center',
+ },
+ bmiScale: {
+ flexDirection: 'row',
+ height: 8,
+ borderRadius: 4,
+ overflow: 'hidden',
+ gap: 1,
+ },
+ bmiBar: {
+ flex: 1,
+ height: '100%',
+ },
+ bmiLabels: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingHorizontal: 4,
+ },
+ bmiLabel: {
+ fontSize: 11,
+ color: '#687076',
+ },
+ collapsibleSection: {
+ paddingVertical: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,0,0,0.06)',
+ },
+ collapsibleHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ caloriesSection: {
+ gap: 12,
+ },
+ caloriesHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ caloriesValue: {
+ fontSize: 18,
+ fontWeight: '800',
+ color: Colors.light.accentGreenDark,
+ },
+ nutritionGrid: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ gap: 16,
+ },
+ nutritionItem: {
+ alignItems: 'center',
+ gap: 8,
+ },
+ nutritionValue: {
+ fontSize: 24,
+ fontWeight: '800',
+ color: '#192126',
+ },
+ nutritionLabelRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ nutritionLabel: {
+ fontSize: 12,
+ color: '#687076',
+ },
+ nutritionNote: {
+ fontSize: 12,
+ color: '#687076',
+ lineHeight: 16,
+ textAlign: 'center',
+ marginTop: 8,
+ },
+ dietPlanButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ backgroundColor: Colors.light.accentGreenDark,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 12,
+ marginTop: 8,
+ },
+ dietPlanButtonText: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ },
});
const markdownStyles = {
diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index b38e6d6..f30e5dd 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -718,4 +718,19 @@ const styles = StyleSheet.create({
fontWeight: '700',
marginTop: 8,
},
+ weightCard: {
+ backgroundColor: '#F0F9FF',
+ },
+ weightValue: {
+ fontSize: 22,
+ color: '#0369A1',
+ fontWeight: '800',
+ marginTop: 8,
+ },
+ addWeightButton: {
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ padding: 4,
+ },
});
diff --git a/components/WeightHistoryCard.tsx b/components/WeightHistoryCard.tsx
index 834e3d7..781bbe9 100644
--- a/components/WeightHistoryCard.tsx
+++ b/components/WeightHistoryCard.tsx
@@ -13,6 +13,14 @@ import {
TouchableOpacity,
View,
} from 'react-native';
+import Animated, {
+ Extrapolation,
+ interpolate,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg';
const { width: screenWidth } = Dimensions.get('window');
@@ -29,6 +37,10 @@ export function WeightHistoryCard() {
const [isLoading, setIsLoading] = useState(false);
const [showChart, setShowChart] = useState(false);
+ // 动画相关状态
+ const animationProgress = useSharedValue(0);
+ const [isAnimating, setIsAnimating] = useState(false);
+
const { pushIfAuthedElseLogin } = useAuthGuard();
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
@@ -54,6 +66,104 @@ export function WeightHistoryCard() {
pushIfAuthedElseLogin(ROUTES.TAB_COACH);
};
+ // 切换图表显示状态的动画函数
+ const toggleChart = () => {
+ if (isAnimating) return; // 防止动画期间重复触发
+
+ setIsAnimating(true);
+ const newShowChart = !showChart;
+ setShowChart(newShowChart);
+
+ animationProgress.value = withTiming(
+ newShowChart ? 1 : 0,
+ {
+ duration: 350,
+ },
+ (finished) => {
+ if (finished) {
+ runOnJS(setIsAnimating)(false);
+ }
+ }
+ );
+ };
+
+ // 动画容器的高度动画
+ const containerAnimatedStyle = useAnimatedStyle(() => {
+ // 只有在展开状态时才应用固定高度
+ if (animationProgress.value === 0) {
+ return {};
+ }
+
+ const height = interpolate(
+ animationProgress.value,
+ [0, 1],
+ [40, 200], // 从摘要高度到图表高度
+ Extrapolation.CLAMP
+ );
+
+ return {
+ height,
+ };
+ });
+
+ // 摘要信息的动画样式
+ const summaryAnimatedStyle = useAnimatedStyle(() => {
+ const opacity = interpolate(
+ animationProgress.value,
+ [0, 0.4, 1],
+ [1, 0.2, 0],
+ Extrapolation.CLAMP
+ );
+
+ const scale = interpolate(
+ animationProgress.value,
+ [0, 1],
+ [1, 0.9],
+ Extrapolation.CLAMP
+ );
+
+ const translateY = interpolate(
+ animationProgress.value,
+ [0, 1],
+ [0, -20],
+ Extrapolation.CLAMP
+ );
+
+ return {
+ opacity,
+ transform: [{ scale }, { translateY }],
+ };
+ });
+
+ // 图表容器的动画样式
+ const chartAnimatedStyle = useAnimatedStyle(() => {
+ const opacity = interpolate(
+ animationProgress.value,
+ [0, 0.6, 1],
+ [0, 0.2, 1],
+ Extrapolation.CLAMP
+ );
+
+ const scale = interpolate(
+ animationProgress.value,
+ [0, 1],
+ [0.9, 1],
+ Extrapolation.CLAMP
+ );
+
+ const translateY = interpolate(
+ animationProgress.value,
+ [0, 1],
+ [20, 0],
+ Extrapolation.CLAMP
+ );
+
+ return {
+ opacity,
+ transform: [{ scale }, { translateY }],
+ };
+ });
+
// 如果正在加载,显示加载状态
if (isLoading) {
return (
@@ -167,7 +277,7 @@ export function WeightHistoryCard() {
setShowChart(!showChart)}
+ onPress={toggleChart}
activeOpacity={0.8}
>
- {/* 默认信息显示 */}
- {!showChart && sortedHistory.length > 0 && (
-
-
-
- 当前体重
- {userProfile.weight}kg
+ {/* 动画容器 */}
+ {sortedHistory.length > 0 && (
+
+ {/* 默认信息显示 - 带动画 */}
+
+
+
+ 当前体重
+ {userProfile.weight}kg
+
+
+ 记录天数
+ {sortedHistory.length}天
+
+
+ 变化范围
+
+ {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
+
+
-
- 记录天数
- {sortedHistory.length}天
-
-
- 变化范围
-
- {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
-
-
-
-
- )}
+
- {/* 图表容器 - 可折叠 */}
- {showChart && (
-
-
- {/* 图表底部信息 */}
-
-
- 当前体重
- {userProfile.weight}kg
+ {/* 图表底部信息 */}
+
+
+ 当前体重
+ {userProfile.weight}kg
+
+
+ 记录天数
+ {sortedHistory.length}天
+
+
+ 变化范围
+
+ {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
+
+
-
- 记录天数
- {sortedHistory.length}天
-
-
- 变化范围
-
- {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
+
+ {/* 最近记录时间 */}
+ {sortedHistory.length > 0 && (
+
+ 最近记录:{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
-
-
-
- {/* 最近记录时间 */}
- {sortedHistory.length > 0 && (
-
- 最近记录:{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
-
- )}
-
+ )}
+
+
)}
);
@@ -315,7 +426,7 @@ const styles = StyleSheet.create({
backgroundColor: '#FFFFFF',
borderRadius: 22,
padding: 18,
- marginBottom: 16,
+ marginBottom: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
@@ -390,7 +501,18 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '700',
},
+ animationContainer: {
+ position: 'relative',
+ overflow: 'hidden',
+ minHeight: 40,
+ },
+ summaryInfo: {
+ position: 'absolute',
+ width: '100%',
+ },
chartContainer: {
+ position: 'absolute',
+ width: '100%',
alignItems: 'center',
minHeight: 100,
},
@@ -419,14 +541,12 @@ const styles = StyleSheet.create({
fontSize: 12,
color: '#687076',
textAlign: 'center',
- marginTop: 8,
- },
- summaryInfo: {
+ marginTop: 4,
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-around',
- marginBottom: 6,
+ marginBottom: 0,
},
summaryItem: {
alignItems: 'center',
diff --git a/utils/health.ts b/utils/health.ts
index 8b3c8dd..c7b4a50 100644
--- a/utils/health.ts
+++ b/utils/health.ts
@@ -139,7 +139,7 @@ export async function fetchHealthDataForDate(date: Date): Promise