Add OSS upload and Kingfisher image support
This commit is contained in:
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user