feat: 更新应用版本和主题设置
- 将应用版本更新至 1.0.3,修改相关配置文件 - 强制全局使用浅色主题,确保一致的用户体验 - 在训练计划功能中新增激活计划的 API 接口,支持用户激活训练计划 - 优化打卡功能,支持自动同步打卡记录至服务器 - 更新样式以适应新功能的展示和交互
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { Colors, palette } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
@@ -7,6 +7,7 @@ import { updateUser as updateUserApi } from '@/services/users';
|
||||
import { fetchMyProfile } from '@/store/userSlice';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { router } from 'expo-router';
|
||||
@@ -16,7 +17,9 @@ import {
|
||||
Alert,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
@@ -24,7 +27,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
@@ -34,7 +37,7 @@ type HeightUnit = 'cm' | 'ft';
|
||||
interface UserProfile {
|
||||
name?: string;
|
||||
gender?: 'male' | 'female' | '';
|
||||
age?: string; // 存储为字符串,方便非必填
|
||||
birthDate?: string; // 出生日期
|
||||
// 以公制为基准存储
|
||||
weight?: number; // kg
|
||||
height?: number; // cm
|
||||
@@ -63,7 +66,7 @@ export default function EditProfileScreen() {
|
||||
const [profile, setProfile] = useState<UserProfile>({
|
||||
name: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
birthDate: '',
|
||||
weight: undefined,
|
||||
height: undefined,
|
||||
avatarUri: null,
|
||||
@@ -72,6 +75,10 @@ export default function EditProfileScreen() {
|
||||
const [weightInput, setWeightInput] = useState<string>('');
|
||||
const [heightInput, setHeightInput] = useState<string>('');
|
||||
|
||||
// 出生日期选择器
|
||||
const [datePickerVisible, setDatePickerVisible] = useState(false);
|
||||
const [pickerDate, setPickerDate] = useState<Date>(new Date());
|
||||
|
||||
// 输入框字符串
|
||||
|
||||
// 从本地存储加载(身高/体重等本地字段)
|
||||
@@ -84,7 +91,7 @@ export default function EditProfileScreen() {
|
||||
let next: UserProfile = {
|
||||
name: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
birthDate: '',
|
||||
weight: undefined,
|
||||
height: undefined,
|
||||
avatarUri: null,
|
||||
@@ -95,7 +102,7 @@ export default function EditProfileScreen() {
|
||||
|
||||
if (o?.weight) next.weight = parseFloat(o.weight) || undefined;
|
||||
if (o?.height) next.height = parseFloat(o.height) || undefined;
|
||||
if (o?.age) next.age = String(o.age);
|
||||
if (o?.birthDate) next.birthDate = o.birthDate;
|
||||
if (o?.gender) next.gender = o.gender;
|
||||
} catch { }
|
||||
}
|
||||
@@ -207,6 +214,24 @@ export default function EditProfileScreen() {
|
||||
|
||||
const { upload, uploading } = useCosUpload();
|
||||
|
||||
// 出生日期选择器交互
|
||||
const openDatePicker = () => {
|
||||
const base = profile.birthDate ? new Date(profile.birthDate) : new Date();
|
||||
base.setHours(0, 0, 0, 0);
|
||||
setPickerDate(base);
|
||||
setDatePickerVisible(true);
|
||||
};
|
||||
const closeDatePicker = () => setDatePickerVisible(false);
|
||||
const onConfirmDate = (date: Date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const picked = new Date(date);
|
||||
picked.setHours(0, 0, 0, 0);
|
||||
const finalDate = picked > today ? today : picked;
|
||||
setProfile((p) => ({ ...p, birthDate: finalDate.toISOString() }));
|
||||
closeDatePicker();
|
||||
};
|
||||
|
||||
const pickAvatarFromLibrary = async () => {
|
||||
try {
|
||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
@@ -245,7 +270,7 @@ export default function EditProfileScreen() {
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||
<StatusBar barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={'dark-content'} />
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
||||
<HeaderBar title="编辑资料" onBack={() => router.back()} withSafeTop={false} transparent />
|
||||
@@ -332,18 +357,63 @@ export default function EditProfileScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 年龄 */}
|
||||
<FieldLabel text="年龄" />
|
||||
<View style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { color: textColor }]}
|
||||
placeholder="填写年龄(可选)"
|
||||
placeholderTextColor={placeholderColor}
|
||||
keyboardType="number-pad"
|
||||
value={profile.age ?? ''}
|
||||
onChangeText={(t) => setProfile((p) => ({ ...p, age: t }))}
|
||||
/>
|
||||
</View>
|
||||
{/* 出生日期 */}
|
||||
<FieldLabel text="出生日期" />
|
||||
<TouchableOpacity onPress={openDatePicker} activeOpacity={0.8} style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
|
||||
<Text style={[styles.textInput, { color: profile.birthDate ? textColor : placeholderColor }]}>
|
||||
{profile.birthDate
|
||||
? (() => {
|
||||
try {
|
||||
const d = new Date(profile.birthDate);
|
||||
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }).format(d);
|
||||
} catch {
|
||||
return profile.birthDate;
|
||||
}
|
||||
})()
|
||||
: '选择出生日期(可选)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 出生日期选择器弹窗 */}
|
||||
<Modal
|
||||
visible={datePickerVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={closeDatePicker}
|
||||
>
|
||||
<Pressable style={styles.modalBackdrop} onPress={closeDatePicker} />
|
||||
<View style={styles.modalSheet}>
|
||||
<DateTimePicker
|
||||
value={pickerDate}
|
||||
mode="date"
|
||||
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
|
||||
minimumDate={new Date(1900, 0, 1)}
|
||||
maximumDate={new Date()}
|
||||
{...(Platform.OS === 'ios' ? { locale: 'zh-CN' } : {})}
|
||||
onChange={(event, date) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (date) setPickerDate(date);
|
||||
} else {
|
||||
if (event.type === 'set' && date) {
|
||||
onConfirmDate(date);
|
||||
} else {
|
||||
closeDatePicker();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{Platform.OS === 'ios' && (
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
||||
<Text style={styles.modalBtnText}>取消</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<TouchableOpacity onPress={handleSave} activeOpacity={0.9} style={[styles.saveBtn, { backgroundColor: colors.primary }]}>
|
||||
@@ -465,6 +535,43 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
modalBackdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
},
|
||||
modalSheet: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
padding: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 8,
|
||||
gap: 12,
|
||||
},
|
||||
modalBtn: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
modalBtnPrimary: {
|
||||
backgroundColor: palette.primary,
|
||||
},
|
||||
modalBtnText: {
|
||||
color: '#334155',
|
||||
fontWeight: '700',
|
||||
},
|
||||
modalBtnTextPrimary: {
|
||||
color: '#0F172A',
|
||||
fontWeight: '700',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
Reference in New Issue
Block a user