Files
digital-pilates/components/ProgressBar.tsx
richarjiang e3e2f1b8c6 feat: 优化 AI 教练聊天和打卡功能
- 在 AI 教练聊天界面中添加会话缓存功能,支持冷启动时恢复聊天记录
- 实现轻量防抖机制,确保会话变动时及时保存缓存
- 在打卡功能中集成按月加载打卡记录,提升用户体验
- 更新 Redux 状态管理,支持打卡记录的按月加载和缓存
- 新增打卡日历页面,允许用户查看每日打卡记录
- 优化样式以适应新功能的展示和交互
2025-08-14 09:57:13 +08:00

107 lines
2.8 KiB
TypeScript

import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Animated, Easing, LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native';
type ProgressBarProps = {
progress: number; // 0 - 1
height?: number;
style?: ViewStyle;
trackColor?: string;
fillColor?: string;
animated?: boolean;
showLabel?: boolean;
};
function ProgressBarImpl({
progress,
height = 18,
style,
trackColor = '#EDEDED',
fillColor = '#BBF246',
animated = true,
showLabel = true,
}: ProgressBarProps) {
const [trackWidth, setTrackWidth] = useState(0);
const animatedValue = useRef(new Animated.Value(0)).current;
const clamped = useMemo(() => {
if (Number.isNaN(progress)) return 0;
return Math.min(1, Math.max(0, progress));
}, [progress]);
useEffect(() => {
if (!animated) {
animatedValue.setValue(clamped);
return;
}
Animated.timing(animatedValue, {
toValue: clamped,
duration: 650,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
}, [clamped, animated, animatedValue]);
const lastWidthRef = useRef<number>(0);
const onLayout = (e: LayoutChangeEvent) => {
const w = e.nativeEvent.layout.width || 0;
// 仅在宽度发生明显变化时才更新,避免渲染-布局循环
const next = Math.round(w);
if (next > 0 && next !== lastWidthRef.current) {
lastWidthRef.current = next;
setTrackWidth(next);
}
};
const fillWidth = Animated.multiply(animatedValue, trackWidth || 1);
const percent = Math.round(clamped * 100);
return (
<View style={[styles.container, { height }, style]} onLayout={onLayout}>
<View style={[styles.track, { backgroundColor: trackColor }]} />
<Animated.View style={[styles.fill, { width: fillWidth, backgroundColor: fillColor }]}>
{showLabel && (
<Text style={styles.label}>{percent}%</Text>
)}
</Animated.View>
</View>
);
}
export const ProgressBar = memo(ProgressBarImpl, (prev, next) => {
// 仅在这些关键属性变化时重新渲染,忽略 style 的引用变化
return (
prev.progress === next.progress &&
prev.height === next.height &&
prev.trackColor === next.trackColor &&
prev.fillColor === next.fillColor &&
prev.animated === next.animated &&
prev.showLabel === next.showLabel
);
});
const styles = StyleSheet.create({
container: {
width: '100%',
borderRadius: 10,
overflow: 'hidden',
backgroundColor: 'transparent',
},
track: {
...StyleSheet.absoluteFillObject,
borderRadius: 10,
},
fill: {
height: '100%',
borderRadius: 10,
justifyContent: 'center',
paddingHorizontal: 10,
},
label: {
color: '#192126',
fontWeight: '700',
fontSize: 12,
},
});