feat: 更新用户资料编辑功能及相关组件
- 在 EditProfileScreen 中新增活动水平字段,支持用户设置和保存活动水平 - 更新个人信息卡片,增加活动水平的展示和编辑功能 - 在 ProfileCard 组件中优化样式,提升用户体验 - 更新 package.json 和 package-lock.json,新增 @react-native-picker/picker 依赖 - 在多个组件中引入 expo-image,优化图片加载和展示效果
This commit is contained in:
@@ -8,8 +8,9 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Alert, Image, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
|
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
|
||||||
@@ -110,8 +111,11 @@ export default function PersonalScreen() {
|
|||||||
<View style={styles.userInfoContainer}>
|
<View style={styles.userInfoContainer}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: userProfile.avatar || DEFAULT_AVATAR_URL }}
|
source={userProfile.avatar || DEFAULT_AVATAR_URL}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
|
contentFit="cover"
|
||||||
|
transition={200}
|
||||||
|
cachePolicy="memory-disk"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.userDetails}>
|
<View style={styles.userDetails}>
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ import { fetchMyProfile } from '@/store/userSlice';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
Image,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -30,8 +31,6 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
type WeightUnit = 'kg' | 'lb';
|
|
||||||
type HeightUnit = 'cm' | 'ft';
|
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -42,6 +41,7 @@ interface UserProfile {
|
|||||||
height?: number; // cm
|
height?: number; // cm
|
||||||
avatarUri?: string | null;
|
avatarUri?: string | null;
|
||||||
avatarBase64?: string | null; // 兼容旧逻辑(不再上报)
|
avatarBase64?: string | null; // 兼容旧逻辑(不再上报)
|
||||||
|
activityLevel?: number; // 活动水平 1-4
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = '@user_profile';
|
const STORAGE_KEY = '@user_profile';
|
||||||
@@ -68,6 +68,7 @@ export default function EditProfileScreen() {
|
|||||||
weight: undefined,
|
weight: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
avatarUri: null,
|
avatarUri: null,
|
||||||
|
activityLevel: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [weightInput, setWeightInput] = useState<string>('');
|
const [weightInput, setWeightInput] = useState<string>('');
|
||||||
@@ -76,6 +77,8 @@ export default function EditProfileScreen() {
|
|||||||
// 出生日期选择器
|
// 出生日期选择器
|
||||||
const [datePickerVisible, setDatePickerVisible] = useState(false);
|
const [datePickerVisible, setDatePickerVisible] = useState(false);
|
||||||
const [pickerDate, setPickerDate] = useState<Date>(new Date());
|
const [pickerDate, setPickerDate] = useState<Date>(new Date());
|
||||||
|
const [editingField, setEditingField] = useState<string | null>(null);
|
||||||
|
const [tempValue, setTempValue] = useState<string>('');
|
||||||
|
|
||||||
// 输入框字符串
|
// 输入框字符串
|
||||||
|
|
||||||
@@ -93,6 +96,7 @@ export default function EditProfileScreen() {
|
|||||||
weight: undefined,
|
weight: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
avatarUri: null,
|
avatarUri: null,
|
||||||
|
activityLevel: undefined,
|
||||||
};
|
};
|
||||||
if (fromOnboarding) {
|
if (fromOnboarding) {
|
||||||
try {
|
try {
|
||||||
@@ -152,34 +156,20 @@ export default function EditProfileScreen() {
|
|||||||
: prev.avatarUri,
|
: prev.avatarUri,
|
||||||
weight: accountProfile?.weight ?? prev.weight ?? undefined,
|
weight: accountProfile?.weight ?? prev.weight ?? undefined,
|
||||||
height: accountProfile?.height ?? prev.height ?? undefined,
|
height: accountProfile?.height ?? prev.height ?? undefined,
|
||||||
|
activityLevel: accountProfile?.activityLevel ?? prev.activityLevel ?? undefined,
|
||||||
}));
|
}));
|
||||||
}, [accountProfile]);
|
}, [accountProfile]);
|
||||||
|
|
||||||
const textColor = colors.text;
|
const textColor = colors.text;
|
||||||
const placeholderColor = colors.icon;
|
const placeholderColor = colors.icon;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSaveWithProfile = async (profileData: UserProfile) => {
|
||||||
try {
|
try {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
Alert.alert('未登录', '请先登录后再尝试保存');
|
Alert.alert('未登录', '请先登录后再尝试保存');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const next: UserProfile = { ...profile };
|
const next: UserProfile = { ...profileData };
|
||||||
|
|
||||||
// 将当前输入同步为公制(固定 kg/cm)
|
|
||||||
const w = parseFloat(weightInput);
|
|
||||||
if (!isNaN(w)) {
|
|
||||||
next.weight = w;
|
|
||||||
} else {
|
|
||||||
next.weight = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const h = parseFloat(heightInput);
|
|
||||||
if (!isNaN(h)) {
|
|
||||||
next.height = h;
|
|
||||||
} else {
|
|
||||||
next.height = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||||
|
|
||||||
@@ -194,6 +184,7 @@ export default function EditProfileScreen() {
|
|||||||
weight: next.weight || undefined,
|
weight: next.weight || undefined,
|
||||||
height: next.height || undefined,
|
height: next.height || undefined,
|
||||||
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
|
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
|
||||||
|
activityLevel: next.activityLevel || undefined,
|
||||||
});
|
});
|
||||||
// 拉取最新用户信息,刷新全局状态
|
// 拉取最新用户信息,刷新全局状态
|
||||||
await dispatch(fetchMyProfile() as any);
|
await dispatch(fetchMyProfile() as any);
|
||||||
@@ -201,16 +192,11 @@ export default function EditProfileScreen() {
|
|||||||
// 接口失败不阻断本地保存
|
// 接口失败不阻断本地保存
|
||||||
console.warn('更新用户信息失败', e?.message || e);
|
console.warn('更新用户信息失败', e?.message || e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.alert('已保存', '个人资料已更新。');
|
|
||||||
router.back();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('保存失败', '请稍后重试');
|
Alert.alert('保存失败', '请稍后重试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 不再需要单位切换
|
|
||||||
|
|
||||||
const { upload, uploading } = useCosUpload();
|
const { upload, uploading } = useCosUpload();
|
||||||
|
|
||||||
// 出生日期选择器交互
|
// 出生日期选择器交互
|
||||||
@@ -221,14 +207,19 @@ export default function EditProfileScreen() {
|
|||||||
setDatePickerVisible(true);
|
setDatePickerVisible(true);
|
||||||
};
|
};
|
||||||
const closeDatePicker = () => setDatePickerVisible(false);
|
const closeDatePicker = () => setDatePickerVisible(false);
|
||||||
const onConfirmDate = (date: Date) => {
|
const onConfirmDate = async (date: Date) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
const picked = new Date(date);
|
const picked = new Date(date);
|
||||||
picked.setHours(0, 0, 0, 0);
|
picked.setHours(0, 0, 0, 0);
|
||||||
const finalDate = picked > today ? today : picked;
|
const finalDate = picked > today ? today : picked;
|
||||||
|
|
||||||
|
const updatedProfile = { ...profile, birthDate: finalDate.toISOString() };
|
||||||
setProfile((p) => ({ ...p, birthDate: finalDate.toISOString() }));
|
setProfile((p) => ({ ...p, birthDate: finalDate.toISOString() }));
|
||||||
closeDatePicker();
|
closeDatePicker();
|
||||||
|
|
||||||
|
// 保存到后端
|
||||||
|
await handleSaveWithProfile(updatedProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pickAvatarFromLibrary = async () => {
|
const pickAvatarFromLibrary = async () => {
|
||||||
@@ -270,21 +261,21 @@ export default function EditProfileScreen() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||||
<StatusBar barStyle={'dark-content'} />
|
<StatusBar barStyle={'dark-content'} />
|
||||||
|
|
||||||
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
|
{/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */}
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="编辑资料"
|
title="编辑资料"
|
||||||
onBack={() => router.back()}
|
onBack={() => router.back()}
|
||||||
withSafeTop={false}
|
withSafeTop={false}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
||||||
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
||||||
|
|
||||||
{/* 头像(带相机蒙层,点击从相册选择) */}
|
{/* 头像(带相机蒙层,点击从相册选择) */}
|
||||||
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 32 }}>
|
||||||
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary} disabled={uploading}>
|
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary} disabled={uploading}>
|
||||||
<View style={styles.avatarCircle}>
|
<View style={styles.avatarCircle}>
|
||||||
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg' }} style={styles.avatarImage} />
|
<Image source={{ uri: profile.avatarUri || 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg' }} style={styles.avatarImage} />
|
||||||
@@ -300,87 +291,132 @@ export default function EditProfileScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 姓名 */}
|
{/* 用户信息卡片列表 */}
|
||||||
<FieldLabel text="姓名" />
|
<View style={styles.cardContainer}>
|
||||||
<View style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
|
{/* 姓名 */}
|
||||||
<TextInput
|
<ProfileCard
|
||||||
style={[styles.textInput, { color: textColor }]}
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-name.png'
|
||||||
placeholder="填写姓名(可选)"
|
title="昵称"
|
||||||
placeholderTextColor={placeholderColor}
|
value={profile.name || '今晚要吃肉'}
|
||||||
value={profile.name}
|
onPress={() => {
|
||||||
onChangeText={(t) => setProfile((p) => ({ ...p, name: t }))}
|
setTempValue(profile.name || '');
|
||||||
|
setEditingField('name');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 性别 */}
|
||||||
|
<ProfileCard
|
||||||
|
icon="body"
|
||||||
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-sex.png"
|
||||||
|
iconColor="#FF6B9D"
|
||||||
|
title="性别"
|
||||||
|
value={profile.gender === 'male' ? '男' : profile.gender === 'female' ? '女' : '未设置'}
|
||||||
|
onPress={() => {
|
||||||
|
setEditingField('gender');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 身高 */}
|
||||||
|
<ProfileCard
|
||||||
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-height.png"
|
||||||
|
title="身高"
|
||||||
|
value={profile.height ? `${Math.round(profile.height)}厘米` : '170厘米'}
|
||||||
|
onPress={() => {
|
||||||
|
setTempValue(profile.height ? String(Math.round(profile.height)) : '170');
|
||||||
|
setEditingField('height');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 体重 */}
|
||||||
|
<ProfileCard
|
||||||
|
iconUri="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-weight.png"
|
||||||
|
title="体重"
|
||||||
|
value={profile.weight ? `${round(profile.weight, 1)}公斤` : '55公斤'}
|
||||||
|
onPress={() => {
|
||||||
|
setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55');
|
||||||
|
setEditingField('weight');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 活动水平 */}
|
||||||
|
<ProfileCard
|
||||||
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-activity.png'
|
||||||
|
title="活动水平"
|
||||||
|
value={(() => {
|
||||||
|
switch (profile.activityLevel) {
|
||||||
|
case 1: return '久坐';
|
||||||
|
case 2: return '轻度活跃';
|
||||||
|
case 3: return '中度活跃';
|
||||||
|
case 4: return '非常活跃';
|
||||||
|
default: return '久坐';
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
onPress={() => {
|
||||||
|
setEditingField('activity');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 出生日期 */}
|
||||||
|
<ProfileCard
|
||||||
|
iconUri='https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-edit-birth.png'
|
||||||
|
title="出生日期"
|
||||||
|
value={profile.birthDate ? (() => {
|
||||||
|
try {
|
||||||
|
const d = new Date(profile.birthDate);
|
||||||
|
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
|
||||||
|
} catch {
|
||||||
|
return '1995年1月1日';
|
||||||
|
}
|
||||||
|
})() : '1995年1月1日'}
|
||||||
|
onPress={() => {
|
||||||
|
openDatePicker();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 校验勾无需强制,仅装饰 */}
|
|
||||||
{!!profile.name && <Text style={{ color: '#C4C4C4' }}>✓</Text>}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 体重(kg) */}
|
{/* 编辑弹窗 */}
|
||||||
<FieldLabel text="体重" />
|
<EditModal
|
||||||
<View style={styles.row}>
|
visible={!!editingField}
|
||||||
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
|
field={editingField}
|
||||||
<TextInput
|
value={tempValue}
|
||||||
style={[styles.textInput, { color: textColor }]}
|
profile={profile}
|
||||||
placeholder={'输入体重'}
|
onClose={() => {
|
||||||
placeholderTextColor={placeholderColor}
|
setEditingField(null);
|
||||||
keyboardType="numeric"
|
setTempValue('');
|
||||||
value={weightInput}
|
}}
|
||||||
onChangeText={setWeightInput}
|
onSave={async (field, value) => {
|
||||||
/>
|
// 先更新本地状态
|
||||||
<Text style={{ color: '#5E6468', marginLeft: 6 }}>kg</Text>
|
let updatedProfile = { ...profile };
|
||||||
</View>
|
if (field === 'name') {
|
||||||
</View>
|
updatedProfile.name = value;
|
||||||
|
setProfile(p => ({ ...p, name: value }));
|
||||||
|
} else if (field === 'gender') {
|
||||||
|
updatedProfile.gender = value as 'male' | 'female';
|
||||||
|
setProfile(p => ({ ...p, gender: value as 'male' | 'female' }));
|
||||||
|
} else if (field === 'height') {
|
||||||
|
updatedProfile.height = parseFloat(value) || undefined;
|
||||||
|
setProfile(p => ({ ...p, height: parseFloat(value) || undefined }));
|
||||||
|
setHeightInput(value);
|
||||||
|
} else if (field === 'weight') {
|
||||||
|
updatedProfile.weight = parseFloat(value) || undefined;
|
||||||
|
setProfile(p => ({ ...p, weight: parseFloat(value) || undefined }));
|
||||||
|
setWeightInput(value);
|
||||||
|
} else if (field === 'activity') {
|
||||||
|
const activityLevel = parseInt(value) as number;
|
||||||
|
updatedProfile.activityLevel = activityLevel;
|
||||||
|
setProfile(p => ({ ...p, activityLevel: activityLevel }));
|
||||||
|
}
|
||||||
|
|
||||||
{/* 身高(cm) */}
|
setEditingField(null);
|
||||||
<FieldLabel text="身高" />
|
setTempValue('');
|
||||||
<View style={styles.row}>
|
|
||||||
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.textInput, { color: textColor }]}
|
|
||||||
placeholder={'输入身高'}
|
|
||||||
placeholderTextColor={placeholderColor}
|
|
||||||
keyboardType="numeric"
|
|
||||||
value={heightInput}
|
|
||||||
onChangeText={setHeightInput}
|
|
||||||
/>
|
|
||||||
<Text style={{ color: '#5E6468', marginLeft: 6 }}>cm</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 性别 */}
|
// 使用更新后的数据保存
|
||||||
<FieldLabel text="性别" />
|
await handleSaveWithProfile(updatedProfile);
|
||||||
<View style={[styles.selector, { borderColor: '#E0E0E0' }]}>
|
}}
|
||||||
<TouchableOpacity
|
colors={colors}
|
||||||
style={[styles.selectorItem, profile.gender === 'female' && { backgroundColor: colors.primary + '40' }]}
|
textColor={textColor}
|
||||||
onPress={() => setProfile((p) => ({ ...p, gender: 'female' }))}
|
placeholderColor={placeholderColor}
|
||||||
>
|
/>
|
||||||
<Text style={styles.selectorEmoji}>♀</Text>
|
|
||||||
<Text style={styles.selectorText}>女性</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.selectorItem, profile.gender === 'male' && { backgroundColor: colors.primary + '40' }]}
|
|
||||||
onPress={() => setProfile((p) => ({ ...p, gender: 'male' }))}
|
|
||||||
>
|
|
||||||
<Text style={styles.selectorEmoji}>♂</Text>
|
|
||||||
<Text style={styles.selectorText}>男性</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</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
|
<Modal
|
||||||
@@ -415,32 +451,201 @@ export default function EditProfileScreen() {
|
|||||||
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
|
||||||
<Text style={styles.modalBtnText}>取消</Text>
|
<Text style={styles.modalBtnText}>取消</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={() => { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
<Pressable onPress={() => {
|
||||||
|
onConfirmDate(pickerDate);
|
||||||
|
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
||||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 保存按钮 */}
|
|
||||||
<TouchableOpacity onPress={handleSave} activeOpacity={0.9} style={[styles.saveBtn, { backgroundColor: colors.primary }]}>
|
|
||||||
<Text style={{ color: colors.onPrimary, fontSize: 18, fontWeight: '700' }}>保存</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLabel({ text }: { text: string }) {
|
function ProfileCard({ icon, iconUri, iconColor, title, value, onPress }: {
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
iconUri?: string;
|
||||||
|
iconColor?: string;
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Text style={{ fontSize: 14, color: '#5E6468', marginBottom: 8, marginTop: 10 }}>{text}</Text>
|
<TouchableOpacity onPress={onPress} style={styles.profileCard} activeOpacity={0.8}>
|
||||||
|
<View style={styles.profileCardLeft}>
|
||||||
|
<View style={[styles.iconContainer]}>
|
||||||
|
{iconUri ? <Image
|
||||||
|
source={{ uri: iconUri }}
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
cachePolicy="memory-disk" /> : <Ionicons name={icon} size={20} color={iconColor} />}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.profileCardTitle}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.profileCardRight}>
|
||||||
|
<Text style={styles.profileCardValue}>{value}</Text>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color="#C7C7CC" />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单位切换组件已移除(固定 kg/cm)
|
function EditModal({ visible, field, value, profile, onClose, onSave, colors, textColor, placeholderColor }: {
|
||||||
|
visible: boolean;
|
||||||
|
field: string | null;
|
||||||
|
value: string;
|
||||||
|
profile: UserProfile;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (field: string, value: string) => void;
|
||||||
|
colors: any;
|
||||||
|
textColor: string;
|
||||||
|
placeholderColor: string;
|
||||||
|
}) {
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
const [selectedGender, setSelectedGender] = useState(profile.gender || 'female');
|
||||||
|
const [selectedActivity, setSelectedActivity] = useState(profile.activityLevel || 1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
if (field === 'activity') {
|
||||||
|
setSelectedActivity(profile.activityLevel || 1);
|
||||||
|
}
|
||||||
|
}, [value, field, profile.activityLevel]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (field) {
|
||||||
|
case 'name':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.modalTitle}>昵称</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||||
|
placeholder="输入昵称"
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
value={inputValue}
|
||||||
|
onChangeText={setInputValue}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
case 'gender':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.modalTitle}>性别</Text>
|
||||||
|
<View style={styles.genderSelector}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.genderOption, selectedGender === 'female' && { backgroundColor: colors.primary + '20' }]}
|
||||||
|
onPress={() => setSelectedGender('female')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.genderEmoji, selectedGender === 'female' && { color: colors.primary }]}>♀</Text>
|
||||||
|
<Text style={[styles.genderText, selectedGender === 'female' && { color: colors.primary }]}>女性</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.genderOption, selectedGender === 'male' && { backgroundColor: colors.primary + '20' }]}
|
||||||
|
onPress={() => setSelectedGender('male')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.genderEmoji, selectedGender === 'male' && { color: colors.primary }]}>♂</Text>
|
||||||
|
<Text style={[styles.genderText, selectedGender === 'male' && { color: colors.primary }]}>男性</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
case 'height':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.modalTitle}>身高</Text>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={inputValue}
|
||||||
|
onValueChange={setInputValue}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 101 }, (_, i) => 120 + i).map(height => (
|
||||||
|
<Picker.Item key={height} label={`${height}厘米`} value={String(height)} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
case 'weight':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.modalTitle}>体重</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.modalInput, { color: textColor, borderColor: '#E0E0E0' }]}
|
||||||
|
placeholder="输入体重"
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
value={inputValue}
|
||||||
|
onChangeText={setInputValue}
|
||||||
|
keyboardType="numeric"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Text style={styles.unitText}>公斤 (kg)</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
case 'activity':
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.modalTitle}>活动水平</Text>
|
||||||
|
<View style={styles.activitySelector}>
|
||||||
|
{[
|
||||||
|
{ key: 1, label: '久坐', desc: '很少运动' },
|
||||||
|
{ key: 2, label: '轻度活跃', desc: '每周1-3次运动' },
|
||||||
|
{ key: 3, label: '中度活跃', desc: '每周3-5次运动' },
|
||||||
|
{ key: 4, label: '非常活跃', desc: '每周6-7次运动' },
|
||||||
|
].map(item => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.key}
|
||||||
|
style={[styles.activityOption, selectedActivity === item.key && { backgroundColor: colors.primary + '20' }]}
|
||||||
|
onPress={() => setSelectedActivity(item.key)}
|
||||||
|
>
|
||||||
|
<View style={styles.activityContent}>
|
||||||
|
<Text style={[styles.activityLabel, selectedActivity === item.key && { color: colors.primary }]}>{item.label}</Text>
|
||||||
|
<Text style={styles.activityDesc}>{item.desc}</Text>
|
||||||
|
</View>
|
||||||
|
{selectedActivity === item.key && <Ionicons name="checkmark" size={20} color={colors.primary} />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||||
|
<Pressable style={styles.modalBackdrop} onPress={onClose} />
|
||||||
|
<View style={styles.editModalSheet}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
{renderContent()}
|
||||||
|
<View style={styles.modalButtons}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.modalCancelBtn}>
|
||||||
|
<Text style={styles.modalCancelText}>取消</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (field === 'gender') {
|
||||||
|
onSave(field, selectedGender);
|
||||||
|
} else if (field === 'activity') {
|
||||||
|
onSave(field, String(selectedActivity));
|
||||||
|
} else {
|
||||||
|
onSave(field!, inputValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalSaveText, { color: colors.onPrimary }]}>保存</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function round(n: number, d = 0) { const p = Math.pow(10, d); return Math.round(n * p) / p; }
|
function round(n: number, d = 0) { const p = Math.pow(10, d); return Math.round(n * p) / p; }
|
||||||
|
|
||||||
@@ -481,71 +686,51 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
borderRadius: 60,
|
borderRadius: 60,
|
||||||
},
|
},
|
||||||
inputWrapper: {
|
cardContainer: {
|
||||||
height: 52,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
row: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
flex1: { flex: 1 },
|
|
||||||
segmented: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 4,
|
|
||||||
backgroundColor: '#EFEFEF',
|
|
||||||
},
|
|
||||||
segmentBtn: {
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
minWidth: 64,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
selector: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 12,
|
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
borderWidth: 1,
|
borderRadius: 16,
|
||||||
borderRadius: 12,
|
overflow: 'hidden',
|
||||||
padding: 8,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
selectorItem: {
|
profileCard: {
|
||||||
flex: 1,
|
flexDirection: 'row',
|
||||||
height: 48,
|
|
||||||
borderRadius: 10,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#F0F0F0',
|
||||||
},
|
},
|
||||||
selectorEmoji: { fontSize: 16, marginBottom: 2 },
|
profileCardLeft: {
|
||||||
selectorText: { fontSize: 15, fontWeight: '600' },
|
flexDirection: 'row',
|
||||||
saveBtn: {
|
alignItems: 'center',
|
||||||
marginTop: 24,
|
flex: 1,
|
||||||
height: 56,
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
shadowColor: '#000',
|
marginRight: 12,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
},
|
||||||
shadowOpacity: 0.1,
|
profileCardTitle: {
|
||||||
shadowRadius: 4,
|
fontSize: 16,
|
||||||
elevation: 4,
|
color: '#333333',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
profileCardRight: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
profileCardValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666666',
|
||||||
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
modalBackdrop: {
|
modalBackdrop: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
},
|
},
|
||||||
modalSheet: {
|
modalSheet: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -580,6 +765,129 @@ const styles = StyleSheet.create({
|
|||||||
color: '#0F172A',
|
color: '#0F172A',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
editModalSheet: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 40,
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
modalHandle: {
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: '#E0E0E0',
|
||||||
|
borderRadius: 2,
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333333',
|
||||||
|
marginBottom: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
modalInput: {
|
||||||
|
height: 50,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
unitText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#666666',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
genderSelector: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 16,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
genderOption: {
|
||||||
|
flex: 1,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#F8F8F8',
|
||||||
|
},
|
||||||
|
genderEmoji: {
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
genderText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
pickerContainer: {
|
||||||
|
height: 200,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
picker: {
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
activitySelector: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
activityOption: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
backgroundColor: '#F8F8F8',
|
||||||
|
},
|
||||||
|
activityContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
activityLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#333333',
|
||||||
|
},
|
||||||
|
activityDesc: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#666666',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
modalButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
modalCancelBtn: {
|
||||||
|
flex: 1,
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: '#F0F0F0',
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
modalCancelText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#666666',
|
||||||
|
},
|
||||||
|
modalSaveBtn: {
|
||||||
|
flex: 1,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
modalSaveText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TaskListItem } from '@/types/goals';
|
import { TaskListItem } from '@/types/goals';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
@@ -35,7 +36,11 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
|||||||
style={styles.goalsIconButton}
|
style={styles.goalsIconButton}
|
||||||
onPress={handleNavigateToGoals}
|
onPress={handleNavigateToGoals}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="flag" size={18} color="#7A5AF8" />
|
<Image
|
||||||
|
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-goal-edit.png' }}
|
||||||
|
style={{ width: 18, height: 18 }}
|
||||||
|
cachePolicy="memory-disk"
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
@@ -117,6 +122,8 @@ const styles = StyleSheet.create({
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
goalsIconButton: {
|
goalsIconButton: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|||||||
@@ -1740,6 +1740,8 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- RNCPicker (2.11.1):
|
||||||
|
- React-Core
|
||||||
- RNDateTimePicker (8.4.4):
|
- RNDateTimePicker (8.4.4):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNDeviceInfo (14.0.4):
|
- RNDeviceInfo (14.0.4):
|
||||||
@@ -2058,6 +2060,7 @@ DEPENDENCIES:
|
|||||||
- RNAppleHealthKit (from `../node_modules/react-native-health`)
|
- RNAppleHealthKit (from `../node_modules/react-native-health`)
|
||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||||
|
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
|
||||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||||
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
||||||
@@ -2284,6 +2287,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNCMaskedView:
|
RNCMaskedView:
|
||||||
:path: "../node_modules/@react-native-masked-view/masked-view"
|
:path: "../node_modules/@react-native-masked-view/masked-view"
|
||||||
|
RNCPicker:
|
||||||
|
:path: "../node_modules/@react-native-picker/picker"
|
||||||
RNDateTimePicker:
|
RNDateTimePicker:
|
||||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||||
RNDeviceInfo:
|
RNDeviceInfo:
|
||||||
@@ -2414,6 +2419,7 @@ SPEC CHECKSUMS:
|
|||||||
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
||||||
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||||
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
||||||
|
RNCPicker: da0f1c9411208c1ca52bc98383db54a06e0a3862
|
||||||
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
||||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||||
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-native-community/datetimepicker": "^8.4.4",
|
"@react-native-community/datetimepicker": "^8.4.4",
|
||||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||||
|
"@react-native-picker/picker": "^2.11.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
@@ -2920,6 +2921,19 @@
|
|||||||
"react-native": ">=0.57"
|
"react-native": ">=0.57"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-picker/picker": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@react-native-picker/picker/-/picker-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.79.5",
|
"version": "0.79.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz",
|
||||||
@@ -7115,7 +7129,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/expo-image": {
|
"node_modules/expo-image": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.4.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/expo-image/-/expo-image-2.4.0.tgz",
|
||||||
"integrity": "sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==",
|
"integrity": "sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-native-community/datetimepicker": "^8.4.4",
|
"@react-native-community/datetimepicker": "^8.4.4",
|
||||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||||
|
"@react-native-picker/picker": "^2.11.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type UpdateUserDto = {
|
|||||||
pilatesPurposes?: string[];
|
pilatesPurposes?: string[];
|
||||||
weight?: number;
|
weight?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
activityLevel?: number; // 活动水平 1-4
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updateUser(dto: UpdateUserDto): Promise<Record<string, any>> {
|
export async function updateUser(dto: UpdateUserDto): Promise<Record<string, any>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user