feat: 支持步数卡片; 优化数据分析各类卡片样式

This commit is contained in:
richarjiang
2025-08-30 17:07:04 +08:00
parent 465d5350f3
commit 741688065d
9 changed files with 462 additions and 103 deletions

View File

@@ -15,8 +15,6 @@ export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardPr
return (
<TouchableOpacity onPress={onPress} style={styles.moodCardContent} disabled={isLoading}>
<Text style={styles.cardTitle}></Text>
{isLoading ? (
<View style={styles.moodPreview}>
<Text style={styles.moodLoadingText}>...</Text>
@@ -52,7 +50,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 22,
marginTop: 12,
},
moodPreviewText: {
fontSize: 14,

152
components/StepsCard.tsx Normal file
View File

@@ -0,0 +1,152 @@
import React, { useMemo } from 'react';
import {
StyleSheet,
Text,
View,
ViewStyle
} 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 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();
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;
return (
<View
key={`bar-${index}`}
style={[
styles.chartBar,
{
height: data.height || 2, // 最小高度2px
backgroundColor: isCurrent && isActive ? '#FFC365' : '#FFEBCB',
}
]}
/>
);
})}
</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: 16,
fontWeight: '700',
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;

View File

@@ -154,10 +154,11 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
value: {
fontSize: 24,
fontWeight: '800',
fontSize: 20,
fontWeight: '600',
color: '#192126',
lineHeight: 32,
lineHeight: 20,
marginTop: 2,
},
unit: {
fontSize: 12,
@@ -166,10 +167,10 @@ const styles = StyleSheet.create({
marginLeft: 4,
},
progressContainer: {
height: 16,
height: 6,
},
progressTrack: {
height: 8,
height: 6,
borderRadius: 4,
position: 'relative',
overflow: 'visible',
@@ -185,9 +186,9 @@ const styles = StyleSheet.create({
},
indicator: {
position: 'absolute',
top: -4,
width: 16,
height: 16,
top: -2,
width: 10,
height: 10,
borderRadius: 8,
backgroundColor: '#FFFFFF',
shadowColor: '#000',

View File

@@ -34,7 +34,6 @@ const HealthDataCard: React.FC<HealthDataCardProps> = ({
const styles = StyleSheet.create({
card: {
borderRadius: 16,
shadowColor: '#000',
paddingHorizontal: 16,
shadowOffset: {
@@ -48,11 +47,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
},
iconContainer: {
marginRight: 16,
alignItems: 'center',
justifyContent: 'center',
},
content: {
flex: 1,
justifyContent: 'center',
@@ -60,7 +54,7 @@ const styles = StyleSheet.create({
title: {
fontSize: 14,
color: '#192126',
marginBottom: 4,
marginBottom: 14,
fontWeight: '800',
},
valueContainer: {
@@ -68,8 +62,8 @@ const styles = StyleSheet.create({
alignItems: 'flex-end',
},
value: {
fontSize: 24,
fontWeight: '800',
fontSize: 20,
fontWeight: '600',
color: '#192126',
},
unit: {

View File

@@ -1,4 +1,3 @@
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { StyleSheet } from 'react-native';
import HealthDataCard from './HealthDataCard';
@@ -14,10 +13,6 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
style,
oxygenSaturation
}) => {
const oxygenIcon = (
<Ionicons name="water" size={24} color="#3B82F6" />
);
return (
<HealthDataCard
title="血氧饱和度"