diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 4ad167f..d3743bc 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -73,8 +73,8 @@ export default function ExploreScreen() { // 睡眠时长(分钟) const [sleepDuration, setSleepDuration] = useState(null); // HRV数据 - const [hrvValue, setHrvValue] = useState(69); - const [hrvStatus, setHrvStatus] = useState<'放松' | '正常' | '紧张'>('正常'); + const [hrvValue, setHrvValue] = useState(0); + const [hrvUpdateTime, setHrvUpdateTime] = useState(new Date()); const [isLoading, setIsLoading] = useState(false); // 用于触发动画重置的 token(当日期或数据变化时更新) @@ -103,8 +103,14 @@ export default function ExploreScreen() { return; } - // 若未显式传入日期,按当前选中索引推导日期 - const derivedDate = targetDate ?? days[selectedIndex]?.date?.toDate() ?? new Date(); + // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 + let derivedDate: Date; + if (targetDate) { + derivedDate = targetDate; + } else { + derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date(); + } + const requestKey = getDateKey(derivedDate); latestRequestKeyRef.current = requestKey; @@ -118,18 +124,11 @@ export default function ExploreScreen() { setActiveCalories(Math.round(data.activeEnergyBurned)); setSleepDuration(data.sleepDuration); - // 模拟HRV数据(实际应用中应从HealthKit获取) - const simulatedHrv = Math.floor(Math.random() * 80) + 30; // 30-110ms范围 - setHrvValue(simulatedHrv); + const hrv = data.hrv ?? 0; + setHrvValue(hrv); - // 根据HRV值判断状态 - if (simulatedHrv >= 70) { - setHrvStatus('放松'); - } else if (simulatedHrv >= 40) { - setHrvStatus('正常'); - } else { - setHrvStatus('紧张'); - } + // 更新HRV数据时间 + setHrvUpdateTime(new Date()); setAnimToken((t) => t + 1); } else { @@ -149,8 +148,13 @@ export default function ExploreScreen() { try { setIsNutritionLoading(true); - // 若未显式传入日期,按当前选中索引推导日期 - const derivedDate = targetDate ?? days[selectedIndex]?.date?.toDate() ?? new Date(); + // 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期 + let derivedDate: Date; + if (targetDate) { + derivedDate = targetDate; + } else { + derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date(); + } console.log('加载营养数据...', derivedDate); const data = await getDietRecords({ @@ -177,11 +181,14 @@ export default function ExploreScreen() { useFocusEffect( React.useCallback(() => { // 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致 - loadHealthData(); - if (isLoggedIn) { - loadNutritionData(); + const currentDate = days[selectedIndex]?.date?.toDate(); + if (currentDate) { + loadHealthData(currentDate); + if (isLoggedIn) { + loadNutritionData(currentDate); + } } - }, []) + }, [selectedIndex]) ); // 日期点击时,加载对应日期数据 @@ -210,15 +217,6 @@ export default function ExploreScreen() { 健康数据 - {/* HRV压力监测卡片 */} - - - {/* 查看更多 */} - - 查看更多 - - - {/* 标题与日期选择 */} {monthTitle} - {/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */} - - - 训练时间 - - - - - - + {/* 真正瀑布流布局 */} + + {/* 左列 */} + + + + 消耗卡路里 {activeCalories != null ? ( —— )} - + + - + + + 步数 {stepCount != null ? ( @@ -298,20 +293,56 @@ export default function ExploreScreen() { )} + + {/* 右列 */} + + + + + 训练时间 + + + + + + + + + + + 睡眠 + + {sleepDuration != null ? ( + + {Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟 + + ) : ( + —— + )} + + - {/* BMI 指数卡片 */} - + @@ -596,4 +627,77 @@ const styles = StyleSheet.create({ color: '#192126', marginLeft: 4, }, + stressCardRow: { + flexDirection: 'row', + justifyContent: 'flex-start', + marginBottom: 16, + }, + healthCardsRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 16, + }, + compactBMICard: { + width: 140, + minHeight: 110, + }, + healthMetricsContainer: { + marginBottom: 16, + }, + masonryContainer: { + marginBottom: 16, + flexDirection: 'row', + justifyContent: 'space-between', + }, + masonryColumn: { + flex: 1, + marginHorizontal: 3, + }, + masonryCard: { + width: '100%', + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 16, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 3, + marginBottom: 8, + }, + masonryCardNoBg: { + width: '100%', + borderRadius: 16, + padding: 16, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 3, + marginBottom: 8, + }, + compactStepsCard: { + minHeight: 100, + }, + stepsContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 8, + }, + sleepCard: { + backgroundColor: '#E8F4FD', + }, + sleepValue: { + fontSize: 16, + color: '#1E40AF', + fontWeight: '700', + marginTop: 8, + }, }); diff --git a/assets/images/react-logo.png b/assets/images/react-logo.png deleted file mode 100644 index 9d72a9f..0000000 Binary files a/assets/images/react-logo.png and /dev/null differ diff --git a/assets/images/react-logo@2x.png b/assets/images/react-logo@2x.png deleted file mode 100644 index 2229b13..0000000 Binary files a/assets/images/react-logo@2x.png and /dev/null differ diff --git a/assets/images/react-logo@3x.png b/assets/images/react-logo@3x.png deleted file mode 100644 index a99b203..0000000 Binary files a/assets/images/react-logo@3x.png and /dev/null differ diff --git a/components/BMICard.tsx b/components/BMICard.tsx index 20ea5cf..980ce05 100644 --- a/components/BMICard.tsx +++ b/components/BMICard.tsx @@ -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 ( - + - + - BMI 指数 + BMI + {!compact && ( + + + + )} + + + {compact ? ( + + + {!weight && !height ? '完善身高体重' : + !weight ? '完善体重' : '完善身高'} + + + ) : ( + <> + + + + {!weight && !height ? '请完善身高和体重信息' : + !weight ? '请完善体重信息' : '请完善身高信息'} + + + + + 前往完善 + + + + )} + + ); + } + + // 有完整数据的情况 + return ( + + + + + + + BMI + + {!compact && ( - + )} + - - - - {!weight && !height ? '请完善身高和体重信息' : - !weight ? '请完善体重信息' : '请完善身高信息'} + {compact ? ( + + + {bmiResult?.value} - - - - 前往完善 - - - - ); - } - - // 有完整数据的情况 - return ( - - - - - - - BMI 指数 - - - - - - - - - {bmiResult?.value} - - - + {bmiResult?.category.name} - + ) : ( + <> + + + {bmiResult?.value} + + + + {bmiResult?.category.name} + + + - - {bmiResult?.description} - + + {bmiResult?.description} + - - {bmiResult?.category.encouragement} - - + + {bmiResult?.category.encouragement} + + + )} + ); }; @@ -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, diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index db5396d..4033d32 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -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 - - {isLoading ? ( - - 加载中... + + + - ) : ( - - - - - - {nutritionStats.map((stat, index) => ( - - - {stat.label} - {stat.value} - - ))} - + + {nutritionStats.map((stat, index) => ( + + + {stat.label} + {stat.value} + + ))} - )} + ); } @@ -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', - }, }); diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index 818895f..b4e7541 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -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 ( - + + {/* 渐变背景 */} + + {/* 头部区域 */} - + 压力 @@ -55,18 +81,21 @@ export function StressMeter({ value, status }: StressMeterProps) { {/* 渐变背景进度条 */} - - - + {/* 白色圆形指示器 */} - + + + {/* 更新时间 */} + {updateTime && ( + {formatUpdateTime(updateTime)} + )} ); } @@ -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, + }, }); \ No newline at end of file diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index 4658dfe..11a5b95 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -449,7 +449,10 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -504,7 +507,10 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = false; diff --git a/ios/digitalpilates/SplashScreen.storyboard b/ios/digitalpilates/SplashScreen.storyboard index 158767f..47a97b2 100644 --- a/ios/digitalpilates/SplashScreen.storyboard +++ b/ios/digitalpilates/SplashScreen.storyboard @@ -1,14 +1,15 @@ - + - + + @@ -16,27 +17,27 @@ - - + + - - - - + + + + - + - + - + - \ No newline at end of file + diff --git a/utils/health.ts b/utils/health.ts index a1fe3f5..37555a5 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import type { HealthKitPermissions } from 'react-native-health'; import AppleHealthKit from 'react-native-health'; @@ -45,10 +46,8 @@ export async function ensureHealthPermissions(): Promise { export async function fetchHealthDataForDate(date: Date): Promise { console.log('开始获取指定日期健康数据...', date); - const start = new Date(date); - start.setHours(0, 0, 0, 0); - const end = new Date(date); - end.setHours(23, 59, 59, 999); + const start = dayjs(date).startOf('day').toDate(); + const end = dayjs(date).endOf('day').toDate(); const options = { startDate: start.toISOString(),