feat(challenges): 移除旧版挑战页面并优化详情页交互
删除废弃的 app/challenge 目录及其所有文件,统一使用新的 challenges 模块。在详情页新增退出挑战确认弹窗,优化浮动 CTA 文案与交互,调整进度卡片样式与布局。
This commit is contained in:
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user