feat: 新增饮食方案卡片及相关计算功能
- 在教练页面中新增饮食方案卡片,展示用户的饮食计划和相关数据 - 实现BMI计算、每日推荐摄入热量计算及营养素分配功能 - 优化体重历史记录卡片,增加动画效果提升用户体验 - 更新统计页面样式,增加体重卡片样式和按钮功能 - 修复获取HRV值的逻辑,确保数据准确性
This commit is contained in:
@@ -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 (
|
||||
<View style={styles.dietPlanContainer}>
|
||||
{/* 标题部分 */}
|
||||
<View style={styles.dietPlanHeader}>
|
||||
<View style={styles.dietPlanTitleContainer}>
|
||||
<Ionicons name="restaurant-outline" size={20} color={Colors.light.accentGreenDark} />
|
||||
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
||||
</View>
|
||||
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
||||
</View>
|
||||
|
||||
{/* 我的档案数据 */}
|
||||
<View style={styles.profileSection}>
|
||||
<Text style={styles.sectionTitle}>我的档案数据</Text>
|
||||
<View style={styles.profileDataRow}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<View style={styles.avatar}>
|
||||
<Text style={styles.avatarText}>{name.charAt(0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.profileStats}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{age}</Text>
|
||||
<Text style={styles.statLabel}>年龄/岁</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{height.toFixed(1)}</Text>
|
||||
<Text style={styles.statLabel}>身高/CM</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{weight.toFixed(1)}</Text>
|
||||
<Text style={styles.statLabel}>体重/KG</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BMI部分 */}
|
||||
<View style={styles.bmiSection}>
|
||||
<View style={styles.bmiHeader}>
|
||||
<Text style={styles.sectionTitle}>当前BMI</Text>
|
||||
<View style={[styles.bmiStatusBadge, { backgroundColor: bmiStatus.color }]}>
|
||||
<Text style={styles.bmiStatusText}>{bmiStatus.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.bmiValue}>{bmi.toFixed(1)}</Text>
|
||||
<View style={styles.bmiScale}>
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#87CEEB' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#90EE90' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#FFD700' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#FFA07A' }]} />
|
||||
</View>
|
||||
<View style={styles.bmiLabels}>
|
||||
<Text style={styles.bmiLabel}>偏瘦</Text>
|
||||
<Text style={styles.bmiLabel}>正常</Text>
|
||||
<Text style={styles.bmiLabel}>偏胖</Text>
|
||||
<Text style={styles.bmiLabel}>肥胖</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 饮食目标 */}
|
||||
<View style={styles.collapsibleSection}>
|
||||
<View style={styles.collapsibleHeader}>
|
||||
<Text style={styles.sectionTitle}>饮食目标</Text>
|
||||
<Ionicons name="chevron-down" size={16} color="#687076" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 目标体重 */}
|
||||
<View style={styles.collapsibleSection}>
|
||||
<View style={styles.collapsibleHeader}>
|
||||
<Text style={styles.sectionTitle}>目标体重</Text>
|
||||
<Ionicons name="chevron-down" size={16} color="#687076" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 每日推荐摄入 */}
|
||||
<View style={styles.caloriesSection}>
|
||||
<View style={styles.caloriesHeader}>
|
||||
<Text style={styles.sectionTitle}>每日推荐摄入</Text>
|
||||
<Text style={styles.caloriesValue}>{dailyCalories}千卡</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.nutritionGrid}>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.carbs}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="nutrition-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>碳水</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.protein}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="fitness-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>蛋白质</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.fat}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="water-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>脂肪</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.nutritionNote}>
|
||||
根据您的基础代谢率,这到理想体重所需要的热量缺口计算得出每日的热量推荐值。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<TouchableOpacity
|
||||
style={styles.dietPlanButton}
|
||||
onPress={() => {
|
||||
// 这里可以添加跳转到详细饮食方案页面的逻辑
|
||||
console.log('跳转到饮食方案详情');
|
||||
}}
|
||||
>
|
||||
<Ionicons name="restaurant" size={16} color="#FFFFFF" />
|
||||
<Text style={styles.dietPlanButtonText}>饮食方案</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 在流式回复过程中显示取消按钮
|
||||
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 = {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
<View style={styles.headerButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.chartToggleButton}
|
||||
onPress={() => setShowChart(!showChart)}
|
||||
onPress={toggleChart}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons
|
||||
@@ -186,9 +296,11 @@ export function WeightHistoryCard() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 默认信息显示 */}
|
||||
{!showChart && sortedHistory.length > 0 && (
|
||||
<View style={styles.summaryInfo}>
|
||||
{/* 动画容器 */}
|
||||
{sortedHistory.length > 0 && (
|
||||
<Animated.View style={[styles.animationContainer, containerAnimatedStyle]}>
|
||||
{/* 默认信息显示 - 带动画 */}
|
||||
<Animated.View style={[styles.summaryInfo, summaryAnimatedStyle]}>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>当前体重</Text>
|
||||
@@ -205,12 +317,10 @@ export function WeightHistoryCard() {
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* 图表容器 - 可折叠 */}
|
||||
{showChart && (
|
||||
<View style={styles.chartContainer}>
|
||||
{/* 图表容器 - 带动画 */}
|
||||
<Animated.View style={[styles.chartContainer, chartAnimatedStyle]}>
|
||||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||||
{/* 背景网格线 */}
|
||||
{[0, 1, 2, 3, 4].map(i => (
|
||||
@@ -304,7 +414,8 @@ export function WeightHistoryCard() {
|
||||
最近记录:{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
@@ -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',
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
// 获取最新的HRV值
|
||||
const latestHrv = res[res.length - 1];
|
||||
if (latestHrv && latestHrv.value) {
|
||||
resolve(latestHrv.value);
|
||||
resolve(Math.round(latestHrv.value * 1000));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user