feat: 更新统计页面,优化HRV数据展示和逻辑
- 移除模拟HRV数据,改为从健康数据中获取实际HRV值 - 新增HRV更新时间显示,提升用户信息获取体验 - 优化日期推导逻辑,确保数据加载一致性 - 更新BMI卡片和营养雷达图组件,支持紧凑模式展示 - 移除不再使用的图片资源,简化项目结构
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import {
|
||||
BMI_CATEGORIES,
|
||||
canCalculateBMI,
|
||||
getBMIResult,
|
||||
type BMIResult
|
||||
BMI_CATEGORIES,
|
||||
canCalculateBMI,
|
||||
getBMIResult,
|
||||
type BMIResult
|
||||
} from '@/utils/bmi';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
Dimensions,
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
@@ -24,9 +24,10 @@ interface BMICardProps {
|
||||
weight?: number;
|
||||
height?: number;
|
||||
style?: any;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function BMICard({ weight, height, style }: BMICardProps) {
|
||||
export function BMICard({ weight, height, style, compact = false }: BMICardProps) {
|
||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||
const [showInfoModal, setShowInfoModal] = useState(false);
|
||||
|
||||
@@ -61,14 +62,75 @@ export function BMICard({ weight, height, style }: BMICardProps) {
|
||||
if (!canCalculate) {
|
||||
// 缺少数据的情况
|
||||
return (
|
||||
<View style={styles.incompleteContent}>
|
||||
<TouchableOpacity
|
||||
style={[styles.incompleteContent, compact && styles.compactIncompleteContent]}
|
||||
onPress={handleShowInfoModal}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<View style={styles.iconSquare}>
|
||||
<Ionicons name="fitness-outline" size={18} color="#192126" />
|
||||
<Ionicons name="fitness-outline" size={16} color="#192126" />
|
||||
</View>
|
||||
<Text style={styles.cardTitle}>BMI 指数</Text>
|
||||
<Text style={[styles.cardTitle, compact && styles.compactTitle]}>BMI</Text>
|
||||
</View>
|
||||
{!compact && (
|
||||
<TouchableOpacity
|
||||
onPress={handleShowInfoModal}
|
||||
style={styles.infoButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="information-circle-outline" size={20} color="#9AA3AE" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{compact ? (
|
||||
<View style={styles.compactMissingData}>
|
||||
<Text style={styles.compactMissingText}>
|
||||
{!weight && !height ? '完善身高体重' :
|
||||
!weight ? '完善体重' : '完善身高'}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.missingDataContainer}>
|
||||
<Ionicons name="alert-circle-outline" size={24} color="#F59E0B" />
|
||||
<Text style={styles.missingDataText}>
|
||||
{!weight && !height ? '请完善身高和体重信息' :
|
||||
!weight ? '请完善体重信息' : '请完善身高信息'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleGoToProfile}
|
||||
style={styles.completeButton}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.completeButtonText}>前往完善</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#6B7280" />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// 有完整数据的情况
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.completeContent, { backgroundColor: bmiResult?.backgroundColor }, compact && styles.compactCompleteContent]}
|
||||
onPress={handleShowInfoModal}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<View style={styles.iconSquare}>
|
||||
<Ionicons name="fitness-outline" size={16} color="#192126" />
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, compact && styles.compactTitle]}>BMI</Text>
|
||||
</View>
|
||||
{!compact && (
|
||||
<TouchableOpacity
|
||||
onPress={handleShowInfoModal}
|
||||
style={styles.infoButton}
|
||||
@@ -76,66 +138,41 @@ export function BMICard({ weight, height, style }: BMICardProps) {
|
||||
>
|
||||
<Ionicons name="information-circle-outline" size={20} color="#9AA3AE" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.missingDataContainer}>
|
||||
<Ionicons name="alert-circle-outline" size={24} color="#F59E0B" />
|
||||
<Text style={styles.missingDataText}>
|
||||
{!weight && !height ? '请完善身高和体重信息' :
|
||||
!weight ? '请完善体重信息' : '请完善身高信息'}
|
||||
{compact ? (
|
||||
<View style={styles.compactBMIContent}>
|
||||
<Text style={[styles.compactBMIValue, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.value}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleGoToProfile}
|
||||
style={styles.completeButton}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.completeButtonText}>前往完善</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#6B7280" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 有完整数据的情况
|
||||
return (
|
||||
<View style={[styles.completeContent, { backgroundColor: bmiResult?.backgroundColor }]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<View style={styles.iconSquare}>
|
||||
<Ionicons name="fitness-outline" size={18} color="#192126" />
|
||||
</View>
|
||||
<Text style={styles.cardTitle}>BMI 指数</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleShowInfoModal}
|
||||
style={styles.infoButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="information-circle-outline" size={20} color="#9AA3AE" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.bmiValueContainer}>
|
||||
<Text style={[styles.bmiValue, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.value}
|
||||
</Text>
|
||||
<View style={[styles.categoryBadge, { backgroundColor: bmiResult?.color + '20' }]}>
|
||||
<Text style={[styles.categoryText, { color: bmiResult?.color }]}>
|
||||
<Text style={[styles.compactBMICategory, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.category.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.bmiValueContainer}>
|
||||
<Text style={[styles.bmiValue, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.value}
|
||||
</Text>
|
||||
<View style={[styles.categoryBadge, { backgroundColor: bmiResult?.color + '20' }]}>
|
||||
<Text style={[styles.categoryText, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.category.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.bmiDescription, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.description}
|
||||
</Text>
|
||||
<Text style={[styles.bmiDescription, { color: bmiResult?.color }]}>
|
||||
{bmiResult?.description}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.encouragementText}>
|
||||
{bmiResult?.category.encouragement}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.encouragementText}>
|
||||
{bmiResult?.category.encouragement}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -359,6 +396,46 @@ const styles = StyleSheet.create({
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
// 紧凑模式样式
|
||||
compactIncompleteContent: {
|
||||
minHeight: 110,
|
||||
padding: 14,
|
||||
margin: -14,
|
||||
},
|
||||
compactCompleteContent: {
|
||||
minHeight: 110,
|
||||
padding: 14,
|
||||
margin: -14,
|
||||
},
|
||||
compactTitle: {
|
||||
fontSize: 14,
|
||||
},
|
||||
compactMissingData: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
compactMissingText: {
|
||||
fontSize: 12,
|
||||
color: '#9AA3AE',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
},
|
||||
compactBMIContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
compactBMIValue: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
marginBottom: 4,
|
||||
},
|
||||
compactBMICategory: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
// 弹窗样式
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { RadarCategory, RadarChart } from './RadarChart';
|
||||
|
||||
export type NutritionRadarCardProps = {
|
||||
nutritionSummary: NutritionSummary | null;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
// 营养维度定义
|
||||
@@ -22,7 +21,7 @@ const NUTRITION_DIMENSIONS: RadarCategory[] = [
|
||||
{ key: 'sodium', label: '钠' },
|
||||
];
|
||||
|
||||
export function NutritionRadarCard({ nutritionSummary, isLoading = false }: NutritionRadarCardProps) {
|
||||
export function NutritionRadarCard({ nutritionSummary }: NutritionRadarCardProps) {
|
||||
const radarValues = useMemo(() => {
|
||||
// 基于推荐日摄入量计算分数
|
||||
const recommendations = {
|
||||
@@ -71,33 +70,26 @@ export function NutritionRadarCard({ nutritionSummary, isLoading = false }: Nutr
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>加载中...</Text>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.radarContainer}>
|
||||
<RadarChart
|
||||
categories={NUTRITION_DIMENSIONS}
|
||||
values={radarValues}
|
||||
size="small"
|
||||
maxValue={5}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.radarContainer}>
|
||||
<RadarChart
|
||||
categories={NUTRITION_DIMENSIONS}
|
||||
values={radarValues}
|
||||
size="small"
|
||||
maxValue={5}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.statsContainer}>
|
||||
{nutritionStats.map((stat, index) => (
|
||||
<View key={stat.label} style={styles.statItem}>
|
||||
<View style={[styles.statDot, { backgroundColor: stat.color }]} />
|
||||
<Text style={styles.statLabel}>{stat.label}</Text>
|
||||
<Text style={styles.statValue}>{stat.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.statsContainer}>
|
||||
{nutritionStats.map((stat, index) => (
|
||||
<View key={stat.label} style={styles.statItem}>
|
||||
<View style={[styles.statDot, { backgroundColor: stat.color }]} />
|
||||
<Text style={styles.statLabel}>{stat.label}</Text>
|
||||
<Text style={styles.statValue}>{stat.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -176,14 +168,4 @@ const styles = StyleSheet.create({
|
||||
color: '#192126',
|
||||
fontWeight: '700',
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 80,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
color: '#9AA3AE',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,25 +5,35 @@ import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
interface StressMeterProps {
|
||||
value: number;
|
||||
status: '放松' | '正常' | '紧张';
|
||||
updateTime?: Date;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function StressMeter({ value, status }: StressMeterProps) {
|
||||
export function StressMeter({ value, updateTime, style }: StressMeterProps) {
|
||||
|
||||
// 计算进度条位置(0-100%)
|
||||
const progressPercentage = Math.min(100, Math.max(0, (value / 150) * 100));
|
||||
// HRV值范围:30-110ms,对应进度条0-100%
|
||||
const progressPercentage = Math.min(100, Math.max(0, ((value - 30) / 80) * 100));
|
||||
|
||||
// 根据状态获取颜色
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case '放松': return '#10B981';
|
||||
case '正常': return '#F59E0B';
|
||||
case '紧张': return '#EF4444';
|
||||
default: return '#F59E0B';
|
||||
// 根据HRV值计算状态
|
||||
const getHrvStatus = () => {
|
||||
if (value >= 70) {
|
||||
return '放松';
|
||||
} else if (value >= 50) {
|
||||
return '正常';
|
||||
} else {
|
||||
return '紧张';
|
||||
}
|
||||
};
|
||||
|
||||
// 根据状态获取表情
|
||||
const getStatusEmoji = () => {
|
||||
// 当HRV值为0时,不展示表情
|
||||
if (value === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const status = getHrvStatus();
|
||||
switch (status) {
|
||||
case '放松': return '😌';
|
||||
case '正常': return '😊';
|
||||
@@ -32,13 +42,29 @@ export function StressMeter({ value, status }: StressMeterProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化更新时间
|
||||
const formatUpdateTime = (date?: Date) => {
|
||||
if (!date) return '';
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, style]}>
|
||||
{/* 渐变背景 */}
|
||||
<LinearGradient
|
||||
colors={['#F8F9FF', '#F0F4FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.gradientBackground}
|
||||
/>
|
||||
|
||||
{/* 头部区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.leftSection}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="heart" size={20} color="#3B82F6" />
|
||||
<Ionicons name="heart" size={16} color="#3B82F6" />
|
||||
</View>
|
||||
<Text style={styles.title}>压力</Text>
|
||||
</View>
|
||||
@@ -55,18 +81,21 @@ export function StressMeter({ value, status }: StressMeterProps) {
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressTrack}>
|
||||
{/* 渐变背景进度条 */}
|
||||
<View style={[styles.progressBar, { width: `${progressPercentage}%` }]}>
|
||||
<LinearGradient
|
||||
colors={['#F97316', '#FCD34D', '#84CC16', '#10B981']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.gradientBar}
|
||||
/>
|
||||
</View>
|
||||
<LinearGradient
|
||||
colors={['#FFD700', '#87CEEB', '#98FB98']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.gradientTrack}
|
||||
/>
|
||||
{/* 白色圆形指示器 */}
|
||||
<View style={[styles.indicator, { left: `${Math.max(0, progressPercentage - 2)}%` }]} />
|
||||
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 更新时间 */}
|
||||
{updateTime && (
|
||||
<Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -74,8 +103,8 @@ export function StressMeter({ value, status }: StressMeterProps) {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
padding: 14,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
@@ -83,88 +112,101 @@ const styles = StyleSheet.create({
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 12,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: 110,
|
||||
width: 140,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 16,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
leftSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#EBF4FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 8,
|
||||
marginRight: 6,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
color: '#192126',
|
||||
},
|
||||
emoji: {
|
||||
fontSize: 24,
|
||||
fontSize: 16,
|
||||
},
|
||||
valueSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
marginBottom: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
value: {
|
||||
fontSize: 42,
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: '#1F2937',
|
||||
lineHeight: 46,
|
||||
color: '#192126',
|
||||
lineHeight: 32,
|
||||
},
|
||||
unit: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#6B7280',
|
||||
color: '#9AA3AE',
|
||||
marginLeft: 4,
|
||||
},
|
||||
progressContainer: {
|
||||
height: 20,
|
||||
height: 16,
|
||||
marginBottom: 4,
|
||||
},
|
||||
progressTrack: {
|
||||
height: 12,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 6,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
},
|
||||
progressBar: {
|
||||
gradientTrack: {
|
||||
height: '100%',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
gradientBar: {
|
||||
height: '100%',
|
||||
borderRadius: 6,
|
||||
borderRadius: 4,
|
||||
},
|
||||
indicator: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
borderWidth: 2,
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
updateTime: {
|
||||
fontSize: 10,
|
||||
color: '#9AA3AE',
|
||||
textAlign: 'right',
|
||||
marginTop: 2,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user