import SwiftUI import PhotosUI /// 编辑资料:头像(PHPicker 选图上传)+ 昵称(改后 PATCH)。 struct ProfileEditView: View { @Environment(SessionStore.self) private var session @State private var nickname = "" @State private var avatarItem: PhotosPickerItem? @State private var saving = false @State private var uploading = false @State private var toast: String? private let service = UserService() var body: some View { Form { Section { HStack { Spacer() PhotosPicker(selection: $avatarItem, matching: .images) { avatarThumb } Spacer() } } Section("昵称") { TextField("起个昵称", text: $nickname) .onChange(of: nickname) { _, new in nickname = String(new.prefix(16)) } } Section { Button { Task { await saveNickname() } } label: { Text(saving ? "保存中…" : "保存").frame(maxWidth: .infinity) } .disabled(saving || nickname.isEmpty) } } .navigationTitle("编辑资料") .navigationBarTitleDisplayMode(.inline) .onAppear { if nickname.isEmpty { nickname = session.user?.nickname ?? "" } } .onChange(of: avatarItem) { _, item in if let item { Task { await uploadAvatar(item) } } } .overlay(alignment: .bottom) { toastView } } private var avatarThumb: some View { AsyncImage(url: AppConfig.mediaURL(session.user?.avatarUrl)) { phase in if case .success(let image) = phase { image.resizable().aspectRatio(contentMode: .fill) } else { Image(systemName: "person.crop.circle.fill") .resizable() .foregroundStyle(AppColors.gray300) } } .frame(width: 88, height: 88) .clipShape(Circle()) .overlay(alignment: .bottomTrailing) { Image(systemName: "camera.circle.fill") .font(.title3) .foregroundStyle(AppColors.brand) .background(Circle().fill(.white)) } .opacity(uploading ? 0.5 : 1) } @ViewBuilder private var toastView: some View { if let toast { Text(toast) .font(.subheadline) .padding(.horizontal, 16).padding(.vertical, 10) .background(.black.opacity(0.8)).foregroundStyle(.white) .clipShape(Capsule()).padding(.bottom, 30) .task(id: toast) { try? await Task.sleep(for: .seconds(2)) self.toast = nil } } } private func saveNickname() async { saving = true defer { saving = false } do { let updated = try await service.updateNickname(nickname) session.updateUser(updated) toast = "昵称已保存" } catch { toast = (error as? LocalizedError)?.errorDescription ?? "保存失败" } } private func uploadAvatar(_ item: PhotosPickerItem) async { uploading = true // 重置 avatarItem,使重选同一张图仍能再次触发(PhotosPickerItem 同图会生成新实例,但置 nil 更稳) defer { uploading = false; avatarItem = nil } guard let raw = try? await item.loadTransferable(type: Data.self), let jpeg = ImageDownsampler.jpeg(raw) else { toast = "图片读取失败" return } do { let updated = try await service.uploadAvatar(jpeg: jpeg) session.updateUser(updated) toast = "头像已更新" } catch { toast = (error as? LocalizedError)?.errorDescription ?? "上传失败" } } }