feat(challenges): 优化挑战列表与详情页交互体验

- 替换 Image 为 expo-image 并启用缓存策略
- 调整礼物按钮尺寸与图标大小
- 加入挑战失败时弹出 Toast 提示
- 统一异步流程并移除冗余状态监听
- 清理调试日志与多余空行
This commit is contained in:
richarjiang
2025-09-29 09:59:47 +08:00
parent 7259bd7a2c
commit b80af23f4f
3 changed files with 37 additions and 22 deletions

View File

@@ -9,12 +9,12 @@ import {
selectChallengesListStatus, selectChallengesListStatus,
type ChallengeCardViewModel, type ChallengeCardViewModel,
} from '@/store/challengesSlice'; } from '@/store/challengesSlice';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Image,
ScrollView, ScrollView,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
@@ -42,6 +42,9 @@ export default function ChallengesScreen() {
const listStatus = useAppSelector(selectChallengesListStatus); const listStatus = useAppSelector(selectChallengesListStatus);
const listError = useAppSelector(selectChallengesListError); const listError = useAppSelector(selectChallengesListError);
console.log('challenges', challenges);
useEffect(() => { useEffect(() => {
if (listStatus === 'idle') { if (listStatus === 'idle') {
dispatch(fetchChallenges()); dispatch(fetchChallenges());
@@ -124,7 +127,7 @@ export default function ChallengesScreen() {
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
style={styles.giftButton} style={styles.giftButton}
> >
<IconSymbol name="gift.fill" size={22} color={colorTokens.onPrimary} /> <IconSymbol name="gift.fill" size={18} color={colorTokens.onPrimary} />
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -162,7 +165,7 @@ function ChallengeCard({ challenge, surfaceColor, textColor, mutedColor, onPress
<Image <Image
source={{ uri: challenge.image }} source={{ uri: challenge.image }}
style={styles.cardImage} style={styles.cardImage}
resizeMode="cover" cachePolicy={'memory-disk'}
/> />
<View style={styles.cardContent}> <View style={styles.cardContent}>
@@ -252,8 +255,8 @@ const styles = StyleSheet.create({
borderRadius: 26, borderRadius: 26,
}, },
giftButton: { giftButton: {
width: 52, width: 32,
height: 52, height: 32,
borderRadius: 26, borderRadius: 26,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',

View File

@@ -17,6 +17,7 @@ import {
selectProgressError, selectProgressError,
selectProgressStatus, selectProgressStatus,
} from '@/store/challengesSlice'; } from '@/store/challengesSlice';
import { Toast } from '@/utils/toast.utils';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@@ -82,6 +83,7 @@ export default function ChallengeDetailScreen() {
const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]); const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]);
const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined)); const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined));
const detailStatusSelector = useMemo(() => (id ? selectChallengeDetailStatus(id) : undefined), [id]); const detailStatusSelector = useMemo(() => (id ? selectChallengeDetailStatus(id) : undefined), [id]);
const detailStatus = useAppSelector((state) => (detailStatusSelector ? detailStatusSelector(state) : 'idle')); const detailStatus = useAppSelector((state) => (detailStatusSelector ? detailStatusSelector(state) : 'idle'));
const detailErrorSelector = useMemo(() => (id ? selectChallengeDetailError(id) : undefined), [id]); const detailErrorSelector = useMemo(() => (id ? selectChallengeDetailError(id) : undefined), [id]);
@@ -103,22 +105,22 @@ export default function ChallengeDetailScreen() {
const progressError = useAppSelector((state) => (progressErrorSelector ? progressErrorSelector(state) : undefined)); const progressError = useAppSelector((state) => (progressErrorSelector ? progressErrorSelector(state) : undefined));
useEffect(() => { useEffect(() => {
if (id) { const getData = async (id: string) => {
dispatch(fetchChallengeDetail(id)); try {
await dispatch(fetchChallengeDetail(id)).unwrap;
} catch (error) {
}
} }
if (id) {
getData(id);
}
}, [dispatch, id]); }, [dispatch, id]);
const [showCelebration, setShowCelebration] = useState(false); const [showCelebration, setShowCelebration] = useState(false);
useEffect(() => {
setShowCelebration(false);
}, [id]);
useEffect(() => {
if (joinStatus === 'succeeded') {
setShowCelebration(true);
}
}, [joinStatus]);
useEffect(() => { useEffect(() => {
if (!showCelebration) { if (!showCelebration) {
@@ -179,11 +181,16 @@ export default function ChallengeDetailScreen() {
} }
}; };
const handleJoin = () => { const handleJoin = async () => {
if (!id || joinStatus === 'loading') { if (!id || joinStatus === 'loading') {
return; return;
} }
dispatch(joinChallenge(id)); try {
await dispatch(joinChallenge(id));
setShowCelebration(true)
} catch (error) {
Toast.error('加入挑战失败')
}
}; };
const handleLeave = () => { const handleLeave = () => {

View File

@@ -1,4 +1,3 @@
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
import { import {
type ChallengeDetailDto, type ChallengeDetailDto,
type ChallengeListItemDto, type ChallengeListItemDto,
@@ -11,6 +10,7 @@ import {
listChallenges, listChallenges,
reportChallengeProgress as reportChallengeProgressApi, reportChallengeProgress as reportChallengeProgressApi,
} from '@/services/challengesApi'; } from '@/services/challengesApi';
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from './index'; import type { RootState } from './index';
type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'failed'; type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'failed';
@@ -81,8 +81,11 @@ export const fetchChallengeDetail = createAsyncThunk<ChallengeDetail, string, {
'challenges/fetchDetail', 'challenges/fetchDetail',
async (id, { rejectWithValue }) => { async (id, { rejectWithValue }) => {
try { try {
return await getChallengeDetail(id); const ret = await getChallengeDetail(id);
return ret;
} catch (error) { } catch (error) {
console.log('######', error);
return rejectWithValue(toErrorMessage(error)); return rejectWithValue(toErrorMessage(error));
} }
} }
@@ -290,6 +293,8 @@ const formatMonthDay = (input: string | undefined): string | undefined => {
}; };
const buildDateRangeLabel = (challenge: ChallengeEntity): string => { const buildDateRangeLabel = (challenge: ChallengeEntity): string => {
console.log('!!!!!', challenge);
const startLabel = formatMonthDay(challenge.startAt); const startLabel = formatMonthDay(challenge.startAt);
const endLabel = formatMonthDay(challenge.endAt); const endLabel = formatMonthDay(challenge.endAt);
if (startLabel && endLabel) { if (startLabel && endLabel) {