diff --git a/backend/internal/modules/file/handler.go b/backend/internal/modules/file/handler.go index 7c9e1a1..92df9ab 100644 --- a/backend/internal/modules/file/handler.go +++ b/backend/internal/modules/file/handler.go @@ -65,7 +65,7 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { response.BadRequest(c, "文件 key 不正确") return } - if publicOnly && !strings.HasPrefix(key, "home-banner/") { + if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index 52292ec..a388da1 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -50,7 +50,7 @@ func (s *Service) Upload(req uploadRequest) (*UploadDTO, error) { return nil, err } fileURL := "/api/files/object?key=" + url.QueryEscape(key) - if scene == "home-banner" { + if scene == "home-banner" || scene == "avatar" { fileURL = "/api/public/files/object?key=" + url.QueryEscape(key) } return &UploadDTO{ diff --git a/frontend/src/views/mobile/MobileProfileView.vue b/frontend/src/views/mobile/MobileProfileView.vue index a09705a..988e20f 100644 --- a/frontend/src/views/mobile/MobileProfileView.vue +++ b/frontend/src/views/mobile/MobileProfileView.vue @@ -7,6 +7,7 @@ import MobileBottomNav from "@/components/MobileBottomNav.vue"; import { fetchWalletBalance, fetchWalletLedger, type WalletLedger } from "@/api/wallet"; import { formatDateMinute } from "@/utils/time"; +import { uploadFile } from "@/api/files"; const session = useSessionStore(); const router = useRouter(); @@ -22,6 +23,38 @@ const profileForm = reactive({ avatar_url: "", }); +const avatarFileInput = ref(null); +const uploadingAvatar = ref(false); + +function triggerAvatarUpload() { + avatarFileInput.value?.click(); +} + +async function handleAvatarFileChange(event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ""; + if (!file) return; + + uploadingAvatar.value = true; + const toast = showToast({ + type: "loading", + message: "上传中...", + forbidClick: true, + duration: 0, + }); + + try { + const uploaded = await uploadFile(file, "avatar"); + profileForm.avatar_url = uploaded.url; + showToast({ message: "上传成功", icon: "passed" }); + } catch (error) { + showToast({ message: readError(error, "上传失败"), icon: "cross" }); + } finally { + uploadingAvatar.value = false; + } +} + // 收支明细列表 const showLedgers = ref(false); const loadingLedgers = ref(false); @@ -191,6 +224,14 @@ const maskedPhone = computed(() => : "登录后查看手机号" ); const avatarText = computed(() => displayName.value.slice(0, 1)); + +function resolveAvatarURL(url: string | undefined | null) { + if (!url) return ""; + if (url.includes("/api/files/object?key=avatar/")) { + return url.replace("/api/files/object", "/api/public/files/object"); + } + return url; +}