feat: 添加日历功能和进度条组件
- 在项目中引入 dayjs 库以处理日期 - 新增 PlanCard 和 ProgressBar 组件,分别用于展示训练计划和进度条 - 更新首页以显示推荐的训练计划 - 优化个人中心页面的底部留白处理 - 本地化界面文本为中文
This commit is contained in:
92
components/PlanCard.tsx
Normal file
92
components/PlanCard.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ProgressBar } from '@/components/ProgressBar';
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
type Level = '初学者' | '中级' | '高级';
|
||||
|
||||
type PlanCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
level: Level;
|
||||
progress: number; // 0 - 1
|
||||
};
|
||||
|
||||
export function PlanCard({ image, title, subtitle, level, progress }: PlanCardProps) {
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Image source={{ uri: image }} style={styles.image} />
|
||||
<View style={styles.content}>
|
||||
<View style={styles.badgeContainer}>
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{level}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||
|
||||
<View style={styles.progressWrapper}>
|
||||
<ProgressBar progress={progress} showLabel={true} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 28,
|
||||
padding: 20,
|
||||
marginBottom: 18,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
image: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 22,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingLeft: 18,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: -10,
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: '#192126',
|
||||
borderTopRightRadius: 12,
|
||||
borderBottomLeftRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
badgeText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
color: '#192126',
|
||||
fontWeight: '800',
|
||||
},
|
||||
subtitle: {
|
||||
marginTop: 8,
|
||||
fontSize: 12,
|
||||
color: '#B1B6BD',
|
||||
},
|
||||
progressWrapper: {
|
||||
marginTop: 18,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
87
components/ProgressBar.tsx
Normal file
87
components/ProgressBar.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Animated, Easing, LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||
|
||||
type ProgressBarProps = {
|
||||
progress: number; // 0 - 1
|
||||
height?: number;
|
||||
style?: ViewStyle;
|
||||
trackColor?: string;
|
||||
fillColor?: string;
|
||||
animated?: boolean;
|
||||
showLabel?: boolean;
|
||||
};
|
||||
|
||||
export function ProgressBar({
|
||||
progress,
|
||||
height = 18,
|
||||
style,
|
||||
trackColor = '#EDEDED',
|
||||
fillColor = '#BBF246',
|
||||
animated = true,
|
||||
showLabel = true,
|
||||
}: ProgressBarProps) {
|
||||
const [trackWidth, setTrackWidth] = useState(0);
|
||||
const animatedValue = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const clamped = useMemo(() => {
|
||||
if (Number.isNaN(progress)) return 0;
|
||||
return Math.min(1, Math.max(0, progress));
|
||||
}, [progress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!animated) {
|
||||
animatedValue.setValue(clamped);
|
||||
return;
|
||||
}
|
||||
Animated.timing(animatedValue, {
|
||||
toValue: clamped,
|
||||
duration: 650,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [clamped, animated, animatedValue]);
|
||||
|
||||
const onLayout = (e: LayoutChangeEvent) => {
|
||||
setTrackWidth(e.nativeEvent.layout.width);
|
||||
};
|
||||
|
||||
const fillWidth = Animated.multiply(animatedValue, trackWidth || 1);
|
||||
const percent = Math.round(clamped * 100);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { height }, style]} onLayout={onLayout}>
|
||||
<View style={[styles.track, { backgroundColor: trackColor }]} />
|
||||
<Animated.View style={[styles.fill, { width: fillWidth, backgroundColor: fillColor }]}>
|
||||
{showLabel && (
|
||||
<Text style={styles.label}>{percent}%</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
track: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
borderRadius: 10,
|
||||
},
|
||||
fill: {
|
||||
height: '100%',
|
||||
borderRadius: 10,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
label: {
|
||||
color: '#192126',
|
||||
fontWeight: '700',
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react
|
||||
|
||||
interface WorkoutCardProps {
|
||||
title: string;
|
||||
calories: number;
|
||||
duration: number;
|
||||
calories?: number;
|
||||
duration?: number;
|
||||
imageSource: any;
|
||||
onPress?: () => void;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function WorkoutCard({ title, calories, duration, imageSource, onPress }:
|
||||
return (
|
||||
<TouchableOpacity style={styles.container} onPress={onPress}>
|
||||
<ImageBackground
|
||||
source={imageSource}
|
||||
source={{ uri: imageSource }}
|
||||
style={styles.backgroundImage}
|
||||
imageStyle={styles.imageStyle}
|
||||
>
|
||||
@@ -31,10 +31,12 @@ export function WorkoutCard({ title, calories, duration, imageSource, onPress }:
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="time-outline" size={16} color="#fff" />
|
||||
<Text style={styles.statText}>{duration} Min</Text>
|
||||
</View>
|
||||
{duration !== undefined && (
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="time-outline" size={16} color="#fff" />
|
||||
<Text style={styles.statText}>{duration} Min</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user