Files
digital-pilates/components/StepsCard.tsx

191 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useMemo, useRef, useEffect } from 'react';
import {
StyleSheet,
Text,
View,
ViewStyle,
Animated
} from 'react-native';
import { HourlyStepData } from '@/utils/health';
// 使用原生View来替代SVG避免导入问题
// import Svg, { Rect } from 'react-native-svg';
interface StepsCardProps {
stepCount: number | null;
stepGoal: number;
hourlySteps: HourlyStepData[];
style?: ViewStyle;
}
const StepsCard: React.FC<StepsCardProps> = ({
stepCount,
stepGoal,
hourlySteps,
style
}) => {
// 为每个柱体创建独立的动画值
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 = 20; // 柱状图最大高度(缩小一半)
return hourlySteps.map(data => ({
...data,
height: maxSteps > 0 ? (data.steps / maxSteps) * maxHeight : 0
}));
}, [hourlySteps]);
// 获取当前小时
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) {
Animated.spring(animatedValues[index], {
toValue: 1,
tension: 150,
friction: 8,
useNativeDriver: false,
}).start();
}
});
}
}, [chartData, animatedValues]);
return (
<View style={[styles.container, style]}>
{/* 标题和步数显示 */}
<View style={styles.header}>
<Text style={styles.title}></Text>
</View>
{/* 柱状图 */}
<View style={styles.chartContainer}>
<View style={styles.chartWrapper}>
<View style={styles.chartArea}>
{chartData.map((data, index) => {
// 判断是否是当前小时或者有活动的小时
const isActive = data.steps > 0;
const isCurrent = index <= currentHour;
// 动画变换缩放从0到实际高度
const animatedScale = animatedValues[index].interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
});
// 动画变换透明度从0到1
const animatedOpacity = animatedValues[index].interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
});
return (
<Animated.View
key={`bar-${index}`}
style={[
styles.chartBar,
{
height: data.height || 2, // 最小高度2px
backgroundColor: isCurrent && isActive ? '#FFC365' : '#FFEBCB',
transform: [{ scaleY: animatedScale }],
opacity: animatedOpacity,
}
]}
/>
);
})}
</View>
</View>
</View>
{/* 步数和目标显示 */}
<View style={styles.statsContainer}>
<Text style={styles.stepCount}>
{stepCount !== null ? stepCount.toLocaleString() : '——'}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
borderRadius: 20,
padding: 16,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
fontSize: 14,
color: '#192126',
},
footprintIcons: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
chartContainer: {
flex: 1,
justifyContent: 'center',
},
chartWrapper: {
width: '100%',
alignItems: 'center',
},
chartArea: {
flexDirection: 'row',
alignItems: 'flex-end',
height: 20,
width: '100%',
maxWidth: 240,
justifyContent: 'space-between',
paddingHorizontal: 4,
},
chartBar: {
width: 4,
borderRadius: 1,
alignSelf: 'flex-end',
},
statsContainer: {
alignItems: 'flex-start',
marginTop: 6
},
stepCount: {
fontSize: 18,
fontWeight: '600',
color: '#192126',
},
});
export default StepsCard;