From a6dbe7c723c00623af791434b842a1b10e1ba5a1 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Wed, 27 Aug 2025 09:59:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=B5=84=E6=96=99=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 EditProfileScreen 中新增活动水平字段,支持用户设置和保存活动水平 - 更新个人信息卡片,增加活动水平的展示和编辑功能 - 在 ProfileCard 组件中优化样式,提升用户体验 - 更新 package.json 和 package-lock.json,新增 @react-native-picker/picker 依赖 - 在多个组件中引入 expo-image,优化图片加载和展示效果 --- app/(tabs)/personal.tsx | 8 +- app/profile/edit.tsx | 654 +++++++++++++++++++++++--------- components/TaskProgressCard.tsx | 9 +- ios/Podfile.lock | 6 + package-lock.json | 16 +- package.json | 1 + services/users.ts | 1 + 7 files changed, 518 insertions(+), 177 deletions(-) diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index a8dbf40..decc358 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -8,8 +8,9 @@ import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/sto import { Ionicons } from '@expo/vector-icons'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; +import { Image } from 'expo-image'; 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'; const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg'; @@ -110,8 +111,11 @@ export default function PersonalScreen() { diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx index 95d43b7..845ea6b 100644 --- a/app/profile/edit.tsx +++ b/app/profile/edit.tsx @@ -8,14 +8,15 @@ 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 { Picker } from '@react-native-picker/picker'; import { useFocusEffect } from '@react-navigation/native'; +import { Image } from 'expo-image'; import * as ImagePicker from 'expo-image-picker'; import { router } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, - Image, KeyboardAvoidingView, Modal, Platform, @@ -30,8 +31,6 @@ import { View, } from 'react-native'; -type WeightUnit = 'kg' | 'lb'; -type HeightUnit = 'cm' | 'ft'; interface UserProfile { name?: string; @@ -42,6 +41,7 @@ interface UserProfile { height?: number; // cm avatarUri?: string | null; avatarBase64?: string | null; // 兼容旧逻辑(不再上报) + activityLevel?: number; // 活动水平 1-4 } const STORAGE_KEY = '@user_profile'; @@ -68,6 +68,7 @@ export default function EditProfileScreen() { weight: undefined, height: undefined, avatarUri: null, + activityLevel: undefined, }); const [weightInput, setWeightInput] = useState(''); @@ -76,6 +77,8 @@ export default function EditProfileScreen() { // 出生日期选择器 const [datePickerVisible, setDatePickerVisible] = useState(false); const [pickerDate, setPickerDate] = useState(new Date()); + const [editingField, setEditingField] = useState(null); + const [tempValue, setTempValue] = useState(''); // 输入框字符串 @@ -93,6 +96,7 @@ export default function EditProfileScreen() { weight: undefined, height: undefined, avatarUri: null, + activityLevel: undefined, }; if (fromOnboarding) { try { @@ -152,34 +156,20 @@ export default function EditProfileScreen() { : prev.avatarUri, weight: accountProfile?.weight ?? prev.weight ?? undefined, height: accountProfile?.height ?? prev.height ?? undefined, + activityLevel: accountProfile?.activityLevel ?? prev.activityLevel ?? undefined, })); }, [accountProfile]); const textColor = colors.text; const placeholderColor = colors.icon; - const handleSave = async () => { + const handleSaveWithProfile = async (profileData: UserProfile) => { try { if (!userId) { Alert.alert('未登录', '请先登录后再尝试保存'); return; } - const next: UserProfile = { ...profile }; - - // 将当前输入同步为公制(固定 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; - } + const next: UserProfile = { ...profileData }; await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next)); @@ -194,6 +184,7 @@ export default function EditProfileScreen() { weight: next.weight || undefined, height: next.height || undefined, birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined, + activityLevel: next.activityLevel || undefined, }); // 拉取最新用户信息,刷新全局状态 await dispatch(fetchMyProfile() as any); @@ -201,16 +192,11 @@ export default function EditProfileScreen() { // 接口失败不阻断本地保存 console.warn('更新用户信息失败', e?.message || e); } - - Alert.alert('已保存', '个人资料已更新。'); - router.back(); } catch (e) { Alert.alert('保存失败', '请稍后重试'); } }; - // 不再需要单位切换 - const { upload, uploading } = useCosUpload(); // 出生日期选择器交互 @@ -221,14 +207,19 @@ export default function EditProfileScreen() { setDatePickerVisible(true); }; const closeDatePicker = () => setDatePickerVisible(false); - const onConfirmDate = (date: Date) => { + const onConfirmDate = async (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; + + const updatedProfile = { ...profile, birthDate: finalDate.toISOString() }; setProfile((p) => ({ ...p, birthDate: finalDate.toISOString() })); closeDatePicker(); + + // 保存到后端 + await handleSaveWithProfile(updatedProfile); }; const pickAvatarFromLibrary = async () => { @@ -270,21 +261,21 @@ export default function EditProfileScreen() { return ( - + {/* HeaderBar 放在 ScrollView 外部,确保全宽显示 */} - router.back()} - withSafeTop={false} + router.back()} + withSafeTop={false} transparent={true} variant="elevated" /> - + {/* 头像(带相机蒙层,点击从相册选择) */} - + @@ -300,87 +291,132 @@ export default function EditProfileScreen() { - {/* 姓名 */} - - - setProfile((p) => ({ ...p, name: t }))} + {/* 用户信息卡片列表 */} + + {/* 姓名 */} + { + setTempValue(profile.name || ''); + setEditingField('name'); + }} + /> + + {/* 性别 */} + { + setEditingField('gender'); + }} + /> + + {/* 身高 */} + { + setTempValue(profile.height ? String(Math.round(profile.height)) : '170'); + setEditingField('height'); + }} + /> + + {/* 体重 */} + { + setTempValue(profile.weight ? String(round(profile.weight, 1)) : '55'); + setEditingField('weight'); + }} + /> + + {/* 活动水平 */} + { + switch (profile.activityLevel) { + case 1: return '久坐'; + case 2: return '轻度活跃'; + case 3: return '中度活跃'; + case 4: return '非常活跃'; + default: return '久坐'; + } + })()} + onPress={() => { + setEditingField('activity'); + }} + /> + + {/* 出生日期 */} + { + 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 && } - {/* 体重(kg) */} - - - - - kg - - + {/* 编辑弹窗 */} + { + setEditingField(null); + setTempValue(''); + }} + onSave={async (field, value) => { + // 先更新本地状态 + let updatedProfile = { ...profile }; + if (field === 'name') { + 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) */} - - - - - cm - - + setEditingField(null); + setTempValue(''); - {/* 性别 */} - - - setProfile((p) => ({ ...p, gender: 'female' }))} - > - - 女性 - - setProfile((p) => ({ ...p, gender: 'male' }))} - > - - 男性 - - - - {/* 出生日期 */} - - - - {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; - } - })() - : '选择出生日期(可选)'} - - + // 使用更新后的数据保存 + await handleSaveWithProfile(updatedProfile); + }} + colors={colors} + textColor={textColor} + placeholderColor={placeholderColor} + /> {/* 出生日期选择器弹窗 */} 取消 - { onConfirmDate(pickerDate); }} style={[styles.modalBtn, styles.modalBtnPrimary]}> + { + onConfirmDate(pickerDate); + }} style={[styles.modalBtn, styles.modalBtnPrimary]}> 确定 )} - - {/* 保存按钮 */} - - 保存 - ); } -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 ( - {text} + + + + {iconUri ? : } + + {title} + + + {value} + + + ); } -// 单位切换组件已移除(固定 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 ( + + 昵称 + + + ); + case 'gender': + return ( + + 性别 + + setSelectedGender('female')} + > + + 女性 + + setSelectedGender('male')} + > + + 男性 + + + + ); + case 'height': + return ( + + 身高 + + + {Array.from({ length: 101 }, (_, i) => 120 + i).map(height => ( + + ))} + + + + ); + case 'weight': + return ( + + 体重 + + 公斤 (kg) + + ); + case 'activity': + return ( + + 活动水平 + + {[ + { key: 1, label: '久坐', desc: '很少运动' }, + { key: 2, label: '轻度活跃', desc: '每周1-3次运动' }, + { key: 3, label: '中度活跃', desc: '每周3-5次运动' }, + { key: 4, label: '非常活跃', desc: '每周6-7次运动' }, + ].map(item => ( + setSelectedActivity(item.key)} + > + + {item.label} + {item.desc} + + {selectedActivity === item.key && } + + ))} + + + ); + default: + return null; + } + }; + + return ( + + + + + {renderContent()} + + + 取消 + + { + if (field === 'gender') { + onSave(field, selectedGender); + } else if (field === 'activity') { + onSave(field, String(selectedActivity)); + } else { + onSave(field!, inputValue); + } + }} + style={[styles.modalSaveBtn, { backgroundColor: colors.primary }]} + > + 保存 + + + + + ); +} 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)', borderRadius: 60, }, - inputWrapper: { - 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, + cardContainer: { backgroundColor: '#FFFFFF', - borderWidth: 1, - borderRadius: 12, - padding: 8, + borderRadius: 16, + overflow: 'hidden', + marginBottom: 20, }, - selectorItem: { - flex: 1, - height: 48, - borderRadius: 10, + profileCard: { + flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'space-between', + paddingVertical: 16, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: '#F0F0F0', }, - selectorEmoji: { fontSize: 16, marginBottom: 2 }, - selectorText: { fontSize: 15, fontWeight: '600' }, - saveBtn: { - marginTop: 24, - height: 56, + profileCardLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + iconContainer: { + width: 32, + height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 4, + marginRight: 12, + }, + profileCardTitle: { + fontSize: 16, + color: '#333333', + fontWeight: '500', + }, + profileCardRight: { + flexDirection: 'row', + alignItems: 'center', + }, + profileCardValue: { + fontSize: 16, + color: '#666666', + marginRight: 8, }, modalBackdrop: { ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0,0,0,0.35)', + backgroundColor: 'rgba(0,0,0,0.4)', }, modalSheet: { position: 'absolute', @@ -580,6 +765,129 @@ const styles = StyleSheet.create({ color: '#0F172A', 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', + }, }); diff --git a/components/TaskProgressCard.tsx b/components/TaskProgressCard.tsx index ad27bd0..6d1703c 100644 --- a/components/TaskProgressCard.tsx +++ b/components/TaskProgressCard.tsx @@ -1,5 +1,6 @@ import { TaskListItem } from '@/types/goals'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import React, { ReactNode } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -35,7 +36,11 @@ export const TaskProgressCard: React.FC = ({ style={styles.goalsIconButton} onPress={handleNavigateToGoals} > - + @@ -117,6 +122,8 @@ const styles = StyleSheet.create({ gap: 8, }, goalsIconButton: { + width: 24, + height: 24, }, title: { fontSize: 20, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5c35d0d..d85aa37 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1740,6 +1740,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNCPicker (2.11.1): + - React-Core - RNDateTimePicker (8.4.4): - React-Core - RNDeviceInfo (14.0.4): @@ -2058,6 +2060,7 @@ DEPENDENCIES: - RNAppleHealthKit (from `../node_modules/react-native-health`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "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`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - RNExitApp (from `../node_modules/react-native-exit-app`) @@ -2284,6 +2287,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNCMaskedView: :path: "../node_modules/@react-native-masked-view/masked-view" + RNCPicker: + :path: "../node_modules/@react-native-picker/picker" RNDateTimePicker: :path: "../node_modules/@react-native-community/datetimepicker" RNDeviceInfo: @@ -2414,6 +2419,7 @@ SPEC CHECKSUMS: RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9 RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96 + RNCPicker: da0f1c9411208c1ca52bc98383db54a06e0a3862 RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4 diff --git a/package-lock.json b/package-lock.json index ccc83f8..8bd79fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-community/datetimepicker": "^8.4.4", "@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/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", @@ -2920,6 +2921,19 @@ "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": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -7115,7 +7129,7 @@ }, "node_modules/expo-image": { "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==", "license": "MIT", "peerDependencies": { diff --git a/package.json b/package.json index 79ff08f..9dedbbc 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-community/datetimepicker": "^8.4.4", "@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/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", diff --git a/services/users.ts b/services/users.ts index 2797316..0d6b120 100644 --- a/services/users.ts +++ b/services/users.ts @@ -13,6 +13,7 @@ export type UpdateUserDto = { pilatesPurposes?: string[]; weight?: number; height?: number; + activityLevel?: number; // 活动水平 1-4 }; export async function updateUser(dto: UpdateUserDto): Promise> {