feat: 新增饮食方案卡片及相关计算功能

- 在教练页面中新增饮食方案卡片,展示用户的饮食计划和相关数据
- 实现BMI计算、每日推荐摄入热量计算及营养素分配功能
- 优化体重历史记录卡片,增加动画效果提升用户体验
- 更新统计页面样式,增加体重卡片样式和按钮功能
- 修复获取HRV值的逻辑,确保数据准确性
This commit is contained in:
richarjiang
2025-08-20 19:10:04 +08:00
parent 1c44c3083b
commit 3d47073d2f
4 changed files with 641 additions and 116 deletions

View File

@@ -109,6 +109,7 @@ const CardType = {
WEIGHT_INPUT: '__WEIGHT_INPUT_CARD__', WEIGHT_INPUT: '__WEIGHT_INPUT_CARD__',
DIET_INPUT: '__DIET_INPUT_CARD__', DIET_INPUT: '__DIET_INPUT_CARD__',
DIET_TEXT_INPUT: '__DIET_TEXT_INPUT__', DIET_TEXT_INPUT: '__DIET_TEXT_INPUT__',
DIET_PLAN: '__DIET_PLAN_CARD__',
} as const; } as const;
type CardType = typeof CardType[keyof typeof CardType]; type CardType = typeof CardType[keyof typeof CardType];
@@ -279,6 +280,7 @@ export default function CoachScreen() {
// { key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() }, // { key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() }, { key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() }, { key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
], [router, planDraft, checkin]); ], [router, planDraft, checkin]);
const scrollToEnd = useCallback(() => { 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()) { if (isStreaming && pendingAssistantIdRef.current === item.id && item.content?.trim()) {
return ( return (
@@ -1380,6 +1543,58 @@ export default function CoachScreen() {
setTimeout(scrollToEnd, 100); 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) { async function handleSubmitWeight(text?: string, cardId?: string) {
const val = parseFloat(String(text ?? '').trim()); const val = parseFloat(String(text ?? '').trim());
if (isNaN(val) || val <= 0 || val > 500) { if (isNaN(val) || val <= 0 || val > 500) {
@@ -2419,6 +2634,181 @@ const styles = StyleSheet.create({
top: 0, top: 0,
bottom: 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 = { const markdownStyles = {

View File

@@ -718,4 +718,19 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
marginTop: 8, marginTop: 8,
}, },
weightCard: {
backgroundColor: '#F0F9FF',
},
weightValue: {
fontSize: 22,
color: '#0369A1',
fontWeight: '800',
marginTop: 8,
},
addWeightButton: {
position: 'absolute',
right: 0,
top: 0,
padding: 4,
},
}); });

View File

@@ -13,6 +13,14 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } 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'; import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth } = Dimensions.get('window');
@@ -29,6 +37,10 @@ export function WeightHistoryCard() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showChart, setShowChart] = useState(false); const [showChart, setShowChart] = useState(false);
// 动画相关状态
const animationProgress = useSharedValue(0);
const [isAnimating, setIsAnimating] = useState(false);
const { pushIfAuthedElseLogin } = useAuthGuard(); const { pushIfAuthedElseLogin } = useAuthGuard();
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0; const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
@@ -54,6 +66,104 @@ export function WeightHistoryCard() {
pushIfAuthedElseLogin(ROUTES.TAB_COACH); 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) { if (isLoading) {
return ( return (
@@ -167,7 +277,7 @@ export function WeightHistoryCard() {
<View style={styles.headerButtons}> <View style={styles.headerButtons}>
<TouchableOpacity <TouchableOpacity
style={styles.chartToggleButton} style={styles.chartToggleButton}
onPress={() => setShowChart(!showChart)} onPress={toggleChart}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Ionicons <Ionicons
@@ -186,125 +296,126 @@ export function WeightHistoryCard() {
</View> </View>
</View> </View>
{/* 默认信息显示 */} {/* 动画容器 */}
{!showChart && sortedHistory.length > 0 && ( {sortedHistory.length > 0 && (
<View style={styles.summaryInfo}> <Animated.View style={[styles.animationContainer, containerAnimatedStyle]}>
<View style={styles.summaryRow}> {/* 默认信息显示 - 带动画 */}
<View style={styles.summaryItem}> <Animated.View style={[styles.summaryInfo, summaryAnimatedStyle]}>
<Text style={styles.summaryLabel}></Text> <View style={styles.summaryRow}>
<Text style={styles.summaryValue}>{userProfile.weight}kg</Text> <View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{userProfile.weight}kg</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text>
</View>
</View> </View>
<View style={styles.summaryItem}> </Animated.View>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text>
</View>
</View>
</View>
)}
{/* 图表容器 - 可折叠 */} {/* 图表容器 - 带动画 */}
{showChart && ( <Animated.View style={[styles.chartContainer, chartAnimatedStyle]}>
<View style={styles.chartContainer}> <Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}> {/* 背景网格线 */}
{/* 背景网格线 */} {[0, 1, 2, 3, 4].map(i => (
{[0, 1, 2, 3, 4].map(i => ( <Line
<Line key={`grid-${i}`}
key={`grid-${i}`} x1={PADDING}
x1={PADDING} y1={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
y1={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4} x2={CHART_WIDTH - PADDING}
x2={CHART_WIDTH - PADDING} y2={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4}
y2={PADDING + 15 + i * (CHART_HEIGHT - 2 * PADDING - 30) / 4} stroke="#F0F0F0"
stroke="#F0F0F0" strokeWidth={1}
strokeWidth={1} />
))}
{/* 折线 */}
<Path
d={singlePointPath}
stroke={Colors.light.accentGreen}
strokeWidth={3}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/> />
))}
{/* 折线 */} {/* 数据点和标签 */}
<Path {points.map((point, index) => {
d={singlePointPath} const isLastPoint = index === points.length - 1;
stroke={Colors.light.accentGreen} const isFirstPoint = index === 0;
strokeWidth={3} const showLabel = isFirstPoint || isLastPoint || points.length <= 3 || points.length === 1;
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* 数据点和标签 */} return (
{points.map((point, index) => { <React.Fragment key={index}>
const isLastPoint = index === points.length - 1; <Circle
const isFirstPoint = index === 0; cx={point.x}
const showLabel = isFirstPoint || isLastPoint || points.length <= 3 || points.length === 1; cy={point.y}
r={isLastPoint ? 6 : 4}
return ( fill={Colors.light.accentGreen}
<React.Fragment key={index}> stroke="#FFFFFF"
<Circle strokeWidth={2}
cx={point.x} />
cy={point.y} {/* 体重标签 - 只在关键点显示 */}
r={isLastPoint ? 6 : 4} {showLabel && (
fill={Colors.light.accentGreen} <>
stroke="#FFFFFF" <Circle
strokeWidth={2} cx={point.x}
/> cy={point.y - 15}
{/* 体重标签 - 只在关键点显示 */} r={10}
{showLabel && ( fill="rgba(255,255,255,0.9)"
<> stroke={Colors.light.accentGreen}
<Circle strokeWidth={1}
cx={point.x} />
cy={point.y - 15} <SvgText
r={10} x={point.x}
fill="rgba(255,255,255,0.9)" y={point.y - 12}
stroke={Colors.light.accentGreen} fontSize="9"
strokeWidth={1} fill="#192126"
/> textAnchor="middle"
<SvgText >
x={point.x} {point.weight}
y={point.y - 12} </SvgText>
fontSize="9" </>
fill="#192126" )}
textAnchor="middle" </React.Fragment>
> );
{point.weight} })}
</SvgText>
</>
)}
</React.Fragment>
);
})}
</Svg> </Svg>
{/* 图表底部信息 */} {/* 图表底部信息 */}
<View style={styles.chartInfo}> <View style={styles.chartInfo}>
<View style={styles.infoItem}> <View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text> <Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{userProfile.weight}kg</Text> <Text style={styles.infoValue}>{userProfile.weight}kg</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text>
</View>
</View> </View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}></Text> {/* 最近记录时间 */}
<Text style={styles.infoValue}>{sortedHistory.length}</Text> {sortedHistory.length > 0 && (
</View> <Text style={styles.lastRecordText}>
<View style={styles.infoItem}> {dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text> </Text>
</View> )}
</View> </Animated.View>
</Animated.View>
{/* 最近记录时间 */}
{sortedHistory.length > 0 && (
<Text style={styles.lastRecordText}>
{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')}
</Text>
)}
</View>
)} )}
</View> </View>
); );
@@ -315,7 +426,7 @@ const styles = StyleSheet.create({
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
borderRadius: 22, borderRadius: 22,
padding: 18, padding: 18,
marginBottom: 16, marginBottom: 8,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
@@ -390,7 +501,18 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '700', fontWeight: '700',
}, },
animationContainer: {
position: 'relative',
overflow: 'hidden',
minHeight: 40,
},
summaryInfo: {
position: 'absolute',
width: '100%',
},
chartContainer: { chartContainer: {
position: 'absolute',
width: '100%',
alignItems: 'center', alignItems: 'center',
minHeight: 100, minHeight: 100,
}, },
@@ -419,14 +541,12 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
color: '#687076', color: '#687076',
textAlign: 'center', textAlign: 'center',
marginTop: 8, marginTop: 4,
},
summaryInfo: {
}, },
summaryRow: { summaryRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
marginBottom: 6, marginBottom: 0,
}, },
summaryItem: { summaryItem: {
alignItems: 'center', alignItems: 'center',

View File

@@ -139,7 +139,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
// 获取最新的HRV值 // 获取最新的HRV值
const latestHrv = res[res.length - 1]; const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) { if (latestHrv && latestHrv.value) {
resolve(latestHrv.value); resolve(Math.round(latestHrv.value * 1000));
} else { } else {
resolve(null); resolve(null);
} }