- 将应用版本更新至 1.0.3,修改相关配置文件 - 强制全局使用浅色主题,确保一致的用户体验 - 在训练计划功能中新增激活计划的 API 接口,支持用户激活训练计划 - 优化打卡功能,支持自动同步打卡记录至服务器 - 更新样式以适应新功能的展示和交互
427 lines
11 KiB
TypeScript
427 lines
11 KiB
TypeScript
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',
|
||
},
|
||
});
|