index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { Image } from "expo-image";
  2. import * as ImagePicker from "expo-image-picker";
  3. import { useRouter } from "expo-router";
  4. import React, { useEffect, useState } from "react";
  5. import {
  6. Alert,
  7. ImageBackground,
  8. ScrollView,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TextInput,
  13. TouchableOpacity,
  14. View,
  15. } from "react-native";
  16. import { useSafeAreaInsets } from "react-native-safe-area-context";
  17. import { Images } from "@/constants/images";
  18. import { useAuth } from "@/contexts/AuthContext";
  19. import { uploadFile } from "@/services/base";
  20. import { getToken } from "@/services/http";
  21. import {
  22. getUserInfo,
  23. updateAvatar,
  24. updateNickname,
  25. updateUserInfo,
  26. } from "@/services/user";
  27. interface FormData {
  28. nickname: string;
  29. avatar: string;
  30. sex: number; // 1-男 2-女 3-保密
  31. }
  32. export default function ProfileScreen() {
  33. const router = useRouter();
  34. const insets = useSafeAreaInsets();
  35. const { refreshUser } = useAuth();
  36. const [formData, setFormData] = useState<FormData>({
  37. nickname: "",
  38. avatar: "",
  39. sex: 3,
  40. });
  41. const [loading, setLoading] = useState(false);
  42. useEffect(() => {
  43. loadUserInfo();
  44. }, []);
  45. const loadUserInfo = async () => {
  46. try {
  47. const res = await getUserInfo();
  48. if (res) {
  49. setFormData({
  50. nickname: res.nickname || "",
  51. avatar: res.avatar || "",
  52. sex: (res as any).sex || 3,
  53. });
  54. }
  55. } catch (error) {
  56. console.error("获取用户信息失败:", error);
  57. }
  58. };
  59. const handleBack = () => {
  60. router.back();
  61. };
  62. const handleChooseAvatar = async () => {
  63. try {
  64. const permissionResult =
  65. await ImagePicker.requestMediaLibraryPermissionsAsync();
  66. if (!permissionResult.granted) {
  67. Alert.alert("提示", "需要相册权限才能选择头像");
  68. return;
  69. }
  70. const result = await ImagePicker.launchImageLibraryAsync({
  71. mediaTypes: ["images"],
  72. allowsEditing: true,
  73. aspect: [1, 1],
  74. quality: 0.4, // 服务器限制1MB,降低质量确保不超限
  75. });
  76. if (!result.canceled && result.assets[0]) {
  77. const imageUri = result.assets[0].uri;
  78. setFormData((prev) => ({ ...prev, avatar: imageUri }));
  79. }
  80. } catch (error) {
  81. console.error("选择头像失败:", error);
  82. Alert.alert("提示", "选择头像失败");
  83. }
  84. };
  85. const handleSexChange = (sex: number) => {
  86. setFormData((prev) => ({ ...prev, sex }));
  87. };
  88. const handleSave = async () => {
  89. if (!formData.nickname?.trim()) {
  90. Alert.alert("提示", "请输入昵称");
  91. return;
  92. }
  93. try {
  94. setLoading(true);
  95. // 如果头像是本地文件(非http开头),需要先上传
  96. let avatarUrl = formData.avatar;
  97. console.log("[profile] 头像URI:", formData.avatar?.substring(0, 100));
  98. if (formData.avatar && !formData.avatar.startsWith("http")) {
  99. const token = getToken();
  100. const uploadResult = await uploadFile(formData.avatar, "avatar", token || undefined);
  101. if (typeof uploadResult === "string") {
  102. avatarUrl = uploadResult;
  103. await updateAvatar(avatarUrl);
  104. } else {
  105. Alert.alert("头像上传失败", uploadResult.error);
  106. setLoading(false);
  107. return;
  108. }
  109. }
  110. // 更新昵称
  111. const nicknameRes = await updateNickname(formData.nickname);
  112. // 更新其他信息(性别等)
  113. const infoRes = await updateUserInfo({ sex: formData.sex } as any);
  114. if (nicknameRes || infoRes) {
  115. Alert.alert("提示", "保存成功", [
  116. {
  117. text: "确定",
  118. onPress: () => {
  119. refreshUser?.();
  120. router.back();
  121. },
  122. },
  123. ]);
  124. } else {
  125. Alert.alert("提示", "保存失败");
  126. }
  127. } catch (error) {
  128. console.error("保存失败:", error);
  129. Alert.alert("提示", "保存失败");
  130. } finally {
  131. setLoading(false);
  132. }
  133. };
  134. return (
  135. <ImageBackground
  136. source={{ uri: Images.common.commonBg }}
  137. style={styles.container}
  138. resizeMode="cover"
  139. >
  140. <StatusBar barStyle="light-content" />
  141. {/* 顶部导航 */}
  142. <View style={[styles.header, { paddingTop: insets.top }]}>
  143. <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
  144. <Text style={styles.backIcon}>‹</Text>
  145. </TouchableOpacity>
  146. <Text style={styles.headerTitle}>个人资料</Text>
  147. <View style={styles.placeholder} />
  148. </View>
  149. <ScrollView
  150. style={styles.scrollView}
  151. showsVerticalScrollIndicator={false}
  152. >
  153. {/* 头像 */}
  154. <TouchableOpacity
  155. style={styles.avatarSection}
  156. onPress={handleChooseAvatar}
  157. >
  158. <View style={styles.avatarWrapper}>
  159. <Image
  160. source={{ uri: formData.avatar || Images.common.defaultAvatar }}
  161. style={styles.avatar}
  162. contentFit="cover"
  163. />
  164. </View>
  165. <Text style={styles.avatarTip}>点击更换头像</Text>
  166. </TouchableOpacity>
  167. {/* 表单 */}
  168. <View style={styles.formSection}>
  169. {/* 昵称 */}
  170. <View style={styles.formItem}>
  171. <Text style={styles.formLabel}>昵称</Text>
  172. <TextInput
  173. style={styles.formInput}
  174. value={formData.nickname}
  175. onChangeText={(text) =>
  176. setFormData((prev) => ({ ...prev, nickname: text }))
  177. }
  178. placeholder="请输入昵称"
  179. placeholderTextColor="#999"
  180. maxLength={20}
  181. />
  182. </View>
  183. {/* 性别 */}
  184. <View style={styles.formItem}>
  185. <Text style={styles.formLabel}>性别</Text>
  186. <View style={styles.sexOptions}>
  187. <TouchableOpacity
  188. style={[
  189. styles.sexOption,
  190. formData.sex === 1 && styles.sexOptionActive,
  191. ]}
  192. onPress={() => handleSexChange(1)}
  193. >
  194. <View
  195. style={[
  196. styles.radioOuter,
  197. formData.sex === 1 && styles.radioOuterActive,
  198. ]}
  199. >
  200. {formData.sex === 1 && <View style={styles.radioInner} />}
  201. </View>
  202. <Text
  203. style={[
  204. styles.sexText,
  205. formData.sex === 1 && styles.sexTextActive,
  206. ]}
  207. >
  208. </Text>
  209. </TouchableOpacity>
  210. <TouchableOpacity
  211. style={[
  212. styles.sexOption,
  213. formData.sex === 2 && styles.sexOptionActive,
  214. ]}
  215. onPress={() => handleSexChange(2)}
  216. >
  217. <View
  218. style={[
  219. styles.radioOuter,
  220. formData.sex === 2 && styles.radioOuterActive,
  221. ]}
  222. >
  223. {formData.sex === 2 && <View style={styles.radioInner} />}
  224. </View>
  225. <Text
  226. style={[
  227. styles.sexText,
  228. formData.sex === 2 && styles.sexTextActive,
  229. ]}
  230. >
  231. </Text>
  232. </TouchableOpacity>
  233. <TouchableOpacity
  234. style={[
  235. styles.sexOption,
  236. formData.sex === 3 && styles.sexOptionActive,
  237. ]}
  238. onPress={() => handleSexChange(3)}
  239. >
  240. <View
  241. style={[
  242. styles.radioOuter,
  243. formData.sex === 3 && styles.radioOuterActive,
  244. ]}
  245. >
  246. {formData.sex === 3 && <View style={styles.radioInner} />}
  247. </View>
  248. <Text
  249. style={[
  250. styles.sexText,
  251. formData.sex === 3 && styles.sexTextActive,
  252. ]}
  253. >
  254. 保密
  255. </Text>
  256. </TouchableOpacity>
  257. </View>
  258. </View>
  259. </View>
  260. {/* 保存按钮 */}
  261. <TouchableOpacity
  262. style={[styles.saveBtn, loading && styles.saveBtnDisabled]}
  263. onPress={handleSave}
  264. disabled={loading}
  265. >
  266. <ImageBackground
  267. source={{ uri: Images.common.loginBtn }}
  268. style={styles.saveBtnBg}
  269. resizeMode="contain"
  270. >
  271. <Text style={styles.saveBtnText}>
  272. {loading ? "保存中..." : "确定"}
  273. </Text>
  274. </ImageBackground>
  275. </TouchableOpacity>
  276. </ScrollView>
  277. </ImageBackground>
  278. );
  279. }
  280. const styles = StyleSheet.create({
  281. container: {
  282. flex: 1,
  283. },
  284. header: {
  285. flexDirection: "row",
  286. alignItems: "center",
  287. justifyContent: "space-between",
  288. paddingHorizontal: 10,
  289. height: 80,
  290. },
  291. backBtn: {
  292. width: 40,
  293. height: 40,
  294. justifyContent: "center",
  295. alignItems: "center",
  296. },
  297. backIcon: {
  298. fontSize: 32,
  299. color: "#fff",
  300. fontWeight: "bold",
  301. },
  302. headerTitle: {
  303. fontSize: 16,
  304. fontWeight: "bold",
  305. color: "#fff",
  306. },
  307. placeholder: {
  308. width: 40,
  309. },
  310. scrollView: {
  311. flex: 1,
  312. paddingHorizontal: 20,
  313. },
  314. avatarSection: {
  315. alignItems: "center",
  316. paddingVertical: 30,
  317. },
  318. avatarWrapper: {
  319. width: 80,
  320. height: 80,
  321. borderRadius: 40,
  322. borderWidth: 3,
  323. borderColor: "#FFE996",
  324. overflow: "hidden",
  325. },
  326. avatar: {
  327. width: "100%",
  328. height: "100%",
  329. },
  330. avatarTip: {
  331. marginTop: 10,
  332. fontSize: 12,
  333. color: "rgba(255,255,255,0.7)",
  334. },
  335. formSection: {
  336. backgroundColor: "rgba(255,255,255,0.1)",
  337. borderRadius: 10,
  338. padding: 15,
  339. },
  340. formItem: {
  341. flexDirection: "row",
  342. alignItems: "center",
  343. paddingVertical: 15,
  344. borderBottomWidth: 1,
  345. borderBottomColor: "rgba(255,255,255,0.2)",
  346. },
  347. formLabel: {
  348. width: 60,
  349. fontSize: 14,
  350. color: "#fff",
  351. },
  352. formInput: {
  353. flex: 1,
  354. fontSize: 14,
  355. color: "#fff",
  356. padding: 0,
  357. },
  358. sexOptions: {
  359. flex: 1,
  360. flexDirection: "row",
  361. alignItems: "center",
  362. },
  363. sexOption: {
  364. flexDirection: "row",
  365. alignItems: "center",
  366. marginRight: 20,
  367. },
  368. sexOptionActive: {},
  369. radioOuter: {
  370. width: 18,
  371. height: 18,
  372. borderRadius: 9,
  373. borderWidth: 2,
  374. borderColor: "rgba(255,255,255,0.5)",
  375. justifyContent: "center",
  376. alignItems: "center",
  377. marginRight: 6,
  378. },
  379. radioOuterActive: {
  380. borderColor: "#FC7D2E",
  381. },
  382. radioInner: {
  383. width: 10,
  384. height: 10,
  385. borderRadius: 5,
  386. backgroundColor: "#FC7D2E",
  387. },
  388. sexText: {
  389. fontSize: 14,
  390. color: "rgba(255,255,255,0.7)",
  391. },
  392. sexTextActive: {
  393. color: "#fff",
  394. },
  395. saveBtn: {
  396. marginTop: 50,
  397. alignItems: "center",
  398. },
  399. saveBtnDisabled: {
  400. opacity: 0.6,
  401. },
  402. saveBtnBg: {
  403. width: 280,
  404. height: 60,
  405. justifyContent: "center",
  406. alignItems: "center",
  407. },
  408. saveBtnText: {
  409. fontSize: 16,
  410. fontWeight: "bold",
  411. color: "#fff",
  412. textShadowColor: "#000",
  413. textShadowOffset: { width: 1, height: 1 },
  414. textShadowRadius: 2,
  415. },
  416. });