Files
digital-pilates/app/onboarding/personal-info.tsx
richarjiang 807e185761 feat: 更新应用版本和主题设置
- 将应用版本更新至 1.0.3,修改相关配置文件
- 强制全局使用浅色主题,确保一致的用户体验
- 在训练计划功能中新增激活计划的 API 接口,支持用户激活训练计划
- 优化打卡功能,支持自动同步打卡记录至服务器
- 更新样式以适应新功能的展示和交互
2025-08-14 22:23:45 +08:00

427 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useThemeColor } from '@/hooks/useThemeColor';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { router } from 'expo-router';
import React, { useState } from 'react';
import {
Alert,
Dimensions,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
const { width } = Dimensions.get('window');
interface PersonalInfo {
gender: 'male' | 'female' | '';
age: string;
height: string;
weight: string;
}
export default function PersonalInfoScreen() {
const colorScheme = useColorScheme();
const backgroundColor = useThemeColor({}, 'background');
const primaryColor = useThemeColor({}, 'primary');
const textColor = useThemeColor({}, 'text');
const iconColor = useThemeColor({}, 'icon');
const [personalInfo, setPersonalInfo] = useState<PersonalInfo>({
gender: '',
age: '',
height: '',
weight: '',
});
const [currentStep, setCurrentStep] = useState(0);
const steps = [
{
title: '请选择您的性别',
subtitle: '这将帮助我们为您制定更合适的训练计划',
type: 'gender' as const,
},
{
title: '请输入您的年龄',
subtitle: '年龄信息有助于调整训练强度',
type: 'age' as const,
},
{
title: '请输入您的身高',
subtitle: '身高信息用于计算身体比例',
type: 'height' as const,
},
{
title: '请输入您的体重',
subtitle: '体重信息用于个性化训练方案',
type: 'weight' as const,
},
];
const handleGenderSelect = (gender: 'male' | 'female') => {
setPersonalInfo(prev => ({ ...prev, gender }));
};
const handleInputChange = (field: keyof PersonalInfo, value: string) => {
setPersonalInfo(prev => ({ ...prev, [field]: value }));
};
const handleNext = () => {
const currentStepType = steps[currentStep].type;
// 验证当前步骤是否已填写
if (currentStepType === 'gender' && !personalInfo.gender) {
Alert.alert('提示', '请选择您的性别');
return;
}
if (currentStepType === 'age' && !personalInfo.age) {
Alert.alert('提示', '请输入您的年龄');
return;
}
if (currentStepType === 'height' && !personalInfo.height) {
Alert.alert('提示', '请输入您的身高');
return;
}
if (currentStepType === 'weight' && !personalInfo.weight) {
Alert.alert('提示', '请输入您的体重');
return;
}
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
handleComplete();
}
};
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSkip = async () => {
try {
await AsyncStorage.setItem('@onboarding_completed', 'true');
router.replace('/(tabs)');
} catch (error) {
console.error('保存引导状态失败:', error);
router.replace('/(tabs)');
}
};
const handleComplete = async () => {
try {
// 保存用户信息和引导完成状态
await AsyncStorage.multiSet([
['@onboarding_completed', 'true'],
['@user_personal_info', JSON.stringify(personalInfo)],
]);
console.log('用户信息:', personalInfo);
router.replace('/(tabs)');
} catch (error) {
console.error('保存用户信息失败:', error);
router.replace('/(tabs)');
}
};
const renderGenderSelection = () => (
<View style={styles.optionsContainer}>
<TouchableOpacity
style={[
styles.genderOption,
{ borderColor: primaryColor },
personalInfo.gender === 'female' && { backgroundColor: primaryColor + '20', borderWidth: 2 }
]}
onPress={() => handleGenderSelect('female')}
>
<Text style={styles.genderIcon}>👩</Text>
<ThemedText style={[styles.genderText, { color: textColor }]}></ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.genderOption,
{ borderColor: primaryColor },
personalInfo.gender === 'male' && { backgroundColor: primaryColor + '20', borderWidth: 2 }
]}
onPress={() => handleGenderSelect('male')}
>
<Text style={styles.genderIcon}>👨</Text>
<ThemedText style={[styles.genderText, { color: textColor }]}></ThemedText>
</TouchableOpacity>
</View>
);
const renderNumberInput = (
field: 'age' | 'height' | 'weight',
placeholder: string,
unit: string
) => (
<View style={styles.inputContainer}>
<View style={[styles.inputWrapper, { borderColor: iconColor + '30' }]}>
<TextInput
style={[styles.numberInput, { color: textColor }]}
value={personalInfo[field]}
onChangeText={(value) => handleInputChange(field, value)}
placeholder={placeholder}
placeholderTextColor={iconColor}
keyboardType="numeric"
maxLength={field === 'age' ? 3 : 4}
/>
<ThemedText style={[styles.unitText, { color: iconColor }]}>{unit}</ThemedText>
</View>
</View>
);
const renderStepContent = () => {
const step = steps[currentStep];
switch (step.type) {
case 'gender':
return renderGenderSelection();
case 'age':
return renderNumberInput('age', '请输入年龄', '岁');
case 'height':
return renderNumberInput('height', '请输入身高', 'cm');
case 'weight':
return renderNumberInput('weight', '请输入体重', 'kg');
default:
return null;
}
};
const isStepCompleted = () => {
const currentStepType = steps[currentStep].type;
switch (currentStepType) {
case 'gender':
return !!personalInfo.gender;
case 'age':
return !!personalInfo.age;
case 'height':
return !!personalInfo.height;
case 'weight':
return !!personalInfo.weight;
default:
return false;
}
};
return (
<ThemedView style={[styles.container, { backgroundColor }]}>
<StatusBar
barStyle={'dark-content'}
backgroundColor={backgroundColor}
/>
{/* 顶部导航 */}
<View style={styles.header}>
{currentStep > 0 && (
<TouchableOpacity style={styles.backButton} onPress={handlePrevious}>
<ThemedText style={[styles.backText, { color: textColor }]}> </ThemedText>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<ThemedText style={[styles.skipText, { color: iconColor }]}></ThemedText>
</TouchableOpacity>
</View>
{/* 进度条 */}
<View style={styles.progressContainer}>
<View style={[styles.progressBackground, { backgroundColor: iconColor + '20' }]}>
<View
style={[
styles.progressBar,
{
backgroundColor: primaryColor,
width: `${((currentStep + 1) / steps.length) * 100}%`
}
]}
/>
</View>
<ThemedText style={[styles.progressText, { color: iconColor }]}>
{currentStep + 1} / {steps.length}
</ThemedText>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
{/* 标题区域 */}
<View style={styles.titleContainer}>
<ThemedText type="title" style={styles.title}>
{steps[currentStep].title}
</ThemedText>
<ThemedText style={[styles.subtitle, { color: textColor + '80' }]}>
{steps[currentStep].subtitle}
</ThemedText>
</View>
{/* 内容区域 */}
{renderStepContent()}
</ScrollView>
{/* 底部按钮 */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.nextButton,
{ backgroundColor: isStepCompleted() ? primaryColor : iconColor + '30' }
]}
onPress={handleNext}
disabled={!isStepCompleted()}
activeOpacity={0.8}
>
<Text style={[
styles.nextButtonText,
{ color: isStepCompleted() ? '#192126' : iconColor }
]}>
{currentStep === steps.length - 1 ? '完成' : '下一步'}
</Text>
</TouchableOpacity>
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: StatusBar.currentHeight || 44,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
},
backButton: {
padding: 8,
},
backText: {
fontSize: 16,
fontWeight: '500',
},
skipButton: {
padding: 8,
},
skipText: {
fontSize: 16,
fontWeight: '500',
},
progressContainer: {
paddingHorizontal: 20,
marginBottom: 32,
},
progressBackground: {
height: 4,
borderRadius: 2,
marginBottom: 8,
},
progressBar: {
height: '100%',
borderRadius: 2,
},
progressText: {
fontSize: 12,
textAlign: 'right',
},
content: {
flex: 1,
},
contentContainer: {
paddingHorizontal: 24,
paddingBottom: 24,
},
titleContainer: {
marginBottom: 48,
},
title: {
textAlign: 'center',
marginBottom: 16,
fontWeight: '700',
},
subtitle: {
fontSize: 16,
textAlign: 'center',
lineHeight: 24,
},
optionsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 20,
},
genderOption: {
width: width * 0.35,
height: 120,
borderRadius: 16,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
genderIcon: {
fontSize: 48,
marginBottom: 8,
},
genderText: {
fontSize: 16,
fontWeight: '600',
},
inputContainer: {
alignItems: 'center',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 16,
width: width * 0.6,
height: 56,
},
numberInput: {
flex: 1,
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
},
unitText: {
fontSize: 16,
fontWeight: '500',
marginLeft: 8,
},
buttonContainer: {
paddingHorizontal: 24,
paddingBottom: 48,
},
nextButton: {
height: 56,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
nextButtonText: {
fontSize: 18,
fontWeight: '600',
},
});