202 lines
5.4 KiB
TypeScript
202 lines
5.4 KiB
TypeScript
import React, { useRef, useEffect } from 'react';
|
||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
|
||
|
||
const TRANSITION_OFFSET = 24;
|
||
const TRANSITION_DURATION = 280;
|
||
|
||
interface OnboardingLayoutProps {
|
||
children: React.ReactNode;
|
||
title?: string;
|
||
currentStep: number;
|
||
totalSteps: number;
|
||
onSkip: () => void;
|
||
onBack?: () => void;
|
||
showBackButton?: boolean;
|
||
/** 用户名字,仅在名字步骤之后的第一个问题(currentStep === 1)且非空时显示招呼语 */
|
||
userName?: string;
|
||
}
|
||
|
||
export function OnboardingLayout({
|
||
children,
|
||
title,
|
||
currentStep,
|
||
totalSteps,
|
||
onSkip,
|
||
onBack,
|
||
showBackButton = false,
|
||
userName = '',
|
||
}: OnboardingLayoutProps) {
|
||
const { t } = useTranslation();
|
||
const showGreeting = currentStep === 1 && userName.trim().length > 0;
|
||
const displayName = userName.trim();
|
||
const prevStepRef = useRef(currentStep);
|
||
const isFirstRenderRef = useRef(true);
|
||
const translateX = useRef(new Animated.Value(0)).current;
|
||
const opacity = useRef(new Animated.Value(1)).current;
|
||
|
||
useEffect(() => {
|
||
if (isFirstRenderRef.current) {
|
||
isFirstRenderRef.current = false;
|
||
prevStepRef.current = currentStep;
|
||
return;
|
||
}
|
||
if (prevStepRef.current === currentStep) return;
|
||
|
||
const direction = currentStep > prevStepRef.current ? 'forward' : 'back';
|
||
prevStepRef.current = currentStep;
|
||
|
||
const startX = direction === 'forward' ? TRANSITION_OFFSET : -TRANSITION_OFFSET;
|
||
translateX.setValue(startX);
|
||
opacity.setValue(0.72);
|
||
|
||
Animated.parallel([
|
||
Animated.timing(translateX, {
|
||
toValue: 0,
|
||
duration: TRANSITION_DURATION,
|
||
useNativeDriver: true,
|
||
easing: Easing.out(Easing.cubic),
|
||
}),
|
||
Animated.timing(opacity, {
|
||
toValue: 1,
|
||
duration: TRANSITION_DURATION,
|
||
useNativeDriver: true,
|
||
easing: Easing.out(Easing.cubic),
|
||
}),
|
||
]).start();
|
||
}, [currentStep, translateX, opacity]);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<StatusBar barStyle="dark-content" />
|
||
<SafeAreaView style={styles.safeArea}>
|
||
{/* Header: Back & Skip */}
|
||
<View style={styles.header}>
|
||
<View style={styles.headerLeft}>
|
||
{showBackButton && onBack && (
|
||
<TouchableOpacity onPress={onBack} style={styles.iconButton}>
|
||
<Image
|
||
source={require('@/assets/images/icon/back_icon.png')}
|
||
style={styles.backIcon}
|
||
/>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
|
||
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
|
||
<Text style={styles.skipText}>{t('onboarding.skipAll')}</Text>
|
||
<Image
|
||
source={require('@/assets/images/icon/skip_icon.png')}
|
||
style={styles.skipIcon}
|
||
/>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* Title & Progress Row(名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
|
||
<View style={styles.titleRow}>
|
||
<View style={styles.titleBlock}>
|
||
{showGreeting && (
|
||
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
|
||
)}
|
||
<Text style={styles.questionTitle}>{title}</Text>
|
||
</View>
|
||
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
|
||
</View>
|
||
|
||
{/* Content:step 切换时滑动 + 淡入 */}
|
||
<Animated.View
|
||
style={[
|
||
styles.content,
|
||
{
|
||
opacity,
|
||
transform: [{ translateX }],
|
||
},
|
||
]}
|
||
>
|
||
{children}
|
||
</Animated.View>
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: OnboardingColors.background,
|
||
},
|
||
safeArea: {
|
||
flex: 1,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 20,
|
||
height: 44,
|
||
},
|
||
headerLeft: {
|
||
width: 44,
|
||
height: 44,
|
||
justifyContent: 'center',
|
||
},
|
||
iconButton: {
|
||
padding: 8,
|
||
},
|
||
backIcon: {
|
||
width: 19,
|
||
height: 19,
|
||
resizeMode: 'contain',
|
||
},
|
||
skipButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 8,
|
||
},
|
||
skipText: {
|
||
fontSize: 13,
|
||
color: OnboardingColors.textMuted,
|
||
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
|
||
marginRight: 4,
|
||
},
|
||
skipIcon: {
|
||
width: 10,
|
||
height: 4,
|
||
resizeMode: 'contain',
|
||
},
|
||
titleRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'flex-end',
|
||
paddingHorizontal: 20,
|
||
marginTop: 20,
|
||
marginBottom: 8,
|
||
},
|
||
titleBlock: {
|
||
flex: 1,
|
||
justifyContent: 'flex-end',
|
||
},
|
||
greetingText: {
|
||
fontSize: 22,
|
||
color: OnboardingColors.questionTitle,
|
||
fontFamily: OnboardingFont.question,
|
||
marginBottom: 4,
|
||
},
|
||
questionTitle: {
|
||
fontSize: 22,
|
||
color: OnboardingColors.questionTitle,
|
||
fontFamily: OnboardingFont.question,
|
||
},
|
||
progressText: {
|
||
fontSize: 18,
|
||
color: OnboardingColors.textProgress,
|
||
fontFamily: OnboardingFont.question,
|
||
marginLeft: 10,
|
||
},
|
||
content: {
|
||
flex: 1,
|
||
paddingHorizontal: 20,
|
||
},
|
||
});
|