feat(challenges): 登录态守卫与进度条动画优化
- 在 _layout 中仅当已登录时才拉取挑战列表,避免未授权请求 - 挑战详情页加入 ensureLoggedIn 守卫,未登录时跳转登录 - ChallengeProgressCard 新增分段进度动画,提升视觉反馈 - 升级版本号至 1.0.15
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Animated, Easing, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
||||
|
||||
import type { ChallengeProgress } from '@/store/challengesSlice';
|
||||
|
||||
@@ -60,12 +60,42 @@ export const ChallengeProgressCard: React.FC<ChallengeProgressCardProps> = ({
|
||||
inactiveColor = DEFAULT_INACTIVE_COLOR,
|
||||
}) => {
|
||||
const hasValidProgress = Boolean(progress && progress.target && progress.target > 0);
|
||||
const segmentAnimations = useRef<Animated.Value[]>([]);
|
||||
|
||||
const segments = useMemo(() => {
|
||||
if (!hasValidProgress || !progress) return undefined;
|
||||
return clampSegments(progress.target, progress.completed);
|
||||
}, [hasValidProgress, progress]);
|
||||
|
||||
if (segments) {
|
||||
if (segmentAnimations.current.length < segments.segmentsCount) {
|
||||
const additional = Array.from(
|
||||
{ length: segments.segmentsCount - segmentAnimations.current.length },
|
||||
() => new Animated.Value(0)
|
||||
);
|
||||
segmentAnimations.current = [...segmentAnimations.current, ...additional];
|
||||
} else if (segmentAnimations.current.length > segments.segmentsCount) {
|
||||
segmentAnimations.current = segmentAnimations.current.slice(0, segments.segmentsCount);
|
||||
}
|
||||
} else if (segmentAnimations.current.length) {
|
||||
segmentAnimations.current = [];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!segments) return;
|
||||
|
||||
segmentAnimations.current.forEach((animation, index) => {
|
||||
const isComplete = index < segments.completedSegments;
|
||||
Animated.timing(animation, {
|
||||
toValue: isComplete ? 1 : 0,
|
||||
duration: isComplete ? 460 : 240,
|
||||
delay: isComplete ? index * 55 : 0,
|
||||
easing: isComplete ? Easing.out(Easing.cubic) : Easing.out(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
});
|
||||
}, [segments?.completedSegments, segments?.segmentsCount]);
|
||||
|
||||
const remainingDays = useMemo(() => calculateRemainingDays(endAt), [endAt]);
|
||||
|
||||
if (!hasValidProgress || !progress || !segments) {
|
||||
@@ -93,19 +123,48 @@ export const ChallengeProgressCard: React.FC<ChallengeProgressCardProps> = ({
|
||||
|
||||
<View style={[styles.track, { backgroundColor: trackColor }]}>
|
||||
{Array.from({ length: segments.segmentsCount }).map((_, index) => {
|
||||
const isComplete = index < segments.completedSegments;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === segments.segmentsCount - 1;
|
||||
const animation = segmentAnimations.current[index];
|
||||
|
||||
if (!animation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scaleY = animation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.55, 1],
|
||||
});
|
||||
const scaleX = animation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.7, 1],
|
||||
});
|
||||
const opacity = animation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.25, 1],
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`progress-segment-${index}`}
|
||||
style={[
|
||||
styles.segment,
|
||||
{ backgroundColor: isComplete ? accentColor : inactiveColor },
|
||||
{ backgroundColor: inactiveColor },
|
||||
isFirst && styles.segmentFirst,
|
||||
isLast && styles.segmentLast,
|
||||
]}
|
||||
/>
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.segmentFill,
|
||||
{
|
||||
backgroundColor: accentColor,
|
||||
opacity,
|
||||
transform: [{ scaleX }, { scaleY }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -168,6 +227,7 @@ const styles = StyleSheet.create({
|
||||
height: 4,
|
||||
borderRadius: 4,
|
||||
marginHorizontal: 3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
segmentFirst: {
|
||||
marginLeft: 0,
|
||||
@@ -175,6 +235,10 @@ const styles = StyleSheet.create({
|
||||
segmentLast: {
|
||||
marginRight: 0,
|
||||
},
|
||||
segmentFill: {
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default ChallengeProgressCard;
|
||||
|
||||
Reference in New Issue
Block a user