feat: 个人信息优化

This commit is contained in:
jinyaqiu 2025-08-06 15:45:54 +08:00
parent 11ceca9753
commit 7c4d1529d4
5 changed files with 419 additions and 143 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,49 @@
import { ThemedText } from "@/components/ThemedText";
import { ActivityIndicator, StyleSheet, TouchableOpacity } from "react-native";
interface Props {
isLoading?: boolean;
onPress?: () => void;
text: string
bg?: string
color?: string
}
const StepButton = (props: Props) => {
const { isLoading, onPress, text, bg, color } = props
return (
<TouchableOpacity
style={[styles.button, isLoading && styles.disabledButton, { backgroundColor: bg ? bg : '#E2793F' }]}
onPress={onPress}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={[styles.buttonText, { color: color ? color : '#FFFFFF' }]}>
{text}
</ThemedText>
)}
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
button: {
width: '100%',
backgroundColor: '#E2793F',
borderRadius: 32,
padding: 18,
alignItems: 'center'
},
disabledButton: {
opacity: 0.7,
},
buttonText: {
color: '#FFFFFF',
fontWeight: '600',
fontSize: 18,
},
});
export default StepButton

View File

@ -1,89 +1,216 @@
import DoneSvg from '@/assets/icons/svg/done.svg';
import { router } from 'expo-router';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform, TouchableOpacity, View } from 'react-native';
import { Dimensions, Image, PixelRatio, Platform, StyleSheet, View } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated';
import { ThemedText } from '../ThemedText';
import { Fireworks } from '../firework';
import Lottie from '../lottie/lottie';
import StepButton from '../ui/button/stepButton';
export default function Done() {
const { t } = useTranslation();
const height = Dimensions.get('window').height;
const fontSize = (size: number) => {
const scale = PixelRatio.getFontScale();
return size / scale;
};
// Animation values
const translateX = useSharedValue(300);
const translateY = useSharedValue(300);
const opacity = useSharedValue(0);
// Animation style
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value }
],
opacity: opacity.value
}));
// Start animation when component mounts
useEffect(() => {
translateX.value = withTiming(0, {
duration: 800,
easing: Easing.out(Easing.cubic)
});
translateY.value = withTiming(0, {
duration: 800,
easing: Easing.out(Easing.cubic)
});
opacity.value = withTiming(1, {
duration: 1000,
easing: Easing.out(Easing.cubic)
});
}, []);
const handleContinue = () => {
router.replace('/ask')
};
return (
<View className="flex-1">
{
Platform.OS === 'web'
?
<View className="flex-1 bg-bgPrimary absolute top-0 left-0 right-0 bottom-0 h-full">
<View className="absolute top-[2rem] left-0 right-0 bottom-[10rem] justify-center items-center">
<ThemedText className="!text-4xl !text-white text-center">
{t('auth.userMessage.allDone', { ns: 'login' })}
</ThemedText>
</View>
<View className='flex-1' />
<View className="flex-row justify-end">
<DoneSvg />
</View>
{/* Next Button */}
<View className="absolute bottom-[1rem] left-0 right-0 p-[1rem] z-99">
<TouchableOpacity
className={`w-full bg-buttonFill rounded-full p-4 items-center`}
onPress={handleContinue}
>
<ThemedText className="!text-white text-lg font-semibold">
{t('auth.userMessage.next', { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
</View>
</View>
:
<View className="flex-1 bg-transparent">
{/* 文字 */}
<View className="absolute top-0 left-0 right-0 bottom-0 z-30">
<View className="flex-1 justify-center items-center">
<ThemedText className="!text-4xl !text-white text-center">
{t('auth.userMessage.allDone', { ns: 'login' })}
</ThemedText>
</View>
{/* Next Button */}
<View className="absolute bottom-[1rem] left-0 right-0 p-[1rem] z-99">
<TouchableOpacity
className={`w-full bg-buttonFill rounded-full p-4 items-center`}
onPress={handleContinue}
>
<ThemedText className="!text-white text-lg font-semibold">
{t('auth.userMessage.next', { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
</View>
</View>
{/* 背景动画 - 烟花 */}
<View className="absolute top-0 left-0 right-0 bottom-0 z-10">
<Fireworks
autoPlay={true}
loop={false}
interval={1500}
particleCount={90}
/>
</View>
{/* 前景动画 - Lottie */}
<View className="absolute top-0 left-0 right-0 bottom-0 z-20">
<Lottie
source={'allDone'}
style={{
width: "100%",
height: "100%",
backgroundColor: 'transparent'
}}
loop={false}
/>
</View>
</View>
}
const renderWebView = () => (
<View style={styles.webContainer}>
<View style={styles.webContent}>
<ThemedText style={styles.title}>
{t('auth.userMessage.allDone', { ns: 'login' })}
</ThemedText>
</View>
<View style={styles.flex1} />
<View style={styles.lottieContainer}>
<Animated.View style={animatedStyle}>
<Image
source={require('@/assets/images/png/icon/doneIP.png')}
/>
</Animated.View>
</View>
<View style={styles.webButtonContainer}>
<StepButton
text={t('auth.userMessage.next', { ns: 'login' })}
onPress={handleContinue}
/>
</View>
</View>
)
);
const renderMobileView = () => (
<View style={styles.mobileContainer}>
<View style={styles.mobileContent}>
<View style={[styles.mobileTextContainer, { marginTop: -height * 0.15 }]}>
<ThemedText style={[styles.title, { fontSize: fontSize(36) }]}>
{t('auth.userMessage.allDone', { ns: 'login' })}
</ThemedText>
</View>
<View style={styles.mobileButtonContainer}>
<StepButton
text={t('auth.userMessage.next', { ns: 'login' })}
onPress={handleContinue}
/>
</View>
</View>
<View style={styles.fireworksContainer}>
<Fireworks
autoPlay={true}
loop={false}
interval={1500}
particleCount={90}
/>
</View>
<View style={styles.lottieContainer}>
<Animated.View style={animatedStyle}>
<Image
source={require('@/assets/images/png/icon/doneIP.png')}
/>
</Animated.View>
</View>
</View>
);
return (
<View style={styles.container}>
{Platform.OS === 'web' ? renderWebView() : renderMobileView()}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
flex1: {
flex: 1,
},
webContainer: {
flex: 1,
backgroundColor: '#FFB645',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
height: '100%',
},
webContent: {
position: 'absolute',
top: 32,
left: 0,
right: 0,
bottom: 160,
justifyContent: 'center',
alignItems: 'center',
},
doneSvgContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
webButtonContainer: {
position: 'absolute',
bottom: 16,
left: 0,
right: 0,
padding: 16,
zIndex: 99,
},
mobileContainer: {
flex: 1,
backgroundColor: 'transparent',
},
mobileContent: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 30,
},
mobileTextContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
mobileButtonContainer: {
position: 'absolute',
bottom: 16,
left: 0,
right: 0,
padding: 16,
zIndex: 99,
},
title: {
fontSize: 36,
lineHeight: 40,
color: '#FFFFFF',
textAlign: 'center',
fontWeight: 'bold',
},
nextButton: {
width: '100%',
backgroundColor: '#3B82F6',
borderRadius: 999,
padding: 16,
alignItems: 'center',
},
buttonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
},
fireworksContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 10,
},
lottieContainer: {
position: 'absolute',
right: 0,
bottom: 0,
zIndex: 20
},
});

View File

@ -4,8 +4,10 @@ import LookSvg from '@/assets/icons/svg/look.svg';
import { ThemedText } from '@/components/ThemedText';
import { FileUploadItem } from '@/lib/background-uploader/types';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Alert, Image, TouchableOpacity, View } from 'react-native';
import { Alert, Image, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import FilesUploader from '../file-upload/files-uploader';
import StepButton from '../ui/button/stepButton';
interface Props {
setSteps?: (steps: Steps) => void;
@ -19,57 +21,51 @@ interface Props {
export default function Look(props: Props) {
const { fileData, setFileData, isLoading, handleUser, avatar } = props;
const { t } = useTranslation();
const insets = useSafeAreaInsets();
return (
<View className="flex-1 bg-textPrimary justify-between p-[2rem]">
<View className="flex-1 justify-center items-center">
<ThemedText className="text-4xl font-bold !text-white mb-[2rem]">
<View style={[styles.container, { paddingBottom: insets.bottom, paddingTop: insets.top + 28 }]}>
<View style={styles.contentContainer}>
<ThemedText style={styles.title}>
{t('auth.userMessage.look', { ns: 'login' })}
</ThemedText>
<ThemedText className="text-base !text-white/80 text-center mb-[2rem]">
<ThemedText style={styles.subtitle}>
{t('auth.userMessage.avatarText', { ns: 'login' })}
{"\n"}
{t('auth.userMessage.avatorText2', { ns: 'login' })}
</ThemedText>
{
fileData[0]?.preview || fileData[0]?.previewUrl
?
<Image
className='rounded-full w-[10rem] h-[10rem]'
source={{ uri: fileData[0].preview || fileData[0].previewUrl }}
/>
:
avatar
?
<Image
className='rounded-full w-[10rem] h-[10rem]'
source={{ uri: avatar }}
/>
:
<LookSvg />
}
{fileData[0]?.preview || fileData[0]?.previewUrl ? (
<Image
style={styles.avatarImage}
source={{ uri: fileData[0].preview || fileData[0].previewUrl }}
/>
) : avatar ? (
<Image
style={styles.avatarImage}
source={{ uri: avatar }}
/>
) : (
<LookSvg />
)}
<FilesUploader
onUploadComplete={(fileData) => {
setFileData(fileData as FileUploadItem[]);
}}
showPreview={false}
children={
<View className="w-full rounded-full px-4 py-2 mt-4 items-center bg-inputBackground flex-row flex gap-2">
<View style={styles.uploadButton}>
<ChoicePhoto />
<ThemedText className="text-textTertiary text-lg font-semibold">
<ThemedText style={styles.uploadButtonText}>
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
</ThemedText>
</View>
}
/>
{/* <AutoUploadScreen /> */}
{/* <MediaStatsScreen /> */}
</View>
<View className="w-full">
<TouchableOpacity
className={`w-full bg-white rounded-full p-4 items-center ${isLoading ? 'opacity-70' : ''}`}
<View style={styles.footer}>
<StepButton
text={t('auth.userMessage.next', { ns: 'login' })}
onPress={() => {
if (fileData[0]?.preview || fileData[0]?.previewUrl || avatar) {
handleUser()
@ -77,17 +73,61 @@ export default function Look(props: Props) {
Alert.alert(t('auth.userMessage.avatarRequired', { ns: 'login' }))
}
}}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#000" />
) : (
<ThemedText className="text-textTertiary text-lg font-semibold">
{t('auth.userMessage.next', { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
isLoading={isLoading}
bg="#FFFFFF"
color="#4C320C"
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#AC7E35',
paddingHorizontal: 24,
justifyContent: 'space-between',
},
contentContainer: {
flex: 1,
alignItems: 'center',
gap: 28
},
title: {
fontSize: 32,
lineHeight: 36,
fontWeight: 'bold',
color: '#FFFFFF',
},
subtitle: {
fontSize: 14,
color: "#fff",
textAlign: 'center',
marginBottom: 16,
},
avatarImage: {
borderRadius: 150,
width: 215,
height: 215,
marginBottom: 16,
},
uploadButton: {
width: '100%',
borderRadius: 999,
paddingHorizontal: 16,
paddingVertical: 13,
alignItems: 'center',
backgroundColor: '#FFF8DE',
flexDirection: 'row',
gap: 8,
},
uploadButtonText: {
color: '#4C320C',
fontSize: 14,
fontWeight: '600',
},
footer: {
width: '100%',
}
});

View File

@ -2,7 +2,9 @@ import { Steps } from '@/app/(tabs)/user-message';
import { ThemedText } from '@/components/ThemedText';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, KeyboardAvoidingView, Platform, TextInput, TouchableOpacity, View } from 'react-native';
import { Dimensions, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import StepButton from '../ui/button/stepButton';
interface Props {
setSteps: (steps: Steps) => void;
@ -14,6 +16,8 @@ export default function UserName(props: Props) {
const { setSteps, username, setUsername } = props
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false)
const height = Dimensions.get('window').height;
const insets = useSafeAreaInsets();
const [error, setError] = useState('')
const handleUserName = () => {
if (!username) {
@ -28,45 +32,101 @@ export default function UserName(props: Props) {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
style={styles.keyboardAvoidingView}
>
<View className='bg-bgPrimary flex-1 h-full'>
<View className="flex-1" />
{/* Input container fixed at bottom */}
<View className="w-full bg-white p-4 border-t border-gray-200 rounded-t-3xl">
<View className="flex-col items-center justify-center w-full gap-[1rem]">
<View className='w-full flex flex-row items-center justify-center'>
<ThemedText className="text-textSecondary font-semibold">{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
<View style={[styles.container]}>
<View style={styles.flex1} />
<View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
<View style={styles.contentContainer}>
<View style={styles.titleContainer}>
<ThemedText style={styles.titleText}>{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
</View>
<View className='w-full mb-[1rem]'>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
<ThemedText className="!text-textPrimary ml-2 mb-2 font-semibold">{t('auth.userMessage.username', { ns: 'login' })}</ThemedText>
<ThemedText style={{ color: "#E2793F", fontSize: 14 }}>{error}</ThemedText>
<View style={styles.inputWrapper}>
<View style={styles.labelContainer}>
<ThemedText style={styles.labelText}>{t('auth.userMessage.username', { ns: 'login' })}</ThemedText>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
<TextInput
className="bg-inputBackground rounded-2xl p-4 w-full"
style={[styles.textInput, { marginBottom: height * 0.2 }]}
placeholder={t('auth.userMessage.usernamePlaceholder', { ns: 'login' })}
placeholderTextColor="#9CA3AF"
value={username}
onChangeText={setUsername}
/>
</View>
<TouchableOpacity
className={`w-full bg-[#E2793F] rounded-full text-[#fff] p-4 items-center mb-6 ${isLoading ? 'opacity-70' : ''} rounded-2xl`}
onPress={handleUserName}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText className="!text-white font-semibold">
{t('auth.userMessage.next', { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
<StepButton text={t('auth.userMessage.next', { ns: 'login' })} onPress={handleUserName} isLoading={isLoading} />
</View>
</View>
</View>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
keyboardAvoidingView: {
flex: 1,
},
container: {
flex: 1,
backgroundColor: '#FFB645',
height: '100%',
},
flex1: {
flex: 1,
},
inputContainer: {
width: '100%',
backgroundColor: '#FFFFFF',
padding: 16,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
borderTopLeftRadius: 50,
borderTopRightRadius: 50,
},
contentContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: 16,
},
titleContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
},
titleText: {
color: '#4C320C',
fontWeight: '600',
fontSize: 20,
marginBottom: 16,
},
inputWrapper: {
width: '100%',
marginBottom: 16,
},
labelContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 10,
},
labelText: {
color: '#AC7E35',
marginLeft: 8,
fontSize: 14,
fontWeight: '600',
},
errorText: {
color: '#E2793F',
fontSize: 14,
},
textInput: {
backgroundColor: '#FFF8DE',
borderRadius: 16,
padding: 20,
width: '100%',
}
});