feat(challenges): 登录态守卫与进度条动画优化
- 在 _layout 中仅当已登录时才拉取挑战列表,避免未授权请求 - 挑战详情页加入 ensureLoggedIn 守卫,未登录时跳转登录 - ChallengeProgressCard 新增分段进度动画,提升视觉反馈 - 升级版本号至 1.0.15
This commit is contained in:
@@ -14,18 +14,19 @@ import { setupQuickActions } from '@/services/quickActions';
|
||||
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
|
||||
import { WaterRecordSource } from '@/services/waterRecords';
|
||||
import { store } from '@/store';
|
||||
import { fetchChallenges } from '@/store/challengesSlice';
|
||||
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||
import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||
import { ToastProvider } from '@/contexts/ToastContext';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { STORAGE_KEYS } from '@/services/api';
|
||||
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||
import { fetchChallenges } from '@/store/challengesSlice';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
@@ -34,10 +35,17 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { profile } = useAppSelector((state) => state.user);
|
||||
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
||||
const { isLoggedIn } = useAuthGuard()
|
||||
|
||||
// 初始化快捷动作处理
|
||||
useQuickActions();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
dispatch(fetchChallenges());
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
// 数据已经在启动界面预加载,这里只需要快速同步到 Redux 状态
|
||||
@@ -128,7 +136,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
loadUserData();
|
||||
initHealthPermissions();
|
||||
initializeNotifications();
|
||||
dispatch(fetchChallenges());
|
||||
|
||||
|
||||
// 冷启动时清空 AI 教练会话缓存
|
||||
clearAiCoachSessionCache();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import {
|
||||
fetchChallengeDetail,
|
||||
@@ -15,12 +16,12 @@ import {
|
||||
selectJoinStatus,
|
||||
selectLeaveError,
|
||||
selectLeaveStatus,
|
||||
selectProgressError,
|
||||
selectProgressStatus,
|
||||
selectProgressStatus
|
||||
} from '@/store/challengesSlice';
|
||||
import { Toast } from '@/utils/toast.utils';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import LottieView from 'lottie-react-native';
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Dimensions,
|
||||
Image,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Share,
|
||||
@@ -82,6 +82,8 @@ export default function ChallengeDetailScreen() {
|
||||
const colorTokens = Colors[theme];
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
|
||||
const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]);
|
||||
const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined));
|
||||
|
||||
@@ -103,8 +105,6 @@ export default function ChallengeDetailScreen() {
|
||||
|
||||
const progressStatusSelector = useMemo(() => (id ? selectProgressStatus(id) : undefined), [id]);
|
||||
const progressStatus = useAppSelector((state) => (progressStatusSelector ? progressStatusSelector(state) : 'idle'));
|
||||
const progressErrorSelector = useMemo(() => (id ? selectProgressError(id) : undefined), [id]);
|
||||
const progressError = useAppSelector((state) => (progressErrorSelector ? progressErrorSelector(state) : undefined));
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async (id: string) => {
|
||||
@@ -174,6 +174,13 @@ export default function ChallengeDetailScreen() {
|
||||
if (!id || joinStatus === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
// 如果未登录,用户会被重定向到登录页面
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await dispatch(joinChallenge(id));
|
||||
setShowCelebration(true)
|
||||
@@ -307,7 +314,7 @@ export default function ChallengeDetailScreen() {
|
||||
]}
|
||||
>
|
||||
<View style={styles.heroContainer}>
|
||||
<Image source={{ uri: challenge.image }} style={styles.heroImage} resizeMode="cover" />
|
||||
<Image source={{ uri: challenge.image }} style={styles.heroImage} cachePolicy={'memory-disk'} />
|
||||
<LinearGradient
|
||||
colors={['rgba(0,0,0,0.35)', 'rgba(0,0,0,0.15)', 'rgba(244, 246, 255, 1)']}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
@@ -369,6 +376,7 @@ export default function ChallengeDetailScreen() {
|
||||
key={`${avatar}-${index}`}
|
||||
source={{ uri: avatar }}
|
||||
style={[styles.avatar, index > 0 && styles.avatarOffset]}
|
||||
cachePolicy={'memory-disk'}
|
||||
/>
|
||||
))}
|
||||
{challenge.participantsCount && challenge.participantsCount > participantAvatars.length ? (
|
||||
@@ -401,7 +409,7 @@ export default function ChallengeDetailScreen() {
|
||||
<Text style={styles.rankingOrder}>{index + 1}</Text>
|
||||
</View>
|
||||
{item.avatar ? (
|
||||
<Image source={{ uri: item.avatar }} style={styles.rankingAvatar} />
|
||||
<Image source={{ uri: item.avatar }} style={styles.rankingAvatar} cachePolicy={'memory-disk'} />
|
||||
) : (
|
||||
<View style={styles.rankingAvatarPlaceholder}>
|
||||
<Ionicons name="person-outline" size={20} color="#6f7ba7" />
|
||||
|
||||
Reference in New Issue
Block a user