feat(challenges): 优化挑战列表与详情页交互体验
- 替换 Image 为 expo-image 并启用缓存策略 - 调整礼物按钮尺寸与图标大小 - 加入挑战失败时弹出 Toast 提示 - 统一异步流程并移除冗余状态监听 - 清理调试日志与多余空行
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user