Files
digital-pilates/components/StepsCard.tsx
2025-09-02 15:50:35 +08:00

218 lines
5.7 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, { useEffect, useMemo, useRef } from 'react';
import {
Animated,
StyleSheet,
Text,
View,
ViewStyle
} from 'react-native';
import { HourlyStepData } from '@/utils/health';
import { AnimatedNumber } from './AnimatedNumber';
// 使用原生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 (
<View key={`bar-container-${index}`} style={styles.barContainer}>
{/* 背景柱体 - 始终显示,使用相似色系的淡色 */}
<View
style={[
styles.chartBar,
{
height: 20, // 背景柱体占满整个高度
backgroundColor: isCurrent ? '#FFF4E6' : '#FFF8F0', // 更淡的相似色系
}
]}
/>
{/* 数据柱体 - 只有当有数据时才显示并执行动画 */}
{isActive && (
<Animated.View
style={[
styles.chartBar,
{
height: data.height,
backgroundColor: isCurrent ? '#FFC365' : '#FFEBCB',
transform: [{ scaleY: animatedScale }],
opacity: animatedOpacity,
}
]}
/>
)}
</View>
);
})}
</View>
</View>
</View>
{/* 步数和目标显示 */}
<View style={styles.statsContainer}>
<AnimatedNumber
value={stepCount || 0}
style={styles.stepCount}
format={(v) => stepCount !== null ? `${Math.round(v)}` : '——'}
resetToken={stepCount}
/>
</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,
},
barContainer: {
width: 4,
height: 20,
alignItems: 'center',
justifyContent: 'flex-end',
position: 'relative',
},
chartBar: {
width: 4,
borderRadius: 1,
position: 'absolute',
bottom: 0,
},
statsContainer: {
alignItems: 'flex-start',
marginTop: 6
},
stepCount: {
fontSize: 18,
fontWeight: '600',
color: '#192126',
},
});
export default StepsCard;