Files
shaguabijia-app-ios/App/Features/Profile/ProfileEditView.swift
T
marco 98b7600797 初始化傻瓜比价正式版 iOS 客户端
对标安卓正式版、对接 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>
2026-06-05 00:54:11 +08:00

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 ?? "上传失败"
}
}
}