98b7600797
对标安卓正式版、对接 app-server(8770)的 SwiftUI 客户端。iOS 无障碍受限、无比价核心,做自成一体:美团 CPS 导购 + 福利(签到/任务/金币)+ 查看(比价记录/省钱战绩)。 功能完整 M0-M5:脚手架、短信登录(JWT+Keychain+401 自动刷新)、首页券 feed 导购、比价记录+省钱战绩、福利、我的(资料/反馈/设置/注销)。 单 target、0 三方依赖、XcodeGen 生成工程;Debug/Release 双 plist 分流 ATS。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
4.0 KiB
Swift
114 lines
4.0 KiB
Swift
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 ?? "上传失败"
|
|
}
|
|
}
|
|
}
|