feat: 优化统计页面和BMI卡片,移除压力分析相关代码

- 在统计页面中移除压力分析模态框及相关状态管理,简化组件逻辑
- 更新BMI卡片,改进弹窗展示方式,增加渐变背景和健康建议
- 新增更新体重的功能,支持将体重数据写入健康数据中
- 优化压力计组件,调整数据展示逻辑,提升用户体验
This commit is contained in:
richarjiang
2025-08-20 17:25:42 +08:00
parent d76ba48424
commit 1c44c3083b
4 changed files with 334 additions and 157 deletions

View File

@@ -3,7 +3,6 @@ import { BMICard } from '@/components/BMICard';
import { CircularRing } from '@/components/CircularRing';
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar';
import { StressAnalysisModal } from '@/components/StressAnalysisModal';
import { StressMeter } from '@/components/StressMeter';
import { WeightHistoryCard } from '@/components/WeightHistoryCard';
import { Colors } from '@/constants/Colors';
@@ -87,9 +86,6 @@ export default function ExploreScreen() {
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
// 压力分析浮窗状态
const [showStressModal, setShowStressModal] = useState(false);
// 记录最近一次请求的“日期键”,避免旧请求覆盖新结果
const latestRequestKeyRef = useRef<string | null>(null);
@@ -209,15 +205,6 @@ export default function ExploreScreen() {
}
};
// 处理压力卡片点击
const handleStressCardPress = () => {
setShowStressModal(true);
};
// 关闭压力分析浮窗
const handleCloseStressModal = () => {
setShowStressModal(false);
};
// 使用统一的渐变背景色
const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const;
@@ -280,7 +267,7 @@ export default function ExploreScreen() {
value={hrvValue}
updateTime={hrvUpdateTime}
style={styles.masonryCard}
onPress={handleStressCardPress}
hrvValue={hrvValue}
/>
<View style={[styles.masonryCard, styles.caloriesCard]}>
@@ -371,14 +358,6 @@ export default function ExploreScreen() {
</ScrollView>
</SafeAreaView>
{/* 压力分析浮窗 */}
<StressAnalysisModal
visible={showStressModal}
onClose={handleCloseStressModal}
hrvValue={hrvValue}
updateTime={hrvUpdateTime}
/>
</View>
);
}

View File

@@ -1,5 +1,6 @@
import { Colors } from '@/constants/Colors';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import {
BMI_CATEGORIES,
canCalculateBMI,
@@ -7,11 +8,12 @@ import {
type BMIResult
} from '@/utils/bmi';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react';
import {
Dimensions,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
@@ -177,6 +179,9 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
);
};
const colorScheme = useColorScheme();
const themeColors = Colors[colorScheme ?? 'light'];
return (
<>
<View style={[styles.card, style]}>
@@ -186,99 +191,102 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
{/* BMI 信息弹窗 */}
<Modal
visible={showInfoModal}
transparent
animationType="fade"
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={handleHideInfoModal}
>
<Pressable
style={styles.modalBackdrop}
onPress={handleHideInfoModal}
<LinearGradient
colors={[themeColors.backgroundGradientStart, themeColors.backgroundGradientEnd]}
style={styles.newModalContainer}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
<Pressable
style={styles.modalContainer}
onPress={(e) => e.stopPropagation()}
>
{/* 弹窗头部 */}
<View style={styles.modalHeader}>
<View style={styles.modalTitleContainer}>
<View style={styles.modalIconContainer}>
<Ionicons name="fitness" size={24} color="#3B82F6" />
</View>
<Text style={styles.modalTitle}>BMI </Text>
</View>
<TouchableOpacity
onPress={handleHideInfoModal}
style={styles.closeButton}
activeOpacity={0.7}
>
<Ionicons name="close" size={20} color="#6B7280" />
</TouchableOpacity>
</View>
<ScrollView style={styles.newContent} showsVerticalScrollIndicator={false}>
{/* 标题 */}
<Text style={styles.newTitle}>BMI </Text>
{/* 内容区域 - 去除滚动,精简设计 */}
<View style={styles.modalContent}>
{/* 介绍部分 */}
<View style={styles.introSection}>
<Text style={styles.modalDescription}>
BMI
</Text>
<View style={styles.formulaContainer}>
<Text style={styles.formulaText}>
(kg) ÷ ²(m)
</Text>
</View>
</View>
{/* BMI 分类标准 - 紧凑设计 */}
<View style={styles.categoriesSection}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.categoriesGrid}>
{BMI_CATEGORIES.map((category, index) => {
const colors = index === 0 ? { bg: '#FEF3C7', text: '#B45309' } :
index === 1 ? { bg: '#E8F5E8', text: Colors.light.accentGreenDark } :
index === 2 ? { bg: '#FEF3C7', text: '#B45309' } :
{ bg: '#FEE2E2', text: '#B91C1C' };
return (
<View key={index} style={[styles.categoryCompact, { backgroundColor: colors.bg }]}>
<View style={styles.categoryCompactHeader}>
<Text style={[styles.categoryCompactName, { color: colors.text }]}>
{category.name}
</Text>
<Text style={[styles.categoryCompactRange, { color: colors.text }]}>
{category.range}
</Text>
</View>
<Text style={[styles.categoryCompactAdvice, { color: colors.text }]} numberOfLines={2}>
{category.advice}
</Text>
</View>
);
})}
</View>
</View>
{/* 健康提示 - 简化版 */}
<View style={styles.healthTips}>
<View style={styles.tipsHeader}>
<Ionicons name="heart" size={16} color="#EF4444" />
<Text style={styles.tipsTitle}></Text>
</View>
<Text style={styles.tipsContent}>
</Text>
</View>
{/* 免责声明 - 精简版 */}
<View style={styles.disclaimerCompact}>
<Ionicons name="information-circle" size={14} color="#6B7280" />
<Text style={styles.disclaimerCompactText}>
BMI
{/* 介绍部分 */}
<View style={styles.newIntroSection}>
<Text style={styles.newDescription}>
BMI
</Text>
<View style={styles.newFormulaContainer}>
<Text style={styles.newFormulaText}>
(kg) ÷ ²(m)
</Text>
</View>
</View>
</Pressable>
</Pressable>
{/* BMI 分类标准 */}
<Text style={styles.newSectionTitle}>BMI </Text>
<View style={styles.newStatsCard}>
{BMI_CATEGORIES.map((category, index) => {
const colors = [
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦
{ bg: '#E8F5E8', text: Colors.light.accentGreenDark, border: Colors.light.accentGreen }, // 正常
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
][index];
return (
<View key={index} style={[styles.newStatItem, { backgroundColor: colors.bg, borderColor: colors.border }]}>
<View style={styles.newStatHeader}>
<Text style={[styles.newStatTitle, { color: colors.text }]}>
{category.name}
</Text>
<Text style={[styles.newStatRange, { color: colors.text }]}>
{category.range}
</Text>
</View>
<Text style={[styles.newStatAdvice, { color: colors.text }]}>
{category.advice}
</Text>
</View>
);
})}
</View>
{/* 健康建议 */}
<Text style={styles.newSectionTitle}></Text>
<View style={styles.newHealthTips}>
<View style={styles.newTipsItem}>
<Ionicons name="nutrition-outline" size={20} color="#3B82F6" />
<Text style={styles.newTipsText}></Text>
</View>
<View style={styles.newTipsItem}>
<Ionicons name="walk-outline" size={20} color="#3B82F6" />
<Text style={styles.newTipsText}>150</Text>
</View>
<View style={styles.newTipsItem}>
<Ionicons name="moon-outline" size={20} color="#3B82F6" />
<Text style={styles.newTipsText}>7-9</Text>
</View>
<View style={styles.newTipsItem}>
<Ionicons name="calendar-outline" size={20} color="#3B82F6" />
<Text style={styles.newTipsText}></Text>
</View>
</View>
{/* 免责声明 */}
<View style={styles.newDisclaimer}>
<Ionicons name="information-circle-outline" size={16} color="#6B7280" />
<Text style={styles.newDisclaimerText}>
BMI
</Text>
</View>
</ScrollView>
{/* 底部继续按钮 */}
<View style={styles.newBottomContainer}>
<TouchableOpacity style={styles.newContinueButton} onPress={handleHideInfoModal}>
<View style={styles.newButtonBackground}>
<Text style={styles.newButtonText}></Text>
</View>
</TouchableOpacity>
<View style={styles.newHomeIndicator} />
</View>
</LinearGradient>
</Modal>
</>
);
@@ -614,4 +622,159 @@ const styles = StyleSheet.create({
flex: 1,
},
// 新样式 - 与StressAnalysisModal保持一致
newModalContainer: {
flex: 1,
},
newContent: {
flex: 1,
paddingHorizontal: 20,
},
newTitle: {
fontSize: 24,
fontWeight: '800',
color: '#111827',
textAlign: 'center',
marginTop: 20,
marginBottom: 32,
},
newIntroSection: {
marginBottom: 32,
},
newDescription: {
fontSize: 16,
color: '#374151',
lineHeight: 24,
textAlign: 'center',
marginBottom: 16,
},
newFormulaContainer: {
backgroundColor: '#F8FAFC',
borderRadius: 12,
padding: 16,
borderLeftWidth: 4,
borderLeftColor: '#3B82F6',
},
newFormulaText: {
fontSize: 15,
color: '#1F2937',
fontWeight: '600',
textAlign: 'center',
},
newSectionTitle: {
fontSize: 22,
fontWeight: '800',
color: '#111827',
marginBottom: 20,
},
newStatsCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 32,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
newStatItem: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
marginBottom: 12,
borderLeftWidth: 4,
},
newStatHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
newStatTitle: {
fontSize: 16,
fontWeight: '700',
},
newStatRange: {
fontSize: 14,
fontWeight: '600',
opacity: 0.8,
},
newStatAdvice: {
fontSize: 14,
lineHeight: 20,
fontWeight: '500',
opacity: 0.9,
},
newHealthTips: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 32,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
newTipsItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
newTipsText: {
fontSize: 15,
color: '#374151',
marginLeft: 12,
flex: 1,
},
newDisclaimer: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#F9FAFB',
borderRadius: 12,
padding: 16,
marginBottom: 32,
},
newDisclaimerText: {
fontSize: 13,
color: '#6B7280',
lineHeight: 18,
marginLeft: 8,
flex: 1,
},
newBottomContainer: {
paddingHorizontal: 20,
paddingBottom: 34,
},
newContinueButton: {
borderRadius: 25,
overflow: 'hidden',
marginBottom: 8,
},
newButtonBackground: {
backgroundColor: Colors.light.accentGreen,
paddingVertical: 18,
alignItems: 'center',
justifyContent: 'center',
},
newButtonText: {
fontSize: 18,
fontWeight: '700',
color: '#192126',
},
newHomeIndicator: {
width: 134,
height: 5,
backgroundColor: '#000',
borderRadius: 3,
alignSelf: 'center',
},
});

View File

@@ -1,27 +1,27 @@
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StressAnalysisModal } from './StressAnalysisModal';
interface StressMeterProps {
value: number | null;
updateTime?: Date;
style?: any;
onPress?: () => void;
hrvValue: number;
}
export function StressMeter({ value, updateTime, style, onPress }: StressMeterProps) {
export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterProps) {
// 将HRV值转换为压力指数0-100
// HRV值范围30-110ms映射到压力指数100-0
// HRV值越高压力越小HRV值越低压力越大
const stressIndex = value ? Math.round(Math.min(100, Math.max(0, 100 - ((value - 30) / 80) * 100))) : null;
// 根据压力指数计算状态
const getStressStatus = () => {
if (stressIndex === null) {
if (value === null) {
return '未知';
} else if (stressIndex <= 30) {
} else if (value <= 30) {
return '放松';
} else if (stressIndex <= 70) {
} else if (value <= 70) {
return '正常';
} else {
return '紧张';
@@ -47,55 +47,71 @@ export function StressMeter({ value, updateTime, style, onPress }: StressMeterPr
// 计算进度条位置0-100%
// 压力指数越高,进度条越满
const progressPercentage = stressIndex === null ? 0 : stressIndex;
const progressPercentage = value === null ? 0 : value;
// 在组件内部添加状态
const [showStressModal, setShowStressModal] = useState(false);
// 修改 onPress 处理函数
const handlePress = () => {
setShowStressModal(true);
};
return (
<TouchableOpacity
style={[styles.container, style]}
onPress={onPress}
activeOpacity={0.8}
>
{/* 头部区域 */}
<View style={styles.header}>
<View style={styles.leftSection}>
<View style={styles.iconContainer}>
<Ionicons name="heart" size={16} color="#3B82F6" />
<>
<TouchableOpacity
style={[styles.container, style]}
onPress={handlePress}
activeOpacity={0.8}
>
{/* 头部区域 */}
<View style={styles.header}>
<View style={styles.leftSection}>
<View style={styles.iconContainer}>
<Ionicons name="heart" size={16} color="#3B82F6" />
</View>
<Text style={styles.title}></Text>
</View>
<Text style={styles.title}></Text>
<Text style={styles.emoji}>{getStatusEmoji()}</Text>
</View>
<Text style={styles.emoji}>{getStatusEmoji()}</Text>
</View>
{/* 数值显示区域 */}
<View style={styles.valueSection}>
<Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
<Text style={styles.unit}></Text>
</View>
{/* 数值显示区域 */}
<View style={styles.valueSection}>
<Text style={styles.value}>{value === null ? '--' : value}</Text>
<Text style={styles.unit}></Text>
</View>
{/* 进度条区域 */}
<View style={styles.progressContainer}>
<View style={styles.progressTrack}>
{/* 渐变背景进度条 */}
<View style={[styles.progressBar, { width: `${progressPercentage}%` }]}>
<LinearGradient
colors={['#10B981', '#FCD34D', '#F97316']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.gradientBar}
/>
{/* 进度条区域 */}
<View style={styles.progressContainer}>
<View style={styles.progressTrack}>
{/* 渐变背景进度条 */}
<View style={[styles.progressBar, { width: `${progressPercentage}%` }]}>
<LinearGradient
colors={['#10B981', '#FCD34D', '#F97316']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.gradientBar}
/>
</View>
{/* 白色圆形指示器 */}
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} />
</View>
{/* 白色圆形指示器 */}
<View style={[styles.indicator, { left: `${Math.max(0, Math.min(100, progressPercentage - 2))}%` }]} />
</View>
</View>
{/* 更新时间
{updateTime && (
<Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text>
)} */}
</TouchableOpacity>
{/* 更新时间
{updateTime && (
<Text style={styles.updateTime}>{formatUpdateTime(updateTime)}</Text>
)} */}
</TouchableOpacity>
{/* 压力分析浮窗 */}
<StressAnalysisModal
visible={showStressModal}
onClose={() => setShowStressModal(false)}
hrvValue={hrvValue}
updateTime={updateTime || new Date()}
/>
</>
);
}

View File

@@ -10,7 +10,10 @@ const PERMISSIONS: HealthKitPermissions = {
AppleHealthKit.Constants.Permissions.SleepAnalysis,
AppleHealthKit.Constants.Permissions.HeartRateVariability,
],
write: [],
write: [
// 支持体重写入
AppleHealthKit.Constants.Permissions.Weight,
],
},
};
@@ -192,4 +195,20 @@ export async function fetchHRVForDate(date: Date): Promise<number | null> {
// 新增获取今日HRV数据
export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(new Date());
}
}
// 更新healthkit中的体重
export async function updateWeight(weight: number) {
return new Promise((resolve) => {
AppleHealthKit.saveWeight({
value: weight,
}, (err, res) => {
if (err) {
console.error('更新体重失败:', err);
return resolve(false);
}
console.log('体重更新成功:', res);
resolve(true);
});
});
}