757 lines
22 KiB
TypeScript
757 lines
22 KiB
TypeScript
import { DateSelector } from '@/components/DateSelector';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
|
||
import { getTestHealthData } from '@/utils/mockHealthData';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import dayjs from 'dayjs';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { useRouter } from 'expo-router';
|
||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Animated,
|
||
SafeAreaView,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
|
||
export default function StepsDetailScreen() {
|
||
const router = useRouter();
|
||
const dispatch = useAppDispatch();
|
||
const insets = useSafeAreaInsets();
|
||
|
||
// 开发调试:设置为true来使用mock数据
|
||
const useMockData = __DEV__;
|
||
|
||
// 日期选择相关状态
|
||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||
|
||
// 数据加载状态
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
// 获取当前选中日期
|
||
const currentSelectedDate = useMemo(() => {
|
||
const days = getMonthDaysZh();
|
||
return days[selectedIndex]?.date?.toDate() ?? new Date();
|
||
}, [selectedIndex]);
|
||
|
||
const currentSelectedDateString = useMemo(() => {
|
||
return dayjs(currentSelectedDate).format('YYYY-MM-DD');
|
||
}, [currentSelectedDate]);
|
||
|
||
// 从 Redux 获取指定日期的健康数据
|
||
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
||
|
||
// 解构健康数据(支持mock数据)
|
||
const mockData = useMockData ? getTestHealthData('mock') : null;
|
||
const stepCount: number | null = useMockData ? (mockData?.steps ?? null) : (healthData?.steps ?? null);
|
||
const hourlySteps = useMockData ? (mockData?.hourlySteps ?? []) : (healthData?.hourlySteps ?? []);
|
||
|
||
|
||
// 为每个柱体创建独立的动画值
|
||
const animatedValues = useRef(
|
||
Array.from({ length: 24 }, () => new Animated.Value(0))
|
||
).current;
|
||
|
||
// 计算柱状图数据
|
||
const chartData = useMemo(() => {
|
||
if (!hourlySteps || hourlySteps.length === 0) {
|
||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 }));
|
||
}
|
||
|
||
// 找到最大步数用于计算高度比例
|
||
const maxSteps = Math.max(...hourlySteps.map(data => data.steps), 1);
|
||
const maxHeight = 120; // 详情页面使用更大的高度
|
||
|
||
return hourlySteps.map(data => ({
|
||
...data,
|
||
height: maxSteps > 0 ? (data.steps / maxSteps) * maxHeight : 0
|
||
}));
|
||
}, [hourlySteps]);
|
||
|
||
// 计算平均值刻度线位置
|
||
const averageLinePosition = useMemo(() => {
|
||
if (!hourlySteps || hourlySteps.length === 0 || !chartData || chartData.length === 0) return 0;
|
||
|
||
const activeHours = hourlySteps.filter(h => h.steps > 0);
|
||
if (activeHours.length === 0) return 0;
|
||
|
||
const avgSteps = activeHours.reduce((sum, h) => sum + h.steps, 0) / activeHours.length;
|
||
const maxSteps = Math.max(...hourlySteps.map(data => data.steps), 1);
|
||
const maxHeight = 120;
|
||
|
||
return maxSteps > 0 ? (avgSteps / maxSteps) * maxHeight : 0;
|
||
}, [hourlySteps, chartData]);
|
||
|
||
// 获取当前小时
|
||
const currentHour = new Date().getHours();
|
||
|
||
// 触发柱体动画
|
||
useEffect(() => {
|
||
if (chartData && chartData.length > 0) {
|
||
// 重置所有动画值
|
||
animatedValues.forEach(animValue => animValue.setValue(0));
|
||
|
||
// 延迟启动动画,创建波浪效果
|
||
chartData.forEach((data, index) => {
|
||
if (data.steps > 0) {
|
||
setTimeout(() => {
|
||
Animated.spring(animatedValues[index], {
|
||
toValue: 1,
|
||
tension: 120,
|
||
friction: 8,
|
||
useNativeDriver: false,
|
||
}).start();
|
||
}, index * 50); // 每个柱体延迟50ms
|
||
}
|
||
});
|
||
}
|
||
}, [chartData, animatedValues]);
|
||
|
||
// 加载健康数据
|
||
const loadHealthData = async (targetDate: Date) => {
|
||
if (useMockData) return; // 如果使用mock数据,不需要加载
|
||
|
||
try {
|
||
setIsLoading(true);
|
||
console.log('加载步数详情数据...', targetDate);
|
||
|
||
const ok = await ensureHealthPermissions();
|
||
if (!ok) {
|
||
console.warn('无法获取健康权限');
|
||
return;
|
||
}
|
||
|
||
const data = await fetchHealthDataForDate(targetDate);
|
||
|
||
console.log('data', data);
|
||
|
||
const dateString = dayjs(targetDate).format('YYYY-MM-DD');
|
||
|
||
// 使用 Redux 存储健康数据
|
||
dispatch(setHealthData({
|
||
date: dateString,
|
||
data: data
|
||
}));
|
||
|
||
console.log('步数详情数据加载完成');
|
||
} catch (error) {
|
||
console.error('加载步数详情数据失败:', error);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 日期选择处理
|
||
const onSelectDate = (index: number, date: Date) => {
|
||
setSelectedIndex(index);
|
||
loadHealthData(date);
|
||
};
|
||
|
||
// 页面初始化时加载当前日期数据
|
||
useEffect(() => {
|
||
loadHealthData(currentSelectedDate);
|
||
}, []);
|
||
|
||
// 计算总步数和平均步数
|
||
const totalSteps = stepCount || 0;
|
||
const averageHourlySteps = useMemo(() => {
|
||
if (!hourlySteps || hourlySteps.length === 0) return 0;
|
||
const activeHours = hourlySteps.filter(h => h.steps > 0);
|
||
if (activeHours.length === 0) return 0;
|
||
return Math.round(activeHours.reduce((sum, h) => sum + h.steps, 0) / activeHours.length);
|
||
}, [hourlySteps]);
|
||
|
||
// 找出最活跃的时间段
|
||
const mostActiveHour = useMemo(() => {
|
||
if (!hourlySteps || hourlySteps.length === 0) return null;
|
||
const maxStepsData = hourlySteps.reduce((max, current) =>
|
||
current.steps > max.steps ? current : max
|
||
);
|
||
return maxStepsData.steps > 0 ? maxStepsData : null;
|
||
}, [hourlySteps]);
|
||
|
||
// 活动等级配置
|
||
const activityLevels = useMemo(() => [
|
||
{ key: 'inactive', label: '不怎么动', minSteps: 0, maxSteps: 3000, color: '#B8C8D6' },
|
||
{ key: 'light', label: '轻度活跃', minSteps: 3000, maxSteps: 7500, color: '#93C5FD' },
|
||
{ key: 'moderate', label: '中等活跃', minSteps: 7500, maxSteps: 10000, color: '#FCD34D' },
|
||
{ key: 'very_active', label: '非常活跃', minSteps: 10000, maxSteps: Infinity, color: '#FB923C' }
|
||
], []);
|
||
|
||
// 计算当前活动等级
|
||
const currentActivityLevel = useMemo(() => {
|
||
return activityLevels.find(level =>
|
||
totalSteps >= level.minSteps && totalSteps < level.maxSteps
|
||
) || activityLevels[0];
|
||
}, [totalSteps, activityLevels]);
|
||
|
||
// 计算下一等级
|
||
const nextActivityLevel = useMemo(() => {
|
||
const currentIndex = activityLevels.indexOf(currentActivityLevel);
|
||
return currentIndex < activityLevels.length - 1 ? activityLevels[currentIndex + 1] : null;
|
||
}, [currentActivityLevel, activityLevels]);
|
||
|
||
// 计算进度百分比
|
||
const progressPercentage = useMemo(() => {
|
||
if (!nextActivityLevel) return 100; // 已达到最高级
|
||
|
||
const rangeSize = nextActivityLevel.minSteps - currentActivityLevel.minSteps;
|
||
const currentProgress = totalSteps - currentActivityLevel.minSteps;
|
||
return Math.min(Math.max((currentProgress / rangeSize) * 100, 0), 100);
|
||
}, [totalSteps, currentActivityLevel, nextActivityLevel]);
|
||
|
||
// 倒序显示的活动等级(用于图例)
|
||
const reversedActivityLevels = useMemo(() => [...activityLevels].reverse(), [activityLevels]);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#F0F9FF', '#E0F2FE']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
/>
|
||
|
||
<SafeAreaView style={styles.safeArea}>
|
||
{/* 顶部导航栏 */}
|
||
<View style={styles.header}>
|
||
<TouchableOpacity
|
||
style={styles.backButton}
|
||
onPress={() => router.back()}
|
||
>
|
||
<Ionicons name="chevron-back" size={24} color="#192126" />
|
||
</TouchableOpacity>
|
||
<Text style={styles.headerTitle}>步数详情</Text>
|
||
<View style={styles.headerRight} />
|
||
</View>
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={{}}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* 日期选择器 */}
|
||
<DateSelector
|
||
selectedIndex={selectedIndex}
|
||
onDateSelect={onSelectDate}
|
||
showMonthTitle={true}
|
||
disableFutureDates={true}
|
||
/>
|
||
|
||
{/* 统计卡片 */}
|
||
<View style={styles.statsCard}>
|
||
{isLoading ? (
|
||
<View style={styles.loadingContainer}>
|
||
<Text style={styles.loadingText}>加载中...</Text>
|
||
</View>
|
||
) : (
|
||
<View style={styles.statsRow}>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{totalSteps.toLocaleString()}</Text>
|
||
<Text style={styles.statLabel}>总步数</Text>
|
||
</View>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{averageHourlySteps}</Text>
|
||
<Text style={styles.statLabel}>平均每小时</Text>
|
||
</View>
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>
|
||
{mostActiveHour ? `${mostActiveHour.hour}:00` : '--'}
|
||
</Text>
|
||
<Text style={styles.statLabel}>最活跃时段</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 详细柱状图卡片 */}
|
||
<View style={styles.chartCard}>
|
||
<View style={styles.chartHeader}>
|
||
<Text style={styles.chartTitle}>每小时步数分布</Text>
|
||
<Text style={styles.chartSubtitle}>
|
||
{dayjs(currentSelectedDate).format('YYYY年MM月DD日')}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 柱状图容器 */}
|
||
<View style={styles.chartContainer}>
|
||
{/* 平均值刻度线 - 放在chartArea外面,相对于chartContainer定位 */}
|
||
{averageLinePosition > 0 && (
|
||
<View
|
||
style={[
|
||
styles.averageLine,
|
||
{ bottom: averageLinePosition }
|
||
]}
|
||
>
|
||
<View style={styles.averageLineDashContainer}>
|
||
{/* 创建更多的虚线段来确保完整覆盖 */}
|
||
{Array.from({ length: 80 }, (_, index) => (
|
||
<View
|
||
key={index}
|
||
style={[
|
||
styles.dashSegment,
|
||
{
|
||
marginLeft: index > 0 ? 2 : 0,
|
||
flex: 0 // 防止 flex 拉伸
|
||
}
|
||
]}
|
||
/>
|
||
))}
|
||
</View>
|
||
<Text style={styles.averageLineLabel}>
|
||
平均 {averageHourlySteps}步
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* 柱状图区域 */}
|
||
<View style={styles.chartArea}>
|
||
{chartData.map((data, index) => {
|
||
const isActive = data.steps > 0;
|
||
const isCurrent = index <= currentHour;
|
||
const isKeyTime = index === 0 || index === 12 || index === 23;
|
||
|
||
// 动画变换
|
||
const animatedHeight = animatedValues[index].interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: [0, data.height],
|
||
});
|
||
|
||
const animatedOpacity = animatedValues[index].interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: [0, 1],
|
||
});
|
||
|
||
return (
|
||
<View key={`bar-${index}`} style={styles.barContainer}>
|
||
{/* 背景柱体 */}
|
||
<View
|
||
style={[
|
||
styles.backgroundBar,
|
||
{
|
||
backgroundColor: isKeyTime ? '#FFF4E6' : '#F8FAFC',
|
||
}
|
||
]}
|
||
/>
|
||
|
||
{/* 数据柱体 */}
|
||
{isActive && (
|
||
<Animated.View
|
||
style={[
|
||
styles.dataBar,
|
||
{
|
||
height: animatedHeight,
|
||
backgroundColor: isCurrent ? '#FFC365' : '#FFEBCB',
|
||
opacity: animatedOpacity,
|
||
}
|
||
]}
|
||
/>
|
||
)}
|
||
|
||
{/* 步数标签(仅在有数据且是关键时间点时显示) */}
|
||
{/* {isActive && isKeyTime && (
|
||
<Animated.View
|
||
style={[styles.stepLabel, { opacity: animatedOpacity }]}
|
||
>
|
||
<Text style={styles.stepLabelText}>{data.steps}</Text>
|
||
</Animated.View>
|
||
)} */}
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
|
||
{/* 底部时间轴标签 */}
|
||
<View style={styles.timeLabels}>
|
||
<Text style={styles.timeLabel}>0:00</Text>
|
||
<Text style={styles.timeLabel}>12:00</Text>
|
||
<Text style={styles.timeLabel}>24:00</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 活动等级展示卡片 */}
|
||
<View style={styles.activityLevelCard}>
|
||
|
||
|
||
{/* 活动级别文本 */}
|
||
<Text style={styles.activityMainText}>你今天的活动量处于</Text>
|
||
<Text style={styles.activityLevelText}>{currentActivityLevel.label}</Text>
|
||
|
||
{/* 进度条 */}
|
||
<View style={styles.progressBarContainer}>
|
||
<View style={styles.progressBarBackground}>
|
||
<View
|
||
style={[
|
||
styles.progressBarFill,
|
||
{
|
||
width: `${progressPercentage}%`,
|
||
backgroundColor: currentActivityLevel.color
|
||
}
|
||
]}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 步数信息 */}
|
||
<View style={styles.stepsInfoContainer}>
|
||
<View style={styles.currentStepsInfo}>
|
||
<Text style={styles.stepsValue}>{totalSteps.toLocaleString()} 步</Text>
|
||
<Text style={styles.stepsLabel}>当前</Text>
|
||
</View>
|
||
<View style={styles.nextStepsInfo}>
|
||
<Text style={styles.stepsValue}>
|
||
{nextActivityLevel ? `${nextActivityLevel.minSteps.toLocaleString()} 步` : '--'}
|
||
</Text>
|
||
<Text style={styles.stepsLabel}>
|
||
{nextActivityLevel ? `下一级: ${nextActivityLevel.label}` : '已达最高级'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 活动等级图例 */}
|
||
<View style={styles.activityLegendContainer}>
|
||
{reversedActivityLevels.map((level) => (
|
||
<View key={level.key} style={styles.legendItem}>
|
||
<View style={[styles.legendIcon, { backgroundColor: level.color }]}>
|
||
<Text style={styles.legendIconText}>🏃</Text>
|
||
</View>
|
||
<Text style={styles.legendLabel}>{level.label}</Text>
|
||
<Text style={styles.legendRange}>
|
||
{level.maxSteps === Infinity
|
||
? `> ${level.minSteps.toLocaleString()}`
|
||
: `${level.minSteps.toLocaleString()} - ${level.maxSteps.toLocaleString()}`}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
safeArea: {
|
||
flex: 1,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 16,
|
||
backgroundColor: 'transparent',
|
||
},
|
||
backButton: {
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
headerTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
color: '#192126',
|
||
},
|
||
headerRight: {
|
||
width: 40,
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
paddingHorizontal: 20,
|
||
},
|
||
statsCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 20,
|
||
marginVertical: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 4,
|
||
},
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
statsRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-around',
|
||
},
|
||
statItem: {
|
||
alignItems: 'center',
|
||
},
|
||
statValue: {
|
||
fontSize: 24,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
statLabel: {
|
||
fontSize: 12,
|
||
color: '#64748B',
|
||
},
|
||
loadingContainer: {
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 20,
|
||
},
|
||
loadingText: {
|
||
fontSize: 16,
|
||
color: '#64748B',
|
||
},
|
||
chartCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 20,
|
||
marginBottom: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 4,
|
||
},
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
chartHeader: {
|
||
marginBottom: 20,
|
||
},
|
||
chartTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
chartSubtitle: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
},
|
||
chartContainer: {
|
||
position: 'relative',
|
||
},
|
||
timeLabels: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginTop: 8,
|
||
paddingHorizontal: 8,
|
||
},
|
||
timeLabel: {
|
||
fontSize: 12,
|
||
color: '#64748B',
|
||
fontWeight: '500',
|
||
},
|
||
chartArea: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-end',
|
||
height: 120,
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 4,
|
||
},
|
||
barContainer: {
|
||
width: 8,
|
||
height: 120,
|
||
alignItems: 'center',
|
||
justifyContent: 'flex-end',
|
||
position: 'relative',
|
||
},
|
||
backgroundBar: {
|
||
width: 8,
|
||
height: 120,
|
||
borderRadius: 2,
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
},
|
||
dataBar: {
|
||
width: 8,
|
||
borderRadius: 2,
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
},
|
||
stepLabel: {
|
||
position: 'absolute',
|
||
top: -20,
|
||
alignItems: 'center',
|
||
},
|
||
stepLabelText: {
|
||
fontSize: 10,
|
||
color: '#64748B',
|
||
fontWeight: '500',
|
||
},
|
||
averageLine: {
|
||
position: 'absolute',
|
||
left: 4, // 匹配 chartArea 的 paddingHorizontal
|
||
right: 4, // 匹配 chartArea 的 paddingHorizontal
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
zIndex: 1,
|
||
},
|
||
averageLineDashContainer: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginRight: 8,
|
||
overflow: 'hidden', // 防止虚线段溢出容器
|
||
},
|
||
dashSegment: {
|
||
width: 3,
|
||
height: 1.5,
|
||
backgroundColor: '#FFA726',
|
||
opacity: 0.8,
|
||
},
|
||
averageLineLabel: {
|
||
fontSize: 10,
|
||
color: '#FFA726',
|
||
fontWeight: '600',
|
||
backgroundColor: '#FFFFFF',
|
||
paddingHorizontal: 6,
|
||
paddingVertical: 2,
|
||
borderRadius: 4,
|
||
borderWidth: 0.5,
|
||
borderColor: '#FFA726',
|
||
},
|
||
activityLevelCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 24,
|
||
marginVertical: 16,
|
||
alignItems: 'center',
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 4,
|
||
},
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
activityIconContainer: {
|
||
marginBottom: 16,
|
||
},
|
||
activityIcon: {
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: 40,
|
||
backgroundColor: '#E0F2FE',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderWidth: 2,
|
||
borderColor: '#93C5FD',
|
||
borderStyle: 'dashed',
|
||
},
|
||
meditationIcon: {
|
||
width: 50,
|
||
height: 50,
|
||
borderRadius: 25,
|
||
backgroundColor: '#93C5FD',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
meditationEmoji: {
|
||
fontSize: 24,
|
||
},
|
||
activityMainText: {
|
||
fontSize: 16,
|
||
color: '#64748B',
|
||
marginBottom: 4,
|
||
},
|
||
activityLevelText: {
|
||
fontSize: 24,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 20,
|
||
},
|
||
progressBarContainer: {
|
||
width: '100%',
|
||
marginBottom: 24,
|
||
},
|
||
progressBarBackground: {
|
||
width: '100%',
|
||
height: 8,
|
||
backgroundColor: '#F0F9FF',
|
||
borderRadius: 4,
|
||
overflow: 'hidden',
|
||
},
|
||
progressBarFill: {
|
||
height: '100%',
|
||
borderRadius: 4,
|
||
},
|
||
stepsInfoContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
width: '100%',
|
||
marginBottom: 32,
|
||
},
|
||
currentStepsInfo: {
|
||
alignItems: 'flex-start',
|
||
},
|
||
nextStepsInfo: {
|
||
alignItems: 'flex-end',
|
||
},
|
||
stepsValue: {
|
||
fontSize: 20,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 4,
|
||
},
|
||
stepsLabel: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
},
|
||
activityLegendContainer: {
|
||
width: '100%',
|
||
},
|
||
legendItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 16,
|
||
backgroundColor: '#F8FAFC',
|
||
marginBottom: 8,
|
||
borderRadius: 12,
|
||
},
|
||
legendIcon: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 8,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 12,
|
||
},
|
||
legendIconText: {
|
||
fontSize: 16,
|
||
},
|
||
legendLabel: {
|
||
flex: 1,
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#192126',
|
||
},
|
||
legendRange: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
fontWeight: '500',
|
||
},
|
||
}); |