浏览代码

fix: 头像上传-缩小质量0.4+传token+显示错误详情 build 32

zbb 1 月之前
父节点
当前提交
6fa7600346
共有 3 个文件被更改,包括 169 次插入114 次删除
  1. 1 1
      app.json
  2. 157 95
      app/profile/index.tsx
  3. 11 18
      services/base.ts

+ 1 - 1
app.json

@@ -12,7 +12,7 @@
       "supportsTablet": false,
       "bundleIdentifier": "com.asios",
       "appleTeamId": "Y9ZVX3FRX6",
-      "buildNumber": "31",
+      "buildNumber": "32",
       "infoPlist": {
         "CFBundleDisplayName": "艾斯潮盒",
         "ITSAppUsesNonExemptEncryption": false,

+ 157 - 95
app/profile/index.tsx

@@ -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,
   },

+ 11 - 18
services/base.ts

@@ -82,9 +82,9 @@ export const track = async () => {
 export const uploadFile = async (
   fileUri: string,
   folder = "avatar",
-): Promise<string | null> => {
+  externalToken?: string,
+): Promise<string | { error: string }> => {
   try {
-    // 处理 URI
     let processedUri = decodeURI(fileUri);
     if (Platform.OS === "ios") {
       if (!processedUri.startsWith("file://") && !processedUri.startsWith("http")) {
@@ -92,12 +92,11 @@ export const uploadFile = async (
       }
     }
 
-    // 获取文件名和类型
     const fileName = processedUri.split("/").pop() || `avatar_${Date.now()}.jpg`;
     const fileExt = fileName.split(".").pop()?.toLowerCase() || "jpg";
     const mimeType = fileExt === "png" ? "image/png" : "image/jpeg";
 
-    console.log("[uploadFile] 开始上传, uri:", processedUri.substring(0, 80), "fileName:", fileName);
+    console.log("[uploadFile] uri:", processedUri.substring(0, 80), "name:", fileName);
 
     const formData = new FormData();
     formData.append("file", {
@@ -108,10 +107,9 @@ export const uploadFile = async (
     formData.append("appId", "supermart-acetoys");
     formData.append("folder", folder);
 
-    const token = getToken();
-    console.log("[uploadFile] token:", token ? "有" : "无");
+    const token = externalToken || getToken();
+    console.log("[uploadFile] token:", token ? token.substring(0, 10) + "..." : "空!");
 
-    // 使用与 Vue wx.uploadFile 完全一致的请求方式:只传 Authentication header
     const response = await fetch("https://mm.acefig.com/api/oss/file/upload", {
       method: "POST",
       headers: {
@@ -121,28 +119,23 @@ export const uploadFile = async (
     });
 
     const responseText = await response.text();
-    console.log("[uploadFile] 状态:", response.status, "响应:", responseText?.substring(0, 300));
+    console.log("[uploadFile] status:", response.status, "body:", responseText?.substring(0, 200));
 
     if (!response.ok) {
-      console.error("[uploadFile] HTTP错误:", response.status);
-      return null;
+      return { error: `HTTP ${response.status}: ${responseText?.substring(0, 100)}` };
     }
-
     if (!responseText) {
-      console.error("[uploadFile] 响应为空");
-      return null;
+      return { error: "服务器响应为空" };
     }
 
     const result = JSON.parse(responseText);
     if ((result.code === 0 || result.code === "0") && result.data?.url) {
-      console.log("[uploadFile] 成功:", result.data.url);
       return result.data.url;
     }
-    console.error("[uploadFile] 业务错误:", JSON.stringify(result));
-    return null;
-  } catch (error) {
+    return { error: `code:${result.code} msg:${result.msg || JSON.stringify(result).substring(0, 80)}` };
+  } catch (error: any) {
     console.error("[uploadFile] 异常:", error);
-    return null;
+    return { error: `异常: ${error?.message || String(error).substring(0, 100)}` };
   }
 };