From 6c2f9295be1e8e4299353590dcce56be27a5148e Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 14 Nov 2025 10:02:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=8C=91?= =?UTF-8?q?=E6=88=98=E8=AF=A6=E6=83=85=E5=88=86=E4=BA=AB=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=92=8C=E7=8E=BB=E7=92=83=E6=80=81=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现挑战分享卡片截图生成,根据参与状态展示不同内容 - 已参与用户显示个人进度条和完成情况 - 未参与用户显示挑战详细信息和参与邀请 - 使用 GlassView 组件优化分享按钮和编辑按钮的视觉效果 - 添加 react-native-view-shot 支持视图截图 - 移除硬编码背景色,统一使用玻璃态交互效果 --- app/(tabs)/personal.tsx | 3 +- app/challenges/[id]/index.tsx | 297 ++++++++++++++++++++++++++++++++-- 2 files changed, 281 insertions(+), 19 deletions(-) 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', + }, });