feat(challenges): 移除旧版挑战页面并优化详情页交互

删除废弃的 app/challenge 目录及其所有文件,统一使用新的 challenges 模块。在详情页新增退出挑战确认弹窗,优化浮动 CTA 文案与交互,调整进度卡片样式与布局。
This commit is contained in:
richarjiang
2025-09-29 14:13:10 +08:00
parent 31c4e4fafa
commit 9c86b0e565
4 changed files with 69 additions and 378 deletions

View File

@@ -27,6 +27,7 @@ import LottieView from 'lottie-react-native';
import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Dimensions,
Image,
Platform,
@@ -41,7 +42,7 @@ import {
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
const { width } = Dimensions.get('window');
const HERO_HEIGHT = width * 0.86;
const HERO_HEIGHT = width * 0.76;
const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF'];
const isHttpUrl = (value: string) => /^https?:\/\//i.test(value);
@@ -194,11 +195,32 @@ export default function ChallengeDetailScreen() {
}
};
const handleLeave = () => {
const handleLeave = async () => {
if (!id || leaveStatus === 'loading') {
return;
}
dispatch(leaveChallenge(id));
try {
await dispatch(leaveChallenge(id)).unwrap();
await dispatch(fetchChallengeDetail(id)).unwrap();
} catch (error) {
Toast.error('退出挑战失败');
}
};
const handleLeaveConfirm = () => {
if (!id || leaveStatus === 'loading') {
return;
}
Alert.alert('确认退出挑战?', '退出后需要重新加入才能继续坚持。', [
{ text: '取消', style: 'cancel' },
{
text: '退出挑战',
style: 'destructive',
onPress: () => {
void handleLeave();
},
},
]);
};
const handleProgressReport = () => {
@@ -256,7 +278,16 @@ export default function ChallengeDetailScreen() {
const highlightTitle = challenge.highlightTitle ?? '立即加入挑战';
const highlightSubtitle = challenge.highlightSubtitle ?? '邀请好友一起坚持,更容易收获成果';
const ctaLabel = joinStatus === 'loading' ? '加入中…' : challenge.ctaLabel ?? '立即加入挑战';
const joinCtaLabel = joinStatus === 'loading' ? '加入中…' : challenge.ctaLabel ?? '立即加入挑战';
const leaveHighlightTitle = '先别急着离开';
const leaveHighlightSubtitle = '再坚持一下,下一个里程碑就要出现了';
const leaveCtaLabel = leaveStatus === 'loading' ? '退出中…' : '退出挑战';
const floatingHighlightTitle = isJoined ? leaveHighlightTitle : highlightTitle;
const floatingHighlightSubtitle = isJoined ? leaveHighlightSubtitle : highlightSubtitle;
const floatingCtaLabel = isJoined ? leaveCtaLabel : joinCtaLabel;
const floatingOnPress = isJoined ? handleLeaveConfirm : handleJoin;
const floatingDisabled = isJoined ? leaveStatus === 'loading' : joinStatus === 'loading';
const floatingError = isJoined ? leaveError : joinError;
const participantsLabel = formatParticipantsLabel(challenge.participantsCount);
const inlineErrorMessage = detailStatus === 'failed' && detailError ? detailError : undefined;
@@ -321,15 +352,10 @@ export default function ChallengeDetailScreen() {
style={styles.progressCard}
>
<View style={styles.progressHeaderRow}>
<View style={styles.progressBadgeRing}>
<View style={styles.progressBadgeFallback}>
<Text style={styles.progressBadgeText}></Text>
</View>
</View>
<View style={styles.progressHeadline}>
<Text style={styles.progressTitle}>{challenge.title}</Text>
</View>
<Text style={styles.progressRemaining}> {dayjs(challenge.endAt).diff(dayjs(), 'd') || 0} </Text>
<Text style={styles.progressRemaining}> {dayjs(challenge.endAt).diff(dayjs(), 'd') || 0} </Text>
</View>
<View style={styles.progressMetaRow}>
@@ -485,34 +511,32 @@ export default function ChallengeDetailScreen() {
</View>
</ScrollView>
{!isJoined && (
<View pointerEvents="box-none" style={[styles.floatingCTAContainer, { paddingBottom: insets.bottom }]}>
<BlurView intensity={10} tint="light" style={styles.floatingCTABlur}>
<View style={styles.floatingCTAContent}>
<View style={styles.highlightCopy}>
<Text style={styles.highlightTitle}>{highlightTitle}</Text>
<Text style={styles.highlightSubtitle}>{highlightSubtitle}</Text>
{joinError ? <Text style={styles.ctaErrorText}>{joinError}</Text> : null}
</View>
<TouchableOpacity
style={styles.highlightButton}
activeOpacity={0.9}
onPress={handleJoin}
disabled={joinStatus === 'loading'}
>
<LinearGradient
colors={CTA_GRADIENT}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.highlightButtonBackground}
>
<Text style={styles.highlightButtonLabel}>{ctaLabel}</Text>
</LinearGradient>
</TouchableOpacity>
<View pointerEvents="box-none" style={[styles.floatingCTAContainer, { paddingBottom: insets.bottom }]}>
<BlurView intensity={10} tint="light" style={styles.floatingCTABlur}>
<View style={styles.floatingCTAContent}>
<View style={styles.highlightCopy}>
<Text style={styles.highlightTitle}>{floatingHighlightTitle}</Text>
<Text style={styles.highlightSubtitle}>{floatingHighlightSubtitle}</Text>
{floatingError ? <Text style={styles.ctaErrorText}>{floatingError}</Text> : null}
</View>
</BlurView>
</View>
)}
<TouchableOpacity
style={styles.highlightButton}
activeOpacity={0.9}
onPress={floatingOnPress}
disabled={floatingDisabled}
>
<LinearGradient
colors={CTA_GRADIENT}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.highlightButtonBackground}
>
<Text style={styles.highlightButtonLabel}>{floatingCtaLabel}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</BlurView>
</View>
</View>
{showCelebration && (
<View pointerEvents="none" style={styles.celebrationOverlay}>
@@ -547,8 +571,8 @@ const styles = StyleSheet.create({
height: HERO_HEIGHT,
width: '100%',
overflow: 'hidden',
borderBottomLeftRadius: 36,
borderBottomRightRadius: 36,
position: 'absolute',
top: 0
},
heroImage: {
width: '100%',
@@ -627,17 +651,17 @@ const styles = StyleSheet.create({
color: '#5f6a97',
},
progressRemaining: {
fontSize: 13,
fontSize: 11,
fontWeight: '600',
color: '#707baf',
marginLeft: 16,
alignSelf: 'flex-start',
},
progressMetaRow: {
marginTop: 18,
marginTop: 12,
},
progressMetaValue: {
fontSize: 16,
fontSize: 14,
fontWeight: '700',
color: '#4F5BD5',
},
@@ -647,7 +671,7 @@ const styles = StyleSheet.create({
color: '#7a86bb',
},
progressBarTrack: {
marginTop: 16,
marginTop: 12,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#eceffa',
@@ -657,7 +681,7 @@ const styles = StyleSheet.create({
},
progressBarSegment: {
flex: 1,
height: 8,
height: 4,
borderRadius: 4,
backgroundColor: '#dfe4f6',
marginHorizontal: 3,
@@ -738,7 +762,7 @@ const styles = StyleSheet.create({
},
headerTextBlock: {
paddingHorizontal: 24,
marginTop: 24,
marginTop: HERO_HEIGHT - 60,
alignItems: 'center',
},
periodLabel: {
@@ -795,8 +819,6 @@ const styles = StyleSheet.create({
detailIconWrapper: {
width: 42,
height: 42,
borderRadius: 21,
backgroundColor: '#EFF1FF',
alignItems: 'center',
justifyContent: 'center',
},