- 在统计页面中移除压力分析模态框及相关状态管理,简化组件逻辑 - 更新BMI卡片,改进弹窗展示方式,增加渐变背景和健康建议 - 新增更新体重的功能,支持将体重数据写入健康数据中 - 优化压力计组件,调整数据展示逻辑,提升用户体验
722 lines
20 KiB
TypeScript
722 lines
20 KiB
TypeScript
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||
import { BMICard } from '@/components/BMICard';
|
||
import { CircularRing } from '@/components/CircularRing';
|
||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||
import { ProgressBar } from '@/components/ProgressBar';
|
||
import { StressMeter } from '@/components/StressMeter';
|
||
import { WeightHistoryCard } from '@/components/WeightHistoryCard';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||
import { useAppSelector } from '@/hooks/redux';
|
||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
|
||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||
import { useFocusEffect } from '@react-navigation/native';
|
||
import dayjs from 'dayjs';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
SafeAreaView,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
|
||
export default function ExploreScreen() {
|
||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||
const colorTokens = Colors[theme];
|
||
|
||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||
const userProfile = useAppSelector((s) => s.user.profile);
|
||
|
||
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||
|
||
// 使用 dayjs:当月日期与默认选中“今天”
|
||
const days = getMonthDaysZh();
|
||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||
const tabBarHeight = useBottomTabBarHeight();
|
||
const insets = useSafeAreaInsets();
|
||
const bottomPadding = useMemo(() => {
|
||
return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0);
|
||
}, [tabBarHeight, insets?.bottom]);
|
||
|
||
const monthTitle = getMonthTitleZh();
|
||
|
||
// 日期条自动滚动到选中项
|
||
const daysScrollRef = useRef<import('react-native').ScrollView | null>(null);
|
||
const [scrollWidth, setScrollWidth] = useState(0);
|
||
const DAY_PILL_WIDTH = 68;
|
||
const DAY_PILL_SPACING = 12;
|
||
|
||
const scrollToIndex = (index: number, animated = true) => {
|
||
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING);
|
||
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
|
||
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (scrollWidth > 0) {
|
||
scrollToIndex(selectedIndex, false);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [scrollWidth]);
|
||
|
||
// HealthKit: 每次页面聚焦都拉取今日数据
|
||
const [stepCount, setStepCount] = useState<number | null>(null);
|
||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||
// 睡眠时长(分钟)
|
||
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||
// HRV数据
|
||
const [hrvValue, setHrvValue] = useState<number>(0);
|
||
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date());
|
||
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||
const [animToken, setAnimToken] = useState(0);
|
||
const [trainingProgress, setTrainingProgress] = useState(0); // 暂定静态80%
|
||
|
||
// 营养数据状态
|
||
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
|
||
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
|
||
|
||
// 记录最近一次请求的“日期键”,避免旧请求覆盖新结果
|
||
const latestRequestKeyRef = useRef<string | null>(null);
|
||
|
||
const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`;
|
||
|
||
|
||
const loadHealthData = async (targetDate?: Date) => {
|
||
try {
|
||
console.log('=== 开始HealthKit初始化流程 ===');
|
||
setIsLoading(true);
|
||
|
||
const ok = await ensureHealthPermissions();
|
||
if (!ok) {
|
||
const errorMsg = '无法获取健康权限,请确保在真实iOS设备上运行并授权应用访问健康数据';
|
||
console.warn(errorMsg);
|
||
return;
|
||
}
|
||
|
||
// 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期
|
||
let derivedDate: Date;
|
||
if (targetDate) {
|
||
derivedDate = targetDate;
|
||
} else {
|
||
derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date();
|
||
}
|
||
|
||
const requestKey = getDateKey(derivedDate);
|
||
latestRequestKeyRef.current = requestKey;
|
||
|
||
console.log('权限获取成功,开始获取健康数据...', derivedDate);
|
||
const data = await fetchHealthDataForDate(derivedDate);
|
||
|
||
console.log('设置UI状态:', data);
|
||
// 仅当该请求仍是最新时,才应用结果
|
||
if (latestRequestKeyRef.current === requestKey) {
|
||
setStepCount(data.steps);
|
||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||
setSleepDuration(data.sleepDuration);
|
||
|
||
const hrv = data.hrv ?? 0;
|
||
setHrvValue(hrv);
|
||
|
||
// 更新HRV数据时间
|
||
setHrvUpdateTime(new Date());
|
||
|
||
setAnimToken((t) => t + 1);
|
||
} else {
|
||
console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current);
|
||
}
|
||
console.log('=== HealthKit数据获取完成 ===');
|
||
|
||
} catch (error) {
|
||
console.error('HealthKit流程出现异常:', error);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 加载营养数据
|
||
const loadNutritionData = async (targetDate?: Date) => {
|
||
try {
|
||
setIsNutritionLoading(true);
|
||
|
||
// 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期
|
||
let derivedDate: Date;
|
||
if (targetDate) {
|
||
derivedDate = targetDate;
|
||
} else {
|
||
derivedDate = days[selectedIndex]?.date?.toDate() ?? new Date();
|
||
}
|
||
|
||
console.log('加载营养数据...', derivedDate);
|
||
const data = await getDietRecords({
|
||
startDate: dayjs(derivedDate).startOf('day').toISOString(),
|
||
endDate: dayjs(derivedDate).endOf('day').toISOString(),
|
||
});
|
||
|
||
if (data.records.length > 0) {
|
||
const summary = calculateNutritionSummary(data.records);
|
||
setNutritionSummary(summary);
|
||
} else {
|
||
setNutritionSummary(null);
|
||
}
|
||
console.log('营养数据加载完成:', data);
|
||
|
||
} catch (error) {
|
||
console.error('营养数据加载失败:', error);
|
||
setNutritionSummary(null);
|
||
} finally {
|
||
setIsNutritionLoading(false);
|
||
}
|
||
};
|
||
|
||
useFocusEffect(
|
||
React.useCallback(() => {
|
||
// 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致
|
||
const currentDate = days[selectedIndex]?.date?.toDate();
|
||
if (currentDate) {
|
||
loadHealthData(currentDate);
|
||
if (isLoggedIn) {
|
||
loadNutritionData(currentDate);
|
||
}
|
||
}
|
||
}, [selectedIndex])
|
||
);
|
||
|
||
// 日期点击时,加载对应日期数据
|
||
const onSelectDate = (index: number) => {
|
||
setSelectedIndex(index);
|
||
scrollToIndex(index);
|
||
const target = days[index]?.date?.toDate();
|
||
if (target) {
|
||
loadHealthData(target);
|
||
if (isLoggedIn) {
|
||
loadNutritionData(target);
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
// 使用统一的渐变背景色
|
||
const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<LinearGradient
|
||
colors={backgroundGradientColors}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
<SafeAreaView style={styles.safeArea}>
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={{ paddingBottom: bottomPadding }}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* 体重历史记录卡片 */}
|
||
<Text style={styles.sectionTitle}>健康数据</Text>
|
||
<WeightHistoryCard />
|
||
|
||
{/* 标题与日期选择 */}
|
||
<Text style={styles.monthTitle}>{monthTitle}</Text>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.daysContainer}
|
||
ref={daysScrollRef}
|
||
onLayout={(e) => setScrollWidth(e.nativeEvent.layout.width)}
|
||
>
|
||
{days.map((d, i) => {
|
||
const selected = i === selectedIndex;
|
||
return (
|
||
<View key={`${d.dayOfMonth}`} style={styles.dayItemWrapper}>
|
||
<TouchableOpacity
|
||
style={[styles.dayPill, selected ? styles.dayPillSelected : styles.dayPillNormal]}
|
||
onPress={() => onSelectDate(i)}
|
||
activeOpacity={0.8}
|
||
>
|
||
<Text style={[styles.dayLabel, selected && styles.dayLabelSelected]}> {d.weekdayZh} </Text>
|
||
<Text style={[styles.dayDate, selected && styles.dayDateSelected]}>{d.dayOfMonth}</Text>
|
||
</TouchableOpacity>
|
||
{selected && <View style={styles.selectedDot} />}
|
||
</View>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
|
||
{/* 营养摄入雷达图卡片 */}
|
||
<NutritionRadarCard
|
||
nutritionSummary={nutritionSummary}
|
||
/>
|
||
|
||
{/* 真正瀑布流布局 */}
|
||
<View style={styles.masonryContainer}>
|
||
{/* 左列 */}
|
||
<View style={styles.masonryColumn}>
|
||
<StressMeter
|
||
value={hrvValue}
|
||
updateTime={hrvUpdateTime}
|
||
style={styles.masonryCard}
|
||
hrvValue={hrvValue}
|
||
/>
|
||
|
||
<View style={[styles.masonryCard, styles.caloriesCard]}>
|
||
<Text style={styles.cardTitleSecondary}>消耗卡路里</Text>
|
||
{activeCalories != null ? (
|
||
<AnimatedNumber
|
||
value={activeCalories}
|
||
resetToken={animToken}
|
||
style={styles.caloriesValue}
|
||
format={(v) => `${Math.round(v)} 千卡`}
|
||
/>
|
||
) : (
|
||
<Text style={styles.caloriesValue}>——</Text>
|
||
)}
|
||
</View>
|
||
|
||
<View style={[styles.masonryCard, styles.stepsCard]}>
|
||
<View style={styles.cardHeaderRow}>
|
||
<View style={styles.iconSquare}>
|
||
<Ionicons name="footsteps-outline" size={18} color="#192126" />
|
||
</View>
|
||
<Text style={styles.cardTitle}>步数</Text>
|
||
</View>
|
||
{stepCount != null ? (
|
||
<AnimatedNumber
|
||
value={stepCount}
|
||
resetToken={animToken}
|
||
style={styles.stepsValue}
|
||
format={(v) => `${Math.round(v)}/${stepGoal}`}
|
||
/>
|
||
) : (
|
||
<Text style={styles.stepsValue}>——/{stepGoal}</Text>
|
||
)}
|
||
<ProgressBar
|
||
progress={Math.min(1, Math.max(0, (stepCount ?? 0) / stepGoal))}
|
||
height={16}
|
||
trackColor="#FFEBCB"
|
||
fillColor="#FFC365"
|
||
showLabel={false}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 右列 */}
|
||
<View style={styles.masonryColumn}>
|
||
<BMICard
|
||
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
|
||
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
|
||
style={styles.masonryCardNoBg}
|
||
// compact={true}
|
||
/>
|
||
|
||
<View style={[styles.masonryCard, styles.trainingCard]}>
|
||
<Text style={styles.cardTitleSecondary}>训练时间</Text>
|
||
<View style={styles.trainingContent}>
|
||
<CircularRing
|
||
size={120}
|
||
strokeWidth={12}
|
||
trackColor="#E2D9FD"
|
||
progressColor="#8B74F3"
|
||
progress={trainingProgress}
|
||
resetToken={animToken}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={[styles.masonryCard, styles.sleepCard]}>
|
||
<View style={styles.cardHeaderRow}>
|
||
<View style={styles.iconSquare}>
|
||
<Ionicons name="moon-outline" size={18} color="#192126" />
|
||
</View>
|
||
<Text style={styles.cardTitle}>睡眠</Text>
|
||
</View>
|
||
{sleepDuration != null ? (
|
||
<Text style={styles.sleepValue}>
|
||
{Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
|
||
</Text>
|
||
) : (
|
||
<Text style={styles.sleepValue}>——</Text>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
|
||
|
||
|
||
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const primary = Colors.light.primary;
|
||
const lightColors = Colors.light;
|
||
const darkColors = Colors.dark;
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
safeArea: {
|
||
flex: 1,
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
paddingHorizontal: 20,
|
||
},
|
||
monthTitle: {
|
||
fontSize: 24,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginTop: 8,
|
||
marginBottom: 14,
|
||
},
|
||
daysContainer: {
|
||
paddingBottom: 8,
|
||
},
|
||
dayItemWrapper: {
|
||
alignItems: 'center',
|
||
width: 68,
|
||
marginRight: 12,
|
||
},
|
||
dayPill: {
|
||
width: 68,
|
||
height: 68,
|
||
borderRadius: 18,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
dayPillNormal: {
|
||
backgroundColor: lightColors.datePickerNormal,
|
||
},
|
||
dayPillSelected: {
|
||
backgroundColor: lightColors.datePickerSelected,
|
||
},
|
||
dayLabel: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 2,
|
||
},
|
||
dayLabelSelected: {
|
||
color: '#FFFFFF',
|
||
},
|
||
dayDate: {
|
||
fontSize: 16,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
},
|
||
dayDateSelected: {
|
||
color: '#FFFFFF',
|
||
},
|
||
selectedDot: {
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
backgroundColor: lightColors.datePickerSelected,
|
||
marginTop: 10,
|
||
marginBottom: 4,
|
||
alignSelf: 'center',
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 24,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
marginTop: 24,
|
||
marginBottom: 14,
|
||
},
|
||
metricsRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 16,
|
||
},
|
||
card: {
|
||
backgroundColor: '#0F1418',
|
||
borderRadius: 22,
|
||
padding: 18,
|
||
marginBottom: 16,
|
||
},
|
||
metricsLeft: {
|
||
flex: 1,
|
||
backgroundColor: '#EEE9FF',
|
||
borderRadius: 22,
|
||
padding: 18,
|
||
marginRight: 12,
|
||
},
|
||
metricsRight: {
|
||
width: 160,
|
||
gap: 12,
|
||
},
|
||
metricsRightCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 22,
|
||
padding: 16,
|
||
},
|
||
caloriesCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
},
|
||
trainingCard: {
|
||
backgroundColor: '#EEE9FF',
|
||
},
|
||
|
||
cardTitleSecondary: {
|
||
color: '#9AA3AE',
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
marginBottom: 10,
|
||
},
|
||
caloriesValue: {
|
||
color: '#192126',
|
||
fontSize: 22,
|
||
fontWeight: '800',
|
||
},
|
||
trainingContent: {
|
||
marginTop: 8,
|
||
width: 120,
|
||
height: 120,
|
||
borderRadius: 60,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
alignSelf: 'center',
|
||
},
|
||
trainingRingTrack: {
|
||
position: 'absolute',
|
||
width: '100%',
|
||
height: '100%',
|
||
borderRadius: 60,
|
||
borderWidth: 12,
|
||
borderColor: '#E2D9FD',
|
||
},
|
||
trainingRingProgress: {
|
||
position: 'absolute',
|
||
width: '100%',
|
||
height: '100%',
|
||
borderRadius: 60,
|
||
borderWidth: 12,
|
||
borderColor: 'transparent',
|
||
borderTopColor: '#8B74F3',
|
||
borderRightColor: '#8B74F3',
|
||
transform: [{ rotateZ: '45deg' }],
|
||
},
|
||
trainingPercent: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
color: '#8B74F3',
|
||
},
|
||
cyclingHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
},
|
||
cyclingIconBadge: {
|
||
width: 30,
|
||
height: 30,
|
||
borderRadius: 6,
|
||
backgroundColor: primary,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 8,
|
||
},
|
||
cyclingTitle: {
|
||
color: '#FFFFFF',
|
||
fontSize: 20,
|
||
fontWeight: '800',
|
||
},
|
||
mapArea: {
|
||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||
borderRadius: 14,
|
||
height: 180,
|
||
padding: 8,
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
overflow: 'hidden',
|
||
},
|
||
mapTile: {
|
||
width: '25%',
|
||
height: '25%',
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(255,255,255,0.12)',
|
||
},
|
||
routeLine: {
|
||
position: 'absolute',
|
||
height: 6,
|
||
backgroundColor: primary,
|
||
borderRadius: 3,
|
||
},
|
||
cardHeaderRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
},
|
||
iconSquare: {
|
||
width: 30,
|
||
height: 30,
|
||
borderRadius: 8,
|
||
backgroundColor: '#FFFFFF',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 10,
|
||
},
|
||
cardTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
},
|
||
heartCard: {
|
||
backgroundColor: '#FFE5E5',
|
||
},
|
||
waveContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-end',
|
||
height: 70,
|
||
gap: 6,
|
||
marginBottom: 8,
|
||
},
|
||
waveBar: {
|
||
width: 6,
|
||
borderRadius: 3,
|
||
backgroundColor: '#E54D4D',
|
||
},
|
||
heartValue: {
|
||
alignSelf: 'flex-end',
|
||
color: '#5B5B5B',
|
||
fontWeight: '600',
|
||
},
|
||
stepsCard: {
|
||
backgroundColor: '#FFE4B8',
|
||
},
|
||
stepsValue: {
|
||
fontSize: 16,
|
||
color: '#7A6A42',
|
||
fontWeight: '700',
|
||
marginBottom: 8,
|
||
},
|
||
errorContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: '#FFE5E5',
|
||
borderRadius: 12,
|
||
padding: 12,
|
||
marginBottom: 16,
|
||
},
|
||
errorText: {
|
||
fontSize: 14,
|
||
color: '#E54D4D',
|
||
fontWeight: '600',
|
||
marginLeft: 8,
|
||
flex: 1,
|
||
},
|
||
retryButton: {
|
||
padding: 4,
|
||
marginLeft: 8,
|
||
},
|
||
viewMoreContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
viewMoreText: {
|
||
fontSize: 14,
|
||
color: '#192126',
|
||
},
|
||
viewMoreIcon: {
|
||
fontSize: 16,
|
||
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,
|
||
},
|
||
});
|