feat: 新增健康档案模块,支持家庭邀请与个人健康数据管理

This commit is contained in:
richarjiang
2025-12-04 17:56:04 +08:00
parent e713ffbace
commit a254af92c7
28 changed files with 4177 additions and 315 deletions

View File

@@ -9,6 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -20,18 +21,26 @@ import {
TouchableOpacity,
View
} from 'react-native';
import Svg, { Circle, Path } from 'react-native-svg';
import Svg, { Circle, Defs, Path, Stop, LinearGradient as SvgLinearGradient } from 'react-native-svg';
import { WeightProgressBar } from './WeightProgressBar';
const { width: screenWidth } = Dimensions.get('window');
const CARD_WIDTH = screenWidth - 40; // Subtract left and right margins
const CHART_WIDTH = CARD_WIDTH - 36; // Subtract card padding
const CHART_HEIGHT = 60;
const CARD_WIDTH = screenWidth - 40;
const CHART_WIDTH = CARD_WIDTH - 36;
const CHART_HEIGHT = 70;
const PADDING = 10;
// 主题色
const THEME_PRIMARY = '#4F5BD5';
const THEME_SECONDARY = '#6B6CFF';
const THEME_SUCCESS = '#22C55E';
const THEME_TEXT_PRIMARY = '#1c1f3a';
const THEME_TEXT_SECONDARY = '#6f7ba7';
export function WeightHistoryCard() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const router = useRouter();
const userProfile = useAppSelector((s) => s.user.profile);
const weightHistory = useAppSelector((s) => s.user.weightHistory);
@@ -44,7 +53,6 @@ export function WeightHistoryCard() {
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
useEffect(() => {
if (isLoggedIn) {
loadWeightHistory();
@@ -59,7 +67,8 @@ export function WeightHistoryCard() {
}
};
const navigateToCoach = () => {
// 点击添加按钮 - 需要登录
const handleAddWeight = () => {
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
};
@@ -67,85 +76,97 @@ export function WeightHistoryCard() {
setShowBMIModal(false);
};
// 点击卡片 - 直接跳转,不需要登录
const navigateToWeightRecords = () => {
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
router.push(ROUTES.WEIGHT_RECORDS);
};
// Process weight history data
const sortedHistory = [...weightHistory]
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.slice(-7); // Show only the last 7 records
.slice(-7);
// return (
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
// <View style={styles.cardHeader}>
// <Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
// </View>
// 是否有数据
const hasData = sortedHistory.length > 0;
// <View style={styles.emptyContent}>
// <Text style={styles.emptyDescription}>
// No weight records yet, click the button below to start recording
// </Text>
// <TouchableOpacity
// style={styles.recordButton}
// onPress={(e) => {
// e.stopPropagation();
// navigateToCoach();
// }}
// activeOpacity={0.8}
// >
// <Ionicons name="add" size={18} color="#FFFFFF" />
// <Text style={styles.recordButtonText}>{t('statistics.components.weight.addButton')}</Text>
// </TouchableOpacity>
// </View>
// </TouchableOpacity>
// );
// }
// 计算减重进度
const currentWeight = userProfile?.weight ? parseFloat(userProfile.weight) : 0;
const initialWeight = userProfile?.initialWeight
? parseFloat(userProfile.initialWeight)
: (sortedHistory.length > 0 ? parseFloat(sortedHistory[0].weight) : 0);
const targetWeight = userProfile?.targetWeight ? parseFloat(userProfile.targetWeight) : 0;
// 计算进度百分比
const hasTargetWeight = targetWeight > 0 && initialWeight > targetWeight;
const totalToLose = initialWeight - targetWeight;
const actualLost = initialWeight - currentWeight;
const weightProgress = hasTargetWeight && totalToLose > 0 ? actualLost / totalToLose : 0;
// Generate chart data
const weights = sortedHistory.map(item => parseFloat(item.weight));
const minWeight = Math.min(...weights);
const maxWeight = Math.max(...weights);
const weights = hasData ? sortedHistory.map(item => parseFloat(item.weight)) : [];
const minWeight = hasData ? Math.min(...weights) : 0;
const maxWeight = hasData ? Math.max(...weights) : 0;
const weightRange = maxWeight - minWeight || 1;
const points = sortedHistory.map((item, index) => {
const points = hasData ? sortedHistory.map((item, index) => {
const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING);
const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange;
// Reduce top margin, compress whitespace
const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16);
const y = PADDING + 10 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 20);
return { x, y, weight: item.weight, date: item.createdAt };
});
}) : [];
// Generate path
const pathData = points.map((point, index) => {
if (index === 0) return `M ${point.x} ${point.y}`;
return `L ${point.x} ${point.y}`;
}).join(' ');
// 生成平滑曲线路径(使用贝塞尔曲线)
const generateSmoothPath = (pts: typeof points) => {
if (pts.length === 0) return '';
if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
// If there's only one data point, display as a horizontal line
const singlePointPath = points.length === 1 ?
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
pathData;
let path = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[Math.max(0, i - 1)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(pts.length - 1, i + 2)];
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;
}
return path;
};
const smoothPath = generateSmoothPath(points);
const singlePointPath = points.length === 1
? `M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}`
: smoothPath;
// 空状态下的占位曲线路径(水平虚线效果)
const emptyLinePath = `M ${PADDING} ${CHART_HEIGHT / 2} L ${CHART_WIDTH - PADDING} ${CHART_HEIGHT / 2}`;
return (
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}>
<Image
source={require('@/assets/images/icons/icon-weight.png')}
style={styles.iconSquare}
/>
<View style={styles.iconContainer}>
<Image
source={require('@/assets/images/icons/icon-weight.png')}
style={styles.iconSquare}
/>
</View>
<Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
{isLgAvaliable ? (
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
navigateToCoach();
handleAddWeight();
}}
activeOpacity={0.8}
>
<GlassView style={styles.addButtonGlass}>
<Ionicons name="add" size={18} color={Colors.light.primary} />
<Ionicons name="add" size={18} color={THEME_PRIMARY} />
</GlassView>
</TouchableOpacity>
) : (
@@ -153,68 +174,125 @@ export function WeightHistoryCard() {
style={styles.addButton}
onPress={(e) => {
e.stopPropagation();
navigateToCoach();
handleAddWeight();
}}
activeOpacity={0.8}
>
<Ionicons name="add" size={18} color={Colors.light.primary} />
<Ionicons name="add" size={18} color={THEME_PRIMARY} />
</TouchableOpacity>
)}
</View>
{/* Default chart display */}
{sortedHistory.length > 0 && (
<View style={styles.chartContainer}>
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
{/* Background grid lines */}
{/* 当前体重显示 */}
<View style={styles.currentWeightSection}>
<View style={styles.weightValueContainer}>
<Text style={styles.weightValue}>{hasWeight ? currentWeight.toFixed(1) : '--'}</Text>
<Text style={styles.weightUnit}>kg</Text>
</View>
{sortedHistory.length > 1 && (
<View style={[
styles.changeTag,
{ backgroundColor: actualLost >= 0 ? 'rgba(34, 197, 94, 0.1)' : 'rgba(255, 107, 107, 0.1)' }
]}>
<Ionicons
name={actualLost >= 0 ? 'trending-down' : 'trending-up'}
size={12}
color={actualLost >= 0 ? THEME_SUCCESS : '#FF6B6B'}
/>
<Text style={[
styles.changeText,
{ color: actualLost >= 0 ? THEME_SUCCESS : '#FF6B6B' }
]}>
{actualLost >= 0 ? '-' : '+'}{Math.abs(actualLost).toFixed(1)}kg
</Text>
</View>
)}
</View>
{/* More abstract line - reduce line width and display details */}
{/* 图表显示 */}
<View style={styles.chartContainer}>
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
<Defs>
<SvgLinearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<Stop offset="0%" stopColor={THEME_PRIMARY} stopOpacity="1" />
<Stop offset="100%" stopColor={THEME_SECONDARY} stopOpacity="1" />
</SvgLinearGradient>
</Defs>
{hasData ? (
<>
{/* 平滑曲线 */}
<Path
d={singlePointPath}
stroke="url(#lineGradient)"
strokeWidth={2.5}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* 数据点 */}
{points.map((point, index) => {
const isLastPoint = index === points.length - 1;
return (
<React.Fragment key={index}>
{/* 外圈光晕 */}
{isLastPoint && (
<Circle
cx={point.x}
cy={point.y}
r={8}
fill={THEME_PRIMARY}
opacity={0.15}
/>
)}
{/* 数据点 */}
<Circle
cx={point.x}
cy={point.y}
r={isLastPoint ? 4 : 2.5}
fill={isLastPoint ? THEME_PRIMARY : THEME_SECONDARY}
stroke={isLastPoint ? '#ffffff' : 'none'}
strokeWidth={isLastPoint ? 2 : 0}
/>
</React.Fragment>
);
})}
</>
) : (
/* 空状态 - 虚线占位 */
<Path
d={singlePointPath}
stroke={Colors.light.accentGreen}
d={emptyLinePath}
stroke="#E8EAF0"
strokeWidth={2}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
opacity={0.8}
strokeDasharray="8,6"
/>
)}
</Svg>
{/* Simplified data points - smaller and more refined */}
{points.map((point, index) => {
const isLastPoint = index === points.length - 1;
return (
<React.Fragment key={index}>
<Circle
cx={point.x}
cy={point.y}
r={isLastPoint ? 3 : 2}
fill={Colors.light.accentGreen}
opacity={0.9}
/>
</React.Fragment>
);
})}
</Svg>
{/* Concise chart information */}
<View style={styles.chartInfo}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>{sortedHistory.length}{t('statistics.components.weight.days')}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
</Text>
</View>
{/* 图表信息 */}
<View style={styles.chartInfo}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>{hasData ? sortedHistory.length : '--'}{t('statistics.components.weight.days')}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>
{hasData ? `${minWeight.toFixed(1)}-${maxWeight.toFixed(1)}kg` : '--'}
</Text>
</View>
</View>
)}
</View>
{/* 减重进度条 - 始终显示 */}
<WeightProgressBar
progress={weightProgress}
currentWeight={currentWeight}
targetWeight={targetWeight}
initialWeight={initialWeight}
/>
{/* BMI information modal */}
<Modal
@@ -323,32 +401,38 @@ export function WeightHistoryCard() {
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 22,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
marginTop: 16
borderRadius: 24,
padding: 18,
shadowColor: 'rgba(30, 41, 59, 0.08)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12,
shadowRadius: 12,
elevation: 4,
marginTop: 16,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
},
iconSquare: {
width: 14,
height: 14,
iconContainer: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: 'rgba(79, 91, 213, 0.1)',
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
marginRight: 10,
},
iconSquare: {
width: 16,
height: 16,
tintColor: THEME_PRIMARY,
},
cardTitle: {
fontSize: 14,
color: '#192126',
fontSize: 15,
color: THEME_TEXT_PRIMARY,
flex: 1,
fontWeight: '600',
fontWeight: '700',
fontFamily: 'AliBold',
},
headerButtons: {
@@ -364,19 +448,56 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
addButton: {
width: 28,
height: 28,
borderRadius: 14,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(79, 91, 213, 0.1)',
},
addButtonGlass: {
width: 28,
height: 28,
borderRadius: 14,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(147, 112, 219, 0.3)',
backgroundColor: 'rgba(79, 91, 213, 0.15)',
},
currentWeightSection: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 12,
gap: 12,
},
weightValueContainer: {
flexDirection: 'row',
alignItems: 'baseline',
},
weightValue: {
fontSize: 32,
fontWeight: '800',
color: THEME_TEXT_PRIMARY,
fontFamily: 'AliBold',
},
weightUnit: {
fontSize: 14,
fontWeight: '600',
color: THEME_TEXT_SECONDARY,
fontFamily: 'AliRegular',
marginLeft: 4,
},
changeTag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
gap: 4,
},
changeText: {
fontSize: 12,
fontWeight: '700',
fontFamily: 'AliBold',
},
emptyContent: {
alignItems: 'center',
@@ -384,12 +505,12 @@ const styles = StyleSheet.create({
emptyTitle: {
fontSize: 16,
fontWeight: '700',
color: '#192126',
color: THEME_TEXT_PRIMARY,
marginBottom: 6,
},
emptyDescription: {
fontSize: 14,
color: '#687076',
color: THEME_TEXT_SECONDARY,
textAlign: 'center',
marginBottom: 16,
lineHeight: 20,
@@ -397,14 +518,14 @@ const styles = StyleSheet.create({
recordButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.light.accentGreen,
backgroundColor: THEME_PRIMARY,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
gap: 6,
},
recordButtonText: {
color: '#192126',
color: '#FFFFFF',
fontSize: 14,
fontWeight: '700',
fontFamily: 'AliBold',
@@ -418,20 +539,25 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
marginTop: -14,
},
infoItem: {
alignItems: 'center',
backgroundColor: 'rgba(79, 91, 213, 0.06)',
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 10,
},
infoLabel: {
fontSize: 11,
color: '#687076',
color: THEME_TEXT_SECONDARY,
fontWeight: '500',
fontFamily: 'AliRegular',
},
infoValue: {
fontSize: 14,
fontWeight: '700',
color: '#192126',
color: THEME_TEXT_PRIMARY,
},
// BMI modal styles
@@ -556,7 +682,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
bmiModalButtonBackground: {
backgroundColor: '#192126',
backgroundColor: THEME_TEXT_PRIMARY,
borderRadius: 16,
paddingVertical: 16,
alignItems: 'center',

View File

@@ -0,0 +1,278 @@
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Animated,
Easing,
StyleSheet,
Text,
View,
ViewStyle
} from 'react-native';
// 主题色
const THEME_PRIMARY = '#4F5BD5';
const THEME_SECONDARY = '#6B6CFF';
const THEME_SUCCESS = '#22C55E';
const THEME_TEXT_SECONDARY = '#6f7ba7';
export interface WeightProgressBarProps {
/** 进度值 0-1 */
progress: number;
/** 当前体重 */
currentWeight: number;
/** 目标体重 */
targetWeight: number;
/** 初始体重 */
initialWeight: number;
/** 容器样式 */
style?: ViewStyle;
/** 是否显示顶部分隔线,默认 true */
showTopBorder?: boolean;
}
export const WeightProgressBar: React.FC<WeightProgressBarProps> = ({
progress,
currentWeight,
targetWeight,
initialWeight,
style,
showTopBorder = true,
}) => {
const { t } = useTranslation();
const animatedProgress = useRef(new Animated.Value(0)).current;
const [barWidth, setBarWidth] = useState(0);
const clampedProgress = Math.min(1, Math.max(0, progress));
const percent = Math.round(clampedProgress * 100);
// 判断是否有有效数据
const hasInitialWeight = initialWeight > 0;
const hasTargetWeight = targetWeight > 0;
const hasCurrentWeight = currentWeight > 0;
// 只要有初始体重和当前体重,就可以显示已减重量
const canShowLost = hasInitialWeight && hasCurrentWeight;
// 需要有目标体重才能显示距离目标和进度
const canShowTarget = hasTargetWeight && hasCurrentWeight;
useEffect(() => {
// 延迟 500ms 开始动画,避免页面刚进入时卡顿
const timer = setTimeout(() => {
Animated.timing(animatedProgress, {
toValue: clampedProgress,
duration: 800,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
}, 800);
return () => clearTimeout(timer);
}, [clampedProgress]);
const fillWidth = animatedProgress.interpolate({
inputRange: [0, 1],
outputRange: [0, barWidth],
});
const sliderPosition = animatedProgress.interpolate({
inputRange: [0, 1],
outputRange: [-12, barWidth - 12],
});
const weightLost = initialWeight - currentWeight;
const weightToGo = currentWeight - targetWeight;
return (
<View style={[
styles.container,
showTopBorder && styles.topBorder,
style
]}>
{/* 进度信息 */}
<View style={styles.infoRow}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>{t('statistics.components.weight.progress.lost')}</Text>
<Text style={[styles.infoValue, { color: canShowLost && weightLost >= 0 ? THEME_SUCCESS : (canShowLost ? '#FF6B6B' : THEME_TEXT_SECONDARY) }]}>
{canShowLost ? `${weightLost >= 0 ? '-' : '+'}${Math.abs(weightLost).toFixed(1)}kg` : '--'}
</Text>
</View>
<View style={styles.percentContainer}>
<Text style={styles.percentValue}>{percent}</Text>
<Text style={styles.percentSymbol}>%</Text>
</View>
<View style={[styles.infoItem, { alignItems: 'flex-end' }]}>
<Text style={styles.infoLabel}>{t('statistics.components.weight.progress.toGo')}</Text>
<Text style={[styles.infoValue, { color: THEME_PRIMARY }]}>
{canShowTarget ? `${weightToGo > 0 ? weightToGo.toFixed(1) : '0'}kg` : '--'}
</Text>
</View>
</View>
{/* 进度条 */}
<View
style={styles.trackContainer}
onLayout={(e) => setBarWidth(e.nativeEvent.layout.width)}
>
{/* 背景轨道 */}
<View style={styles.track} />
{/* 填充进度 */}
<Animated.View style={[styles.fill, { width: fillWidth }]}>
<LinearGradient
colors={[THEME_PRIMARY, THEME_SECONDARY]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={StyleSheet.absoluteFillObject}
/>
</Animated.View>
{/* 滑块 - 圆角矩形 */}
<Animated.View style={[styles.slider, { left: sliderPosition }]}>
<LinearGradient
colors={['#ffffff', '#f8f9fc']}
style={styles.sliderInner}
>
<View style={styles.sliderLine} />
</LinearGradient>
</Animated.View>
</View>
{/* 起止标签 */}
<View style={styles.labelRow}>
<Text style={styles.labelText}>{hasInitialWeight ? `${initialWeight.toFixed(1)}kg` : '--'}</Text>
<View style={styles.targetBadge}>
<Ionicons name="flag" size={10} color={THEME_PRIMARY} />
<Text style={styles.targetText}>{hasTargetWeight ? `${targetWeight.toFixed(1)}kg` : '--'}</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 12,
paddingTop: 10,
marginLeft:12,
marginRight: 12
},
topBorder: {
borderTopWidth: 1,
borderTopColor: 'rgba(0,0,0,0.04)',
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
infoItem: {
flex: 1,
},
infoLabel: {
fontSize: 11,
color: THEME_TEXT_SECONDARY,
fontFamily: 'AliRegular',
marginBottom: 2,
},
infoValue: {
fontSize: 14,
fontWeight: '700',
fontFamily: 'AliBold',
},
percentContainer: {
flexDirection: 'row',
alignItems: 'baseline',
justifyContent: 'center',
},
percentValue: {
fontSize: 24,
fontWeight: '800',
color: THEME_PRIMARY,
fontFamily: 'AliBold',
},
percentSymbol: {
fontSize: 12,
fontWeight: '600',
color: THEME_PRIMARY,
fontFamily: 'AliBold',
marginLeft: 2,
},
trackContainer: {
height: 8,
position: 'relative',
marginBottom: 8,
},
track: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#E8EAF0',
borderRadius: 4,
},
fill: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
borderRadius: 4,
overflow: 'hidden',
},
slider: {
position: 'absolute',
top: -8,
width: 24,
height: 24,
borderRadius: 8,
shadowColor: THEME_PRIMARY,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.35,
shadowRadius: 6,
elevation: 6,
},
sliderInner: {
width: '100%',
height: '100%',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2.5,
borderColor: THEME_PRIMARY,
},
sliderLine: {
width: 8,
height: 3,
borderRadius: 1.5,
backgroundColor: THEME_PRIMARY,
},
labelRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
labelText: {
fontSize: 11,
color: THEME_TEXT_SECONDARY,
fontFamily: 'AliRegular',
},
targetBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(79, 91, 213, 0.1)',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
gap: 4,
},
targetText: {
fontSize: 11,
color: THEME_PRIMARY,
fontWeight: '600',
fontFamily: 'AliBold',
},
});
export default WeightProgressBar;