- 在 AI 教练聊天界面中添加会话缓存功能,支持冷启动时恢复聊天记录 - 实现轻量防抖机制,确保会话变动时及时保存缓存 - 在打卡功能中集成按月加载打卡记录,提升用户体验 - 更新 Redux 状态管理,支持打卡记录的按月加载和缓存 - 新增打卡日历页面,允许用户查看每日打卡记录 - 优化样式以适应新功能的展示和交互
107 lines
2.8 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
|
|
|