feat: 更新健康数据功能和用户个人信息页面

- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据
- 重构健康数据获取逻辑,支持根据日期获取健康数据
- 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入
- 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果
- 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖
- 修改布局以支持新功能的显示和交互
This commit is contained in:
richarjiang
2025-08-12 18:54:15 +08:00
parent 2fac3f899c
commit 8ffebfb297
14 changed files with 1034 additions and 72 deletions

121
components/CircularRing.tsx Normal file
View File

@@ -0,0 +1,121 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
import Svg, { Circle, G } from 'react-native-svg';
type CircularRingProps = {
size?: number;
strokeWidth?: number;
trackColor?: string;
progressColor?: string;
progress: number; // 0..1
durationMs?: number;
showCenterText?: boolean;
/** 当该值变化时会从0重新动画到 progress */
resetToken?: unknown;
/** 进度起始角度(度),默认 -90使进度从正上方开始 */
startAngleDeg?: number;
};
/**
* 纯 View 实现的圆环进度条(无依赖),通过两个半圆与旋转实现。
*/
export function CircularRing({
size = 120,
strokeWidth = 12,
trackColor = '#E2D9FD',
progressColor = '#8B74F3',
progress,
durationMs = 900,
showCenterText = true,
resetToken,
startAngleDeg = -90,
}: CircularRingProps) {
const clamped = useMemo(() => {
if (Number.isNaN(progress)) return 0;
return Math.min(1, Math.max(0, progress));
}, [progress]);
const animated = useRef(new Animated.Value(0)).current;
useEffect(() => {
// 每次 resetToken 或目标进度变化时从0动画到 clamped
animated.stopAnimation(() => {
animated.setValue(0);
Animated.timing(animated, {
toValue: clamped,
duration: durationMs,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clamped, resetToken]);
const radius = useMemo(() => (size - strokeWidth) / 2, [size, strokeWidth]);
const circumference = useMemo(() => 2 * Math.PI * radius, [radius]);
const dashOffset = animated.interpolate({
inputRange: [0, 1],
outputRange: [circumference, 0],
});
const percentText = useMemo(() => `${Math.round(clamped * 100)}%`, [clamped]);
const containerStyle = useMemo(() => [styles.container, { width: size, height: size }], [size]);
return (
<View style={containerStyle}>
<Svg width={size} height={size}>
<G rotation={startAngleDeg} originX={size / 2} originY={size / 2}>
{/* 轨道 */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={trackColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="none"
/>
{/* 进度 */}
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={progressColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset as unknown as number}
strokeLinecap="round"
fill="none"
/>
</G>
</Svg>
{showCenterText && (
<View style={styles.center}>
<Text style={styles.centerText}>{percentText}</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
},
center: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
centerText: {
fontSize: 18,
fontWeight: '800',
color: '#8B74F3',
},
});
const AnimatedCircle = Animated.createAnimatedComponent(Circle);