|
|
@@ -1,7 +1,7 @@
|
|
|
-import { Image } from 'expo-image';
|
|
|
-import * as ImagePicker from 'expo-image-picker';
|
|
|
-import { useRouter } from 'expo-router';
|
|
|
-import React, { useEffect, useState } from 'react';
|
|
|
+import { Image } from "expo-image";
|
|
|
+import * as ImagePicker from "expo-image-picker";
|
|
|
+import { useRouter } from "expo-router";
|
|
|
+import React, { useEffect, useState } from "react";
|
|
|
import {
|
|
|
Alert,
|
|
|
ImageBackground,
|
|
|
@@ -12,13 +12,19 @@ import {
|
|
|
TextInput,
|
|
|
TouchableOpacity,
|
|
|
View,
|
|
|
-} from 'react-native';
|
|
|
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
+} from "react-native";
|
|
|
+import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
|
|
|
-import { Images } from '@/constants/images';
|
|
|
-import { useAuth } from '@/contexts/AuthContext';
|
|
|
-import { uploadFile } from '@/services/base';
|
|
|
-import { getUserInfo, updateAvatar, updateNickname, updateUserInfo } from '@/services/user';
|
|
|
+import { Images } from "@/constants/images";
|
|
|
+import { useAuth } from "@/contexts/AuthContext";
|
|
|
+import { uploadFile } from "@/services/base";
|
|
|
+import { getToken } from "@/services/http";
|
|
|
+import {
|
|
|
+ getUserInfo,
|
|
|
+ updateAvatar,
|
|
|
+ updateNickname,
|
|
|
+ updateUserInfo,
|
|
|
+} from "@/services/user";
|
|
|
|
|
|
interface FormData {
|
|
|
nickname: string;
|
|
|
@@ -30,10 +36,10 @@ export default function ProfileScreen() {
|
|
|
const router = useRouter();
|
|
|
const insets = useSafeAreaInsets();
|
|
|
const { refreshUser } = useAuth();
|
|
|
-
|
|
|
+
|
|
|
const [formData, setFormData] = useState<FormData>({
|
|
|
- nickname: '',
|
|
|
- avatar: '',
|
|
|
+ nickname: "",
|
|
|
+ avatar: "",
|
|
|
sex: 3,
|
|
|
});
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
@@ -47,13 +53,13 @@ export default function ProfileScreen() {
|
|
|
const res = await getUserInfo();
|
|
|
if (res) {
|
|
|
setFormData({
|
|
|
- nickname: res.nickname || '',
|
|
|
- avatar: res.avatar || '',
|
|
|
+ nickname: res.nickname || "",
|
|
|
+ avatar: res.avatar || "",
|
|
|
sex: (res as any).sex || 3,
|
|
|
});
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('获取用户信息失败:', error);
|
|
|
+ console.error("获取用户信息失败:", error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -63,80 +69,81 @@ export default function ProfileScreen() {
|
|
|
|
|
|
const handleChooseAvatar = async () => {
|
|
|
try {
|
|
|
- const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
|
+ const permissionResult =
|
|
|
+ await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
|
if (!permissionResult.granted) {
|
|
|
- Alert.alert('提示', '需要相册权限才能选择头像');
|
|
|
+ Alert.alert("提示", "需要相册权限才能选择头像");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
|
- mediaTypes: ['images'],
|
|
|
+ mediaTypes: ["images"],
|
|
|
allowsEditing: true,
|
|
|
aspect: [1, 1],
|
|
|
- quality: 0.8,
|
|
|
+ quality: 0.4, // 服务器限制1MB,降低质量确保不超限
|
|
|
});
|
|
|
|
|
|
if (!result.canceled && result.assets[0]) {
|
|
|
const imageUri = result.assets[0].uri;
|
|
|
- setFormData(prev => ({ ...prev, avatar: imageUri }));
|
|
|
+ setFormData((prev) => ({ ...prev, avatar: imageUri }));
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('选择头像失败:', error);
|
|
|
- Alert.alert('提示', '选择头像失败');
|
|
|
+ console.error("选择头像失败:", error);
|
|
|
+ Alert.alert("提示", "选择头像失败");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleSexChange = (sex: number) => {
|
|
|
- setFormData(prev => ({ ...prev, sex }));
|
|
|
+ setFormData((prev) => ({ ...prev, sex }));
|
|
|
};
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
if (!formData.nickname?.trim()) {
|
|
|
- Alert.alert('提示', '请输入昵称');
|
|
|
+ Alert.alert("提示", "请输入昵称");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
setLoading(true);
|
|
|
-
|
|
|
+
|
|
|
// 如果头像是本地文件(非http开头),需要先上传
|
|
|
let avatarUrl = formData.avatar;
|
|
|
- console.log('[profile] 头像URI:', formData.avatar?.substring(0, 100));
|
|
|
- if (formData.avatar && !formData.avatar.startsWith('http')) {
|
|
|
- const uploadedUrl = await uploadFile(formData.avatar, 'avatar');
|
|
|
- if (uploadedUrl) {
|
|
|
- avatarUrl = uploadedUrl;
|
|
|
- // 更新头像
|
|
|
+ console.log("[profile] 头像URI:", formData.avatar?.substring(0, 100));
|
|
|
+ if (formData.avatar && !formData.avatar.startsWith("http")) {
|
|
|
+ const token = getToken();
|
|
|
+ const uploadResult = await uploadFile(formData.avatar, "avatar", token || undefined);
|
|
|
+ if (typeof uploadResult === "string") {
|
|
|
+ avatarUrl = uploadResult;
|
|
|
await updateAvatar(avatarUrl);
|
|
|
} else {
|
|
|
- Alert.alert('提示', '头像上传失败');
|
|
|
+ Alert.alert("头像上传失败", uploadResult.error);
|
|
|
setLoading(false);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新昵称
|
|
|
const nicknameRes = await updateNickname(formData.nickname);
|
|
|
-
|
|
|
+
|
|
|
// 更新其他信息(性别等)
|
|
|
const infoRes = await updateUserInfo({ sex: formData.sex } as any);
|
|
|
-
|
|
|
+
|
|
|
if (nicknameRes || infoRes) {
|
|
|
- Alert.alert('提示', '保存成功', [
|
|
|
+ Alert.alert("提示", "保存成功", [
|
|
|
{
|
|
|
- text: '确定',
|
|
|
+ text: "确定",
|
|
|
onPress: () => {
|
|
|
refreshUser?.();
|
|
|
router.back();
|
|
|
- }
|
|
|
- }
|
|
|
+ },
|
|
|
+ },
|
|
|
]);
|
|
|
} else {
|
|
|
- Alert.alert('提示', '保存失败');
|
|
|
+ Alert.alert("提示", "保存失败");
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('保存失败:', error);
|
|
|
- Alert.alert('提示', '保存失败');
|
|
|
+ console.error("保存失败:", error);
|
|
|
+ Alert.alert("提示", "保存失败");
|
|
|
} finally {
|
|
|
setLoading(false);
|
|
|
}
|
|
|
@@ -149,7 +156,7 @@ export default function ProfileScreen() {
|
|
|
resizeMode="cover"
|
|
|
>
|
|
|
<StatusBar barStyle="light-content" />
|
|
|
-
|
|
|
+
|
|
|
{/* 顶部导航 */}
|
|
|
<View style={[styles.header, { paddingTop: insets.top }]}>
|
|
|
<TouchableOpacity style={styles.backBtn} onPress={handleBack}>
|
|
|
@@ -159,9 +166,15 @@ export default function ProfileScreen() {
|
|
|
<View style={styles.placeholder} />
|
|
|
</View>
|
|
|
|
|
|
- <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
|
|
+ <ScrollView
|
|
|
+ style={styles.scrollView}
|
|
|
+ showsVerticalScrollIndicator={false}
|
|
|
+ >
|
|
|
{/* 头像 */}
|
|
|
- <TouchableOpacity style={styles.avatarSection} onPress={handleChooseAvatar}>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={styles.avatarSection}
|
|
|
+ onPress={handleChooseAvatar}
|
|
|
+ >
|
|
|
<View style={styles.avatarWrapper}>
|
|
|
<Image
|
|
|
source={{ uri: formData.avatar || Images.common.defaultAvatar }}
|
|
|
@@ -180,7 +193,9 @@ export default function ProfileScreen() {
|
|
|
<TextInput
|
|
|
style={styles.formInput}
|
|
|
value={formData.nickname}
|
|
|
- onChangeText={(text) => setFormData(prev => ({ ...prev, nickname: text }))}
|
|
|
+ onChangeText={(text) =>
|
|
|
+ setFormData((prev) => ({ ...prev, nickname: text }))
|
|
|
+ }
|
|
|
placeholder="请输入昵称"
|
|
|
placeholderTextColor="#999"
|
|
|
maxLength={20}
|
|
|
@@ -192,31 +207,76 @@ export default function ProfileScreen() {
|
|
|
<Text style={styles.formLabel}>性别</Text>
|
|
|
<View style={styles.sexOptions}>
|
|
|
<TouchableOpacity
|
|
|
- style={[styles.sexOption, formData.sex === 1 && styles.sexOptionActive]}
|
|
|
+ style={[
|
|
|
+ styles.sexOption,
|
|
|
+ formData.sex === 1 && styles.sexOptionActive,
|
|
|
+ ]}
|
|
|
onPress={() => handleSexChange(1)}
|
|
|
>
|
|
|
- <View style={[styles.radioOuter, formData.sex === 1 && styles.radioOuterActive]}>
|
|
|
+ <View
|
|
|
+ style={[
|
|
|
+ styles.radioOuter,
|
|
|
+ formData.sex === 1 && styles.radioOuterActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
{formData.sex === 1 && <View style={styles.radioInner} />}
|
|
|
</View>
|
|
|
- <Text style={[styles.sexText, formData.sex === 1 && styles.sexTextActive]}>男</Text>
|
|
|
+ <Text
|
|
|
+ style={[
|
|
|
+ styles.sexText,
|
|
|
+ formData.sex === 1 && styles.sexTextActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ 男
|
|
|
+ </Text>
|
|
|
</TouchableOpacity>
|
|
|
<TouchableOpacity
|
|
|
- style={[styles.sexOption, formData.sex === 2 && styles.sexOptionActive]}
|
|
|
+ style={[
|
|
|
+ styles.sexOption,
|
|
|
+ formData.sex === 2 && styles.sexOptionActive,
|
|
|
+ ]}
|
|
|
onPress={() => handleSexChange(2)}
|
|
|
>
|
|
|
- <View style={[styles.radioOuter, formData.sex === 2 && styles.radioOuterActive]}>
|
|
|
+ <View
|
|
|
+ style={[
|
|
|
+ styles.radioOuter,
|
|
|
+ formData.sex === 2 && styles.radioOuterActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
{formData.sex === 2 && <View style={styles.radioInner} />}
|
|
|
</View>
|
|
|
- <Text style={[styles.sexText, formData.sex === 2 && styles.sexTextActive]}>女</Text>
|
|
|
+ <Text
|
|
|
+ style={[
|
|
|
+ styles.sexText,
|
|
|
+ formData.sex === 2 && styles.sexTextActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ 女
|
|
|
+ </Text>
|
|
|
</TouchableOpacity>
|
|
|
<TouchableOpacity
|
|
|
- style={[styles.sexOption, formData.sex === 3 && styles.sexOptionActive]}
|
|
|
+ style={[
|
|
|
+ styles.sexOption,
|
|
|
+ formData.sex === 3 && styles.sexOptionActive,
|
|
|
+ ]}
|
|
|
onPress={() => handleSexChange(3)}
|
|
|
>
|
|
|
- <View style={[styles.radioOuter, formData.sex === 3 && styles.radioOuterActive]}>
|
|
|
+ <View
|
|
|
+ style={[
|
|
|
+ styles.radioOuter,
|
|
|
+ formData.sex === 3 && styles.radioOuterActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
{formData.sex === 3 && <View style={styles.radioInner} />}
|
|
|
</View>
|
|
|
- <Text style={[styles.sexText, formData.sex === 3 && styles.sexTextActive]}>保密</Text>
|
|
|
+ <Text
|
|
|
+ style={[
|
|
|
+ styles.sexText,
|
|
|
+ formData.sex === 3 && styles.sexTextActive,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ 保密
|
|
|
+ </Text>
|
|
|
</TouchableOpacity>
|
|
|
</View>
|
|
|
</View>
|
|
|
@@ -233,7 +293,9 @@ export default function ProfileScreen() {
|
|
|
style={styles.saveBtnBg}
|
|
|
resizeMode="contain"
|
|
|
>
|
|
|
- <Text style={styles.saveBtnText}>{loading ? '保存中...' : '确定'}</Text>
|
|
|
+ <Text style={styles.saveBtnText}>
|
|
|
+ {loading ? "保存中..." : "确定"}
|
|
|
+ </Text>
|
|
|
</ImageBackground>
|
|
|
</TouchableOpacity>
|
|
|
</ScrollView>
|
|
|
@@ -246,27 +308,27 @@ const styles = StyleSheet.create({
|
|
|
flex: 1,
|
|
|
},
|
|
|
header: {
|
|
|
- flexDirection: 'row',
|
|
|
- alignItems: 'center',
|
|
|
- justifyContent: 'space-between',
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "space-between",
|
|
|
paddingHorizontal: 10,
|
|
|
height: 80,
|
|
|
},
|
|
|
backBtn: {
|
|
|
width: 40,
|
|
|
height: 40,
|
|
|
- justifyContent: 'center',
|
|
|
- alignItems: 'center',
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
},
|
|
|
backIcon: {
|
|
|
fontSize: 32,
|
|
|
- color: '#fff',
|
|
|
- fontWeight: 'bold',
|
|
|
+ color: "#fff",
|
|
|
+ fontWeight: "bold",
|
|
|
},
|
|
|
headerTitle: {
|
|
|
fontSize: 16,
|
|
|
- fontWeight: 'bold',
|
|
|
- color: '#fff',
|
|
|
+ fontWeight: "bold",
|
|
|
+ color: "#fff",
|
|
|
},
|
|
|
placeholder: {
|
|
|
width: 40,
|
|
|
@@ -276,7 +338,7 @@ const styles = StyleSheet.create({
|
|
|
paddingHorizontal: 20,
|
|
|
},
|
|
|
avatarSection: {
|
|
|
- alignItems: 'center',
|
|
|
+ alignItems: "center",
|
|
|
paddingVertical: 30,
|
|
|
},
|
|
|
avatarWrapper: {
|
|
|
@@ -284,49 +346,49 @@ const styles = StyleSheet.create({
|
|
|
height: 80,
|
|
|
borderRadius: 40,
|
|
|
borderWidth: 3,
|
|
|
- borderColor: '#FFE996',
|
|
|
- overflow: 'hidden',
|
|
|
+ borderColor: "#FFE996",
|
|
|
+ overflow: "hidden",
|
|
|
},
|
|
|
avatar: {
|
|
|
- width: '100%',
|
|
|
- height: '100%',
|
|
|
+ width: "100%",
|
|
|
+ height: "100%",
|
|
|
},
|
|
|
avatarTip: {
|
|
|
marginTop: 10,
|
|
|
fontSize: 12,
|
|
|
- color: 'rgba(255,255,255,0.7)',
|
|
|
+ color: "rgba(255,255,255,0.7)",
|
|
|
},
|
|
|
formSection: {
|
|
|
- backgroundColor: 'rgba(255,255,255,0.1)',
|
|
|
+ backgroundColor: "rgba(255,255,255,0.1)",
|
|
|
borderRadius: 10,
|
|
|
padding: 15,
|
|
|
},
|
|
|
formItem: {
|
|
|
- flexDirection: 'row',
|
|
|
- alignItems: 'center',
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
paddingVertical: 15,
|
|
|
borderBottomWidth: 1,
|
|
|
- borderBottomColor: 'rgba(255,255,255,0.2)',
|
|
|
+ borderBottomColor: "rgba(255,255,255,0.2)",
|
|
|
},
|
|
|
formLabel: {
|
|
|
width: 60,
|
|
|
fontSize: 14,
|
|
|
- color: '#fff',
|
|
|
+ color: "#fff",
|
|
|
},
|
|
|
formInput: {
|
|
|
flex: 1,
|
|
|
fontSize: 14,
|
|
|
- color: '#fff',
|
|
|
+ color: "#fff",
|
|
|
padding: 0,
|
|
|
},
|
|
|
sexOptions: {
|
|
|
flex: 1,
|
|
|
- flexDirection: 'row',
|
|
|
- alignItems: 'center',
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
},
|
|
|
sexOption: {
|
|
|
- flexDirection: 'row',
|
|
|
- alignItems: 'center',
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
marginRight: 20,
|
|
|
},
|
|
|
sexOptionActive: {},
|
|
|
@@ -335,30 +397,30 @@ const styles = StyleSheet.create({
|
|
|
height: 18,
|
|
|
borderRadius: 9,
|
|
|
borderWidth: 2,
|
|
|
- borderColor: 'rgba(255,255,255,0.5)',
|
|
|
- justifyContent: 'center',
|
|
|
- alignItems: 'center',
|
|
|
+ borderColor: "rgba(255,255,255,0.5)",
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
marginRight: 6,
|
|
|
},
|
|
|
radioOuterActive: {
|
|
|
- borderColor: '#FC7D2E',
|
|
|
+ borderColor: "#FC7D2E",
|
|
|
},
|
|
|
radioInner: {
|
|
|
width: 10,
|
|
|
height: 10,
|
|
|
borderRadius: 5,
|
|
|
- backgroundColor: '#FC7D2E',
|
|
|
+ backgroundColor: "#FC7D2E",
|
|
|
},
|
|
|
sexText: {
|
|
|
fontSize: 14,
|
|
|
- color: 'rgba(255,255,255,0.7)',
|
|
|
+ color: "rgba(255,255,255,0.7)",
|
|
|
},
|
|
|
sexTextActive: {
|
|
|
- color: '#fff',
|
|
|
+ color: "#fff",
|
|
|
},
|
|
|
saveBtn: {
|
|
|
marginTop: 50,
|
|
|
- alignItems: 'center',
|
|
|
+ alignItems: "center",
|
|
|
},
|
|
|
saveBtnDisabled: {
|
|
|
opacity: 0.6,
|
|
|
@@ -366,14 +428,14 @@ const styles = StyleSheet.create({
|
|
|
saveBtnBg: {
|
|
|
width: 280,
|
|
|
height: 60,
|
|
|
- justifyContent: 'center',
|
|
|
- alignItems: 'center',
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
},
|
|
|
saveBtnText: {
|
|
|
fontSize: 16,
|
|
|
- fontWeight: 'bold',
|
|
|
- color: '#fff',
|
|
|
- textShadowColor: '#000',
|
|
|
+ fontWeight: "bold",
|
|
|
+ color: "#fff",
|
|
|
+ textShadowColor: "#000",
|
|
|
textShadowOffset: { width: 1, height: 1 },
|
|
|
textShadowRadius: 2,
|
|
|
},
|