From 07e0001b87102de7b2301042752c30dfb871c4cf Mon Sep 17 00:00:00 2001 From: oneao Date: Thu, 23 Apr 2026 21:35:54 +0800 Subject: [PATCH] u --- .../allapp-go-v3/internal/bootstrap/server.go | 1 + .../allapp/allapp-go-v3/internal/handle/s3.go | 58 ++-- code/app/app-rn4/.env | 4 +- code/app/app-rn4/app.json | 14 - code/app/app-rn4/package.json | 1 - code/app/app-rn4/pnpm-lock.yaml | 22 -- .../app/app-rn4/src/app/(tabs)/chat/index.tsx | 87 ++++-- code/app/app-rn4/src/app/(tabs)/my/index.tsx | 3 +- code/app/app-rn4/src/app/auth/login.tsx | 240 +++++++++++++--- code/app/app-rn4/src/app/auth/login.txt | 119 ++++++++ code/app/app-rn4/src/app/auth/login.txt2 | 262 ------------------ code/app/app-rn4/src/app/family/user.tsx | 74 +++-- code/app/app-rn4/src/request/api.ts | 2 +- code/app/app-rn4/src/store/user.ts | 10 + code/app/app-rn4/src/utils/file.ts | 40 +++ 15 files changed, 527 insertions(+), 410 deletions(-) create mode 100644 code/app/app-rn4/src/app/auth/login.txt delete mode 100644 code/app/app-rn4/src/app/auth/login.txt2 create mode 100644 code/app/app-rn4/src/utils/file.ts diff --git a/code/allapp/allapp-go-v3/internal/bootstrap/server.go b/code/allapp/allapp-go-v3/internal/bootstrap/server.go index 60011f72..59c0246e 100644 --- a/code/allapp/allapp-go-v3/internal/bootstrap/server.go +++ b/code/allapp/allapp-go-v3/internal/bootstrap/server.go @@ -16,6 +16,7 @@ import ( func RunServer(cfg *config.Config) error { app := fiber.New(fiber.Config{ ErrorHandler: errors.ErrorHandler(), + BodyLimit: 20 * 1024 * 1024, // 20MB }) // ===== middleware ===== diff --git a/code/allapp/allapp-go-v3/internal/handle/s3.go b/code/allapp/allapp-go-v3/internal/handle/s3.go index 40462092..9b226725 100644 --- a/code/allapp/allapp-go-v3/internal/handle/s3.go +++ b/code/allapp/allapp-go-v3/internal/handle/s3.go @@ -5,7 +5,6 @@ import ( "allapp-go/internal/httpx" "allapp-go/pkg/s3store" "fmt" - "mime/multipart" "path/filepath" "time" @@ -31,20 +30,17 @@ func S3ListBuckets(c fiber.Ctx) error { } func S3UploadFile(c fiber.Ctx) error { - fileHeader, err := c.FormFile("file") + form, err := c.MultipartForm() if err != nil { return errors.WithStack(err) } - file, err := fileHeader.Open() - if err != nil { - return errors.WithStack(err) + files := form.File["files"] + + if len(files) == 0 { + return httpx.OK(c, nil) } - defer func(file multipart.File) { - _ = file.Close() - }(file) - // 日期目录 now := time.Now() folderPath := fmt.Sprintf("%d/%02d/%02d", now.Year(), @@ -52,28 +48,36 @@ func S3UploadFile(c fiber.Ctx) error { now.Day(), ) - // UUID 文件名(保留后缀) - ext := filepath.Ext(fileHeader.Filename) - newName := fmt.Sprintf("%s%s", uuid.NewString(), ext) + var result []string - // S3 object key - key := fmt.Sprintf("%s/%s", folderPath, newName) + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + return errors.WithStack(err) + } - // ContentType(防止前端乱传) - contentType := fileHeader.Header.Get("Content-Type") + ext := filepath.Ext(fileHeader.Filename) + newName := fmt.Sprintf("%s%s", uuid.NewString(), ext) + key := fmt.Sprintf("%s/%s", folderPath, newName) - // 调用存储层 - err = s3store.UploadToRustFS( - c.Context(), - key, - file, - fileHeader.Size, - contentType, - ) + contentType := fileHeader.Header.Get("Content-Type") - if err != nil { - return errors.WithStack(err) + err = s3store.UploadToRustFS( + c.Context(), + key, + file, + fileHeader.Size, + contentType, + ) + + _ = file.Close() + + if err != nil { + return errors.WithStack(err) + } + + result = append(result, key) } - return httpx.OK(c, nil) + return httpx.OK(c, result) } diff --git a/code/app/app-rn4/.env b/code/app/app-rn4/.env index 9af85c75..7c8acd2f 100644 --- a/code/app/app-rn4/.env +++ b/code/app/app-rn4/.env @@ -1 +1,3 @@ -EXPO_PUBLIC_API_URL=https://dev.api.com \ No newline at end of file +EXPO_PUBLIC_API_URL=https://dev.api.com +EXPO_PUBLIC_G3_URL=http://118.89.70.199:9001 +EXPO_PUBLIC_G3_BUCKET=test diff --git a/code/app/app-rn4/app.json b/code/app/app-rn4/app.json index 114954e1..622b05f6 100644 --- a/code/app/app-rn4/app.json +++ b/code/app/app-rn4/app.json @@ -67,20 +67,6 @@ { "iCloudContainerEnvironment": "Production" } - ], - [ - "expo-image-picker", - { - "photosPermission": "The app accesses your photos to let you share them with your friends.", - "colors": { - "cropToolbarColor": "#000000" - }, - "dark": { - "colors": { - "cropToolbarColor": "#000000" - } - } - } ] ], "experiments": { diff --git a/code/app/app-rn4/package.json b/code/app/app-rn4/package.json index 526e1d33..f5247456 100644 --- a/code/app/app-rn4/package.json +++ b/code/app/app-rn4/package.json @@ -34,7 +34,6 @@ "expo-font": "~55.0.6", "expo-glass-effect": "~55.0.10", "expo-image": "~55.0.8", - "expo-image-picker": "~55.0.18", "expo-linear-gradient": "~55.0.13", "expo-linking": "~55.0.13", "expo-qq": "^0.1.8", diff --git a/code/app/app-rn4/pnpm-lock.yaml b/code/app/app-rn4/pnpm-lock.yaml index f8b6b9ba..6c864ce8 100644 --- a/code/app/app-rn4/pnpm-lock.yaml +++ b/code/app/app-rn4/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: expo-image: specifier: ~55.0.8 version: 55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-image-picker: - specifier: ~55.0.18 - version: 55.0.18(expo@55.0.15) expo-linear-gradient: specifier: ~55.0.13 version: 55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -2311,16 +2308,6 @@ packages: react: '*' react-native: '*' - expo-image-loader@55.0.0: - resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==} - peerDependencies: - expo: '*' - - expo-image-picker@55.0.18: - resolution: {integrity: sha512-lGpPGRu+7mE8qN0ma2boRsCmfOGbdHZ2bXTpWVeWly0JCZdogGlTrYFnhTqgS8+lmiRb/UCOs7iTm2P5Rra6kw==} - peerDependencies: - expo: '*' - expo-image@55.0.8: resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==} peerDependencies: @@ -6609,15 +6596,6 @@ snapshots: react: 19.2.0 react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) - expo-image-loader@55.0.0(expo@55.0.15): - dependencies: - expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) - - expo-image-picker@55.0.18(expo@55.0.15): - dependencies: - expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) - expo-image-loader: 55.0.0(expo@55.0.15) - expo-image@55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) diff --git a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx index fcd72187..6364b63e 100644 --- a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx @@ -1,40 +1,66 @@ -import { useState } from "react"; -import { Alert, Button, Image, View, StyleSheet } from "react-native"; -import * as ImagePicker from "expo-image-picker"; +import React, { useState } from "react"; +import { View, Button, Image, ScrollView, StyleSheet } from "react-native"; +import ImagePicker from "react-native-image-crop-picker"; -export default function ImagePickerExample() { - const [image, setImage] = useState(null); +export default function ImageCropPickerDemo() { + const [images, setImages] = useState([]); - const pickImage = async () => { - // No permissions request is necessary for launching the image library. - // Manually request permissions for videos on iOS when `allowsEditing` is set to `false` - // and `videoExportPreset` is `'Passthrough'` (the default), ideally before launching the picker - // so the app users aren't surprised by a system dialog after picking a video. - // See "Invoke permissions for videos" sub section for more details. - const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); + // 选择单张 + 裁剪 + const pickSingle = async () => { + try { + const image = await ImagePicker.openPicker({ + freeStyleCropEnabled: true, + cropping: true, + compressImageQuality: 0.7, + }); - if (!permissionResult.granted) { - Alert.alert("Permission required", "Permission to access the media library is required."); - return; + setImages([image]); + } catch (e) { + console.log(e); } + }; - let result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ["images", "videos"], - allowsEditing: true, - quality: 1, - }); + // 拍照 + 裁剪 + const takePhoto = async () => { + try { + const image = await ImagePicker.openCamera({ + width: 300, + height: 300, + cropping: true, + compressImageQuality: 0.7, + }); - console.log(result); + setImages([image]); + } catch (e) { + console.log(e); + } + }; - if (!result.canceled) { - setImage(result.assets[0].uri); + // 多选(不裁剪) + const pickMultiple = async () => { + try { + const result = await ImagePicker.openPicker({ + multiple: true, + compressImageQuality: 0.7, + }); + + setImages(result); + } catch (e) { + console.log(e); } }; return ( - */} - - diff --git a/code/app/app-rn4/src/app/auth/login.txt b/code/app/app-rn4/src/app/auth/login.txt new file mode 100644 index 00000000..e811d96d --- /dev/null +++ b/code/app/app-rn4/src/app/auth/login.txt @@ -0,0 +1,119 @@ +import { Stack } from "expo-router"; +import { Image, Text, View } from "react-native"; +import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native"; +import { IconQq } from "@/components/iconfont"; +import { useState } from "react"; +import { FadeIn } from "react-native-reanimated"; +import { useUserStore, useFamilyStore } from "@/store"; +import { useRouter } from "expo-router"; + +export default function Login() { + const router = useRouter(); + const userStore = useUserStore(); + const familyStore = useFamilyStore(); + + const [loading, setLoading] = useState(false); + const [isAgreed, setIsAgreed] = useState(true); + + /** + * {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100", "family_id": 797225979793477, "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8", "userId": 796541901389893}, "isSuccess": true, "message": "success", "track_id": "hI5gEsPHu1LGwkQaJ6lf1dDQby0rzxGVJj0KcNC3Nxw"} + */ + + function handleTest() { + if (loading) { + return; + } + + setLoading(true); + + setTimeout(() => { + requestAnimationFrame(() => { + userStore.setUserInfo({ + id: 796541901389893, + token: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8", + nickname: "Zhang", + avatar: + "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100", + }); + userStore.setAuthExpired(false); + console.log(familyStore.inFamily()); + + // if (familyStore.inFamily()) { + // router.replace("/(tabs)/home"); + // } else { + // router.replace("/family/entry"); + // return; + // } + + familyStore.setCurrentFamily({ + family_id: 797225979793477, + }); + + router.replace("/(tabs)/home"); + }); + setLoading(false); + }, 500); + } + + return ( + <> + + + + + Welcome to Allapp + + + + {/* + + */} + + + + + + + + + + 登录即表示你同意我们的 + + 用户协议 + + 和 + + 隐私政策 + + + + + + ); +} diff --git a/code/app/app-rn4/src/app/auth/login.txt2 b/code/app/app-rn4/src/app/auth/login.txt2 deleted file mode 100644 index d69ebe6b..00000000 --- a/code/app/app-rn4/src/app/auth/login.txt2 +++ /dev/null @@ -1,262 +0,0 @@ -import { useEffect } from "react"; -import { Stack } from "expo-router"; -import { Image, Text, View } from "react-native"; -import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native"; -import { IconQq, IconWeixin } from "@/components/iconfont"; -import { useState } from "react"; -import { FadeIn } from "react-native-reanimated"; -import { useUserStore, useFamilyStore } from "@/store"; -import { useRouter } from "expo-router"; -import ExpoQQ from "expo-qq"; -import ExpoWeChat from "expo-wechat"; -import { useEvent } from "expo"; -import { toast } from "sonner-native"; -import { loginQqApi, loginWxApi } from "@/request/api"; - -export default function Login() { - const router = useRouter(); - const userStore = useUserStore(); - const familyStore = useFamilyStore(); - - const [loading, setLoading] = useState(false); - const [isAgreed, setIsAgreed] = useState(true); - const [form, setForm] = useState({ - type: 0, - openid: "", - code: "", - }); - - // 登录 - const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished"); - const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo"); - const authResult = useEvent(ExpoWeChat, "onAuthResult"); - - useEffect(() => { - ExpoQQ.init("102826474", ""); - ExpoWeChat.registerApp("wxdab3e21a1f7e392f", ""); - }, []); - - // 微信登录 - async function handleLoginWechat() { - if (loading) { - return; - } - setLoading(true); - - // 检查微信是否安装 - const isInstalled = await ExpoWeChat.isWXAppInstalled(); - if (!isInstalled) { - toast.warning(onLoginFinished?.errorMessage || "请先安装微信"); - setLoading(false); - return; - } - - setForm((val) => ({ - ...val, - type: 0, - })); - - ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123"); - } - - // QQ 登录 - async function handleLoginQQ() { - if (loading) { - return; - } - - setLoading(true); - - setForm((val) => ({ - ...val, - type: 1, - })); - await ExpoQQ.login(["get_user_info"]); - } - - useEffect(() => { - if (!onLoginFinished) { - return; - } - - if (!onLoginFinished.success) { - toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权"); - setLoading(false); - return; - } - - const fetchUserInfo = async () => { - try { - const tokenInfo = await ExpoQQ.getLoginTokenInfo(); - if (!tokenInfo.openId) { - toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权"); - setLoading(false); - return; - } - - setForm((val) => ({ - ...val, - openid: String(tokenInfo.openId), - })); - - await ExpoQQ.sendGetUserInfoReq(); - } catch (e) { - console.log(e); - - setLoading(false); - } - }; - - fetchUserInfo(); - }, [onLoginFinished]); - - // QQ登录 - useEffect(() => { - const handleLogin = async () => { - if (!onGetUserInfo) { - setLoading(false); - return; - } - - try { - const loginRes = await loginQqApi({ - openid: form.openid, - nickname: onGetUserInfo.nickname || "", - avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2, - }); - - if (loginRes.isSuccess) { - requestAnimationFrame(() => { - userStore.setUserInfo({ - token: loginRes.data.token, - id: loginRes.data.userId, - nickname: loginRes.data.nickname, - avatar: loginRes.data.avatar, - }); - - userStore.setAuthExpired(false); - - if (loginRes.data.family_id === 0) { - router.replace("/family/entry"); - } else { - familyStore.setCurrentFamily({ - family_id: loginRes.data.family_id, - }); - - router.replace("/(tabs)/home"); - } - }); - } - } catch (err) { - console.error("登录接口出错", err); - } finally { - setLoading(false); - } - }; - - handleLogin(); - }, [onGetUserInfo]); - - // 微信登录 - useEffect(() => { - if (!authResult) return; - - const handleLogin = async () => { - if (authResult.errorCode === 0) { - try { - const loginRes = await loginWxApi({ - code: authResult.code, - }); - - if (loginRes.isSuccess) { - requestAnimationFrame(() => { - userStore.setUserInfo({ - token: loginRes.data.token, - id: loginRes.data.userId, - nickname: loginRes.data.nickname, - avatar: loginRes.data.avatar, - }); - - userStore.setAuthExpired(false); - - if (loginRes.data.family_id === 0) { - router.replace("/family/entry"); - } else { - familyStore.setCurrentFamily({ - family_id: loginRes.data.family_id, - }); - - router.replace("/(tabs)/home"); - } - }); - } - } catch (err) { - console.error("登录接口出错", err); - } finally { - setLoading(false); - } - } else { - console.log("授权失败:", authResult.errorMessage); - setLoading(false); - } - }; - - handleLogin(); - }, [authResult]); - - return ( - <> - - - - - Welcome to Allapp - - - - - - - - - - - - - - 登录即表示你同意我们的 - - 用户协议 - - 和 - - 隐私政策 - - - - - - ); -} diff --git a/code/app/app-rn4/src/app/family/user.tsx b/code/app/app-rn4/src/app/family/user.tsx index f0aa3188..d9ef9e75 100644 --- a/code/app/app-rn4/src/app/family/user.tsx +++ b/code/app/app-rn4/src/app/family/user.tsx @@ -2,11 +2,13 @@ import LayoutView from "@/components/layout/view"; import { View, Text, Pressable } from "react-native"; import PageLayout from "@/layouts/PageLayout"; import { Avatar, PressableFeedback } from "heroui-native"; -import { loadDataApi } from "@/request/api"; +import { loadDataApi, saveDataApi } from "@/request/api"; import { useUserStore } from "@/store"; import { useEffect, useState } from "react"; import { CameraIcon } from "lucide-react-native"; -import * as ImagePicker from "expo-image-picker"; +import ImagePicker from "react-native-image-crop-picker"; +import { s3UploadFileApi } from "@/request/api"; +import { buildFileUrl } from "@/utils/file"; export default function UserSetting() { const userStore = useUserStore(); @@ -36,14 +38,47 @@ export default function UserSetting() { }, []); const pickImage = async () => { - const res = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ["images"], - allowsEditing: true, - quality: 0.7, - }); + try { + const img = await ImagePicker.openPicker({ + freeStyleCropEnabled: true, + cropping: true, + compressImageQuality: 0.7, + }); - if (!res.canceled) { - console.log(res.assets[0].uri); + const file = { + uri: img.path, + name: img.filename || `photo_${Date.now()}.jpg`, + type: img.mime || "image/jpeg", + }; + + const uploadRes = await s3UploadFileApi([file]); + + if (!uploadRes.isSuccess) return; + + const key = uploadRes.data?.[0]; + if (!key) return; + + saveDataApi([ + { + table_name: "b_user", + key_field: "id", + inserts: [], + deletes: [], + updates: [ + { + id: userInfo.id, + avatar: key, + }, + ], + }, + ]).catch(console.log); + + userStore.setUserInfo({ + ...userStore.userInfo, + avatar: key, + }); + } catch (e) { + console.log("图片选择/上传失败", e); } }; @@ -64,25 +99,26 @@ export default function UserSetting() { className="w-full h-full rounded-full overflow-hidden" > - - JD + + {userInfo.name} - + { + pickImage(); + }} + className="absolute right-0 bottom-0 bg-white rounded-full p-1" + > - + - { - pickImage(); - }} - > + 基础信息 - + diff --git a/code/app/app-rn4/src/request/api.ts b/code/app/app-rn4/src/request/api.ts index 31e3cac1..f4e895bd 100644 --- a/code/app/app-rn4/src/request/api.ts +++ b/code/app/app-rn4/src/request/api.ts @@ -52,7 +52,7 @@ export function s3ListBucketsApi() { return alovaInstance.Post>(`/s3/listBuckets`); } -export function s3UploadFileApi(files: { uri: string; name: string; type: string }[]) { +export function s3UploadFileApi(files: any[]) { const form = new FormData(); files.forEach((file) => { diff --git a/code/app/app-rn4/src/store/user.ts b/code/app/app-rn4/src/store/user.ts index 264fa1da..d28c3fb8 100644 --- a/code/app/app-rn4/src/store/user.ts +++ b/code/app/app-rn4/src/store/user.ts @@ -13,6 +13,8 @@ interface UserState { authExpired: boolean; setAuthExpired: (v: boolean) => void; + + updateToken: (token: string) => void; } const defaultUserInfo: UserInfo = { @@ -46,6 +48,14 @@ export const useUserStore = create()( set({ authExpired: v, }), + + updateToken: (token: string) => + set((state) => ({ + userInfo: { + ...state.userInfo, + token, + }, + })), }), { name: "user-storage", diff --git a/code/app/app-rn4/src/utils/file.ts b/code/app/app-rn4/src/utils/file.ts new file mode 100644 index 00000000..7f74c538 --- /dev/null +++ b/code/app/app-rn4/src/utils/file.ts @@ -0,0 +1,40 @@ +const S3_URL = process.env.EXPO_PUBLIC_G3_URL ?? ""; +const S3_BUCKET = process.env.EXPO_PUBLIC_G3_BUCKET ?? ""; + +export function isS3Key(value?: string) { + if (!value) return false; + + // ❗关键特征:没有 http,且是路径结构 + return !value.startsWith("http") && /^\d{4}\/\d{2}\/\d{2}\//.test(value); +} + +export function isS3Url(url?: string) { + if (!url) return false; + + try { + const u = new URL(url); + const base = new URL(S3_URL); + + return u.origin === base.origin && url.includes(`/${S3_BUCKET}/`); + } catch { + return false; + } +} +export function buildS3Url(key: string) { + if (!key) return ""; + + // 防止重复 / + const cleanKey = key.replace(/^\/+/, ""); + + return `${S3_URL}/${S3_BUCKET}/${cleanKey}`; +} + +export function buildFileUrl(keyOrUrl?: string) { + if (!keyOrUrl) return ""; + + if (isS3Url(keyOrUrl)) return keyOrUrl; + + if (isS3Key(keyOrUrl)) return buildS3Url(keyOrUrl); + + return keyOrUrl; +}