diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx
index babbc24..dc8ce77 100644
--- a/app/(tabs)/personal.tsx
+++ b/app/(tabs)/personal.tsx
@@ -243,7 +243,7 @@ export default function PersonalScreen() {
{isLgAvaliable ? (
pushIfAuthedElseLogin('/profile/edit')}>
-
+
{profileActionLabel}
@@ -834,7 +834,6 @@ const styles = StyleSheet.create({
borderRadius: 16,
},
editButtonGlass: {
- backgroundColor: '#ffffff',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 16,
diff --git a/app/challenges/[id]/index.tsx b/app/challenges/[id]/index.tsx
index 6fbe217..acb8097 100644
--- a/app/challenges/[id]/index.tsx
+++ b/app/challenges/[id]/index.tsx
@@ -24,11 +24,12 @@ import {
import { Toast } from '@/utils/toast.utils';
import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
+import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams, useRouter } from 'expo-router';
import LottieView from 'lottie-react-native';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
@@ -43,6 +44,7 @@ import {
View,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import { captureRef } from 'react-native-view-shot';
const { width } = Dimensions.get('window');
const HERO_HEIGHT = width * 0.76;
@@ -87,6 +89,9 @@ export default function ChallengeDetailScreen() {
const insets = useSafeAreaInsets();
const { ensureLoggedIn } = useAuthGuard();
+
+ // 用于截图分享的引用
+ const shareCardRef = useRef(null);
const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]);
const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined));
@@ -180,17 +185,35 @@ export default function ChallengeDetailScreen() {
);
const handleShare = async () => {
- if (!challenge) {
+ if (!challenge || !shareCardRef.current) {
return;
}
+
try {
+ Toast.show({
+ type: 'info',
+ text1: '正在生成分享卡片...',
+ });
+
+ // 捕获分享卡片视图
+ const uri = await captureRef(shareCardRef, {
+ format: 'png',
+ quality: 0.9,
+ });
+
+ // 分享图片
+ const shareMessage = isJoined && progress
+ ? `我正在参与「${challenge.title}」挑战,已完成 ${progress.completed}/${progress.target} 天!一起加入吧!`
+ : `发现一个很棒的挑战「${challenge.title}」,一起来参与吧!`;
+
await Share.share({
title: challenge.title,
- message: `我正在参与「${challenge.title}」,一起坚持吧!`,
- url: challenge.image,
+ message: shareMessage,
+ url: Platform.OS === 'ios' ? uri : `file://${uri}`,
});
} catch (error) {
console.warn('分享失败', error);
+ Toast.error('分享失败,请稍后重试');
}
};
@@ -357,8 +380,104 @@ export default function ChallengeDetailScreen() {
const participantsLabel = formatParticipantsLabel(challenge.participantsCount);
const inlineErrorMessage = detailStatus === 'failed' && detailError ? detailError : undefined;
+
return (
+ {/* 隐藏的分享卡片,用于截图 */}
+
+
+ {/* 背景图片 */}
+
+
+
+ {/* 分享卡片内容 */}
+
+ {challenge.title}
+ {challenge.summary ? (
+
+ {challenge.summary}
+
+ ) : null}
+
+ {/* 根据是否加入显示不同内容 */}
+ {isJoined && progress ? (
+ // 已加入:显示个人进度
+
+
+ 我的坚持进度
+
+ {progress.completed} / {progress.target} 天
+
+
+
+ {/* 进度条 */}
+
+
+
+
+
+ {progress.completed === progress.target
+ ? '🎉 已完成挑战!'
+ : `还差 ${progress.target - progress.completed} 天完成挑战`}
+
+
+ ) : (
+ // 未加入:显示挑战信息
+
+
+
+
+
+
+ {dateRangeLabel}
+ {challenge.durationLabel ? (
+ {challenge.durationLabel}
+ ) : null}
+
+
+
+
+
+
+
+
+ {challenge.requirementLabel}
+ 按日打卡自动累计
+
+
+
+
+
+
+
+
+ {participantsLabel}
+ 快来一起坚持吧
+
+
+
+ )}
+
+ {/* 底部标识 */}
+
+ Out Live · 超越生命
+
+
+
+
+
@@ -367,11 +486,31 @@ export default function ChallengeDetailScreen() {
tone="light"
transparent
withSafeTop={false}
- // right={
- //
- //
- //
- // }
+ right={
+ isLiquidGlassAvailable() ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
/>
@@ -774,20 +913,18 @@ const styles = StyleSheet.create({
highlightButtonLabelDisabled: {
color: '#6f7799',
},
- circularButton: {
+ shareButton: {
width: 40,
height: 40,
borderRadius: 20,
- backgroundColor: 'rgba(255,255,255,0.24)',
alignItems: 'center',
justifyContent: 'center',
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.45)',
+ overflow: 'hidden',
},
- shareIcon: {
- fontSize: 18,
- color: '#ffffff',
- fontWeight: '700',
+ fallbackShareButton: {
+ backgroundColor: 'rgba(255, 255, 255, 0.24)',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.45)',
},
missingContainer: {
flex: 1,
@@ -820,4 +957,130 @@ const styles = StyleSheet.create({
width: width * 1.3,
height: width * 1.3,
},
+ // 分享卡片样式
+ offscreenContainer: {
+ position: 'absolute',
+ left: -9999,
+ top: -9999,
+ opacity: 0,
+ },
+ shareCard: {
+ width: 375,
+ height: 500,
+ backgroundColor: '#fff',
+ overflow: 'hidden',
+ borderRadius: 24,
+ },
+ shareCardBg: {
+ ...StyleSheet.absoluteFillObject,
+ width: '100%',
+ height: '100%',
+ },
+ shareCardContent: {
+ flex: 1,
+ padding: 24,
+ justifyContent: 'space-between',
+ },
+ shareCardTitle: {
+ fontSize: 28,
+ fontWeight: '800',
+ color: '#ffffff',
+ marginTop: 20,
+ textShadowColor: 'rgba(0, 0, 0, 0.3)',
+ textShadowOffset: { width: 0, height: 2 },
+ textShadowRadius: 4,
+ },
+ shareCardSummary: {
+ fontSize: 15,
+ color: '#ffffff',
+ marginTop: 12,
+ lineHeight: 22,
+ opacity: 0.95,
+ textShadowColor: 'rgba(0, 0, 0, 0.25)',
+ textShadowOffset: { width: 0, height: 1 },
+ textShadowRadius: 3,
+ },
+ shareProgressContainer: {
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
+ borderRadius: 20,
+ padding: 20,
+ marginTop: 'auto',
+ },
+ shareInfoContainer: {
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
+ borderRadius: 20,
+ padding: 20,
+ marginTop: 'auto',
+ gap: 16,
+ },
+ shareInfoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ shareInfoIconWrapper: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: '#EEF0FF',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ shareInfoTextWrapper: {
+ marginLeft: 12,
+ flex: 1,
+ },
+ shareInfoLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#1c1f3a',
+ },
+ shareInfoMeta: {
+ fontSize: 12,
+ color: '#707baf',
+ marginTop: 2,
+ },
+ shareProgressHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ shareProgressLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#1c1f3a',
+ },
+ shareProgressValue: {
+ fontSize: 18,
+ fontWeight: '800',
+ color: '#5E8BFF',
+ },
+ shareProgressTrack: {
+ height: 8,
+ backgroundColor: '#eceffa',
+ borderRadius: 4,
+ overflow: 'hidden',
+ },
+ shareProgressBar: {
+ height: '100%',
+ backgroundColor: '#5E8BFF',
+ borderRadius: 4,
+ },
+ shareProgressSubtext: {
+ fontSize: 13,
+ color: '#707baf',
+ marginTop: 12,
+ textAlign: 'center',
+ fontWeight: '500',
+ },
+ shareCardFooter: {
+ alignItems: 'center',
+ paddingTop: 16,
+ },
+ shareCardFooterText: {
+ fontSize: 12,
+ color: '#ffffff',
+ opacity: 0.8,
+ fontWeight: '600',
+ },
});