214 lines
5.6 KiB
TypeScript
214 lines
5.6 KiB
TypeScript
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 (
|
||
<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}>
|
||
<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,
|
||
},
|
||
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; |