feat: 更新健康数据功能和用户个人信息页面
- 在 Explore 页面中添加日期选择功能,允许用户查看指定日期的健康数据 - 重构健康数据获取逻辑,支持根据日期获取健康数据 - 在个人信息页面中集成用户资料编辑功能,支持姓名、性别、年龄、体重和身高的输入 - 新增 AnimatedNumber 和 CircularRing 组件,优化数据展示效果 - 更新 package.json 和 package-lock.json,添加 react-native-svg 依赖 - 修改布局以支持新功能的显示和交互
This commit is contained in:
121
components/CircularRing.tsx
Normal file
121
components/CircularRing.tsx
Normal 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user