Add OSS upload and Kingfisher image support

This commit is contained in:
2026-06-22 16:30:05 +08:00
parent 484566d27e
commit 0b83a73509
20 changed files with 1399 additions and 134 deletions

View File

@ -5,7 +5,9 @@
// Created by Codex on 2026/6/20.
//
import PhotosUI
import SwiftUI
import UIKit
/// 退
struct ProfileView: View {
@ -16,6 +18,7 @@ struct ProfileView: View {
@Environment(AppRouter.self) private var appRouter
@Environment(RouterPath.self) private var router
@Environment(ProfileAPI.self) private var profileAPI
@Environment(OSSUploadService.self) private var ossUploadService
@Environment(ToastCenter.self) private var toastCenter
@Environment(AuthSessionCoordinator.self) private var authSessionCoordinator
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@ -23,6 +26,7 @@ struct ProfileView: View {
@State private var viewModel = ProfileViewModel()
@State private var showsPasswordSheet = false
@State private var showsLogoutConfirm = false
@State private var pickedAvatarItem: PhotosPickerItem?
@FocusState private var nicknameFocused: Bool
private var contentMaxWidth: CGFloat {
@ -83,6 +87,9 @@ struct ProfileView: View {
nicknameFocused = true
}
}
.onChange(of: pickedAvatarItem) { _, item in
Task { await prepareAvatarImage(from: item) }
}
}
private var profileBackground: some View {
@ -201,10 +208,8 @@ struct ProfileView: View {
}
private var avatarImage: some View {
Button {
toastCenter.show("头像上传待接入阿里云 OSS")
} label: {
AsyncAvatarImage(urlString: viewModel.displayAvatarURL)
PhotosPicker(selection: $pickedAvatarItem, matching: .images) {
avatarPreview
.frame(width: 86, height: 86)
.clipShape(Circle())
.overlay {
@ -221,11 +226,34 @@ struct ProfileView: View {
Circle().stroke(.white, lineWidth: 2)
}
}
.overlay {
if let progress = viewModel.avatarUploadProgress {
ZStack {
Circle()
.fill(.black.opacity(0.34))
Text("\(progress)%")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white)
}
}
}
}
.buttonStyle(.plain)
.disabled(viewModel.isSaving)
.accessibilityLabel("头像")
}
@ViewBuilder
private var avatarPreview: some View {
if let data = viewModel.pendingAvatarData, let image = UIImage(data: data) {
Image(uiImage: image)
.resizable()
.scaledToFill()
} else {
RemoteAvatarImage(urlString: viewModel.displayAvatarURL)
}
}
private var infoCard: some View {
VStack(spacing: 0) {
infoRow(title: "姓名") {
@ -503,7 +531,11 @@ struct ProfileView: View {
///
private func saveProfileEdits() async {
do {
try await viewModel.saveProfile(api: profileAPI)
try await viewModel.saveProfile(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
if let userInfo = viewModel.userInfo {
authSessionCoordinator.refreshCachedProfile(from: userInfo, accountContext: accountContext)
} else {
@ -515,6 +547,21 @@ struct ProfileView: View {
}
}
///
private func prepareAvatarImage(from item: PhotosPickerItem?) async {
guard let item else { return }
defer { pickedAvatarItem = nil }
do {
guard let data = try await item.loadTransferable(type: Data.self) else {
toastCenter.show("请选择有效的头像图片")
return
}
try viewModel.prepareAvatarImage(data: data)
} catch {
toastCenter.show(error.localizedDescription)
}
}
///
private func updatePassword(_ password: String) async {
do {
@ -533,38 +580,6 @@ struct ProfileView: View {
}
}
///
private struct AsyncAvatarImage: View {
let urlString: String
var body: some View {
if let url = URL(string: urlString), !urlString.isEmpty {
AsyncImage(url: url) { phase in
switch phase {
case let .success(image):
image
.resizable()
.scaledToFill()
case .failure, .empty:
placeholder
@unknown default:
placeholder
}
}
} else {
placeholder
}
}
private var placeholder: some View {
Image(systemName: "person.fill")
.font(.system(size: 44, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppDesign.primarySoft)
}
}
///
private struct ProfileHeaderWaveShape: Shape {
var phase: CGFloat = 0
@ -643,6 +658,7 @@ private struct PasswordUpdateSheet: View {
.environment(AppRouter())
.environment(ToastCenter())
.environment(ProfileAPI(client: APIClient()))
.environment(OSSUploadService(configService: UploadAPI(client: APIClient())))
.environment(AuthSessionCoordinator())
}
}