Add submit-task flow aligned with Android and extract profile edit page.
Implement task creation with cloud/local media, order linking, and OSS upload; wire home entry and move profile nickname/avatar editing to ProfileEditViewController. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
195
suixinkan/UI/Profile/ProfileEditViewController.swift
Normal file
195
suixinkan/UI/Profile/ProfileEditViewController.swift
Normal file
@ -0,0 +1,195 @@
|
||||
//
|
||||
// ProfileEditViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 资料编辑页,支持修改头像与昵称并保存到服务端。
|
||||
final class ProfileEditViewController: BaseViewController {
|
||||
|
||||
private let viewModel: ProfileEditViewModel
|
||||
private let profileAPI = NetworkServices.shared.profileAPI
|
||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||
|
||||
private let avatarContainer = UIView()
|
||||
private let avatarImageView = UIImageView()
|
||||
private let avatarHintLabel = UILabel()
|
||||
private let nicknameCard = UIView()
|
||||
private let nicknameTitleLabel = UILabel()
|
||||
private let nicknameField = UITextField()
|
||||
private let saveButton = AppButton(title: "保存", style: .primary)
|
||||
|
||||
/// 使用「我的」页当前展示的资料快照进入编辑。
|
||||
init(nickname: String, avatarURL: String) {
|
||||
viewModel = ProfileEditViewModel(nickname: nickname, avatarURL: avatarURL)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "编辑资料"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackgroundSoft
|
||||
|
||||
avatarImageView.layer.cornerRadius = 48
|
||||
avatarImageView.clipsToBounds = true
|
||||
avatarImageView.contentMode = .scaleAspectFill
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
|
||||
avatarHintLabel.text = "点击更换头像"
|
||||
avatarHintLabel.font = .systemFont(ofSize: 14)
|
||||
avatarHintLabel.textColor = AppColor.textTertiary
|
||||
avatarHintLabel.textAlignment = .center
|
||||
|
||||
avatarContainer.addSubview(avatarImageView)
|
||||
avatarContainer.addSubview(avatarHintLabel)
|
||||
view.addSubview(avatarContainer)
|
||||
|
||||
nicknameCard.backgroundColor = .white
|
||||
nicknameCard.layer.cornerRadius = AppRadius.sm
|
||||
view.addSubview(nicknameCard)
|
||||
|
||||
nicknameTitleLabel.text = "昵称"
|
||||
nicknameTitleLabel.font = .systemFont(ofSize: 14)
|
||||
nicknameTitleLabel.textColor = AppColor.textTertiary
|
||||
|
||||
nicknameField.font = .systemFont(ofSize: 16)
|
||||
nicknameField.textColor = AppColor.text333
|
||||
nicknameField.placeholder = "请输入昵称"
|
||||
nicknameField.clearButtonMode = .whileEditing
|
||||
nicknameField.returnKeyType = .done
|
||||
|
||||
nicknameCard.addSubview(nicknameTitleLabel)
|
||||
nicknameCard.addSubview(nicknameField)
|
||||
|
||||
view.addSubview(saveButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
avatarContainer.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(32)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.top.centerX.equalToSuperview()
|
||||
make.width.height.equalTo(96)
|
||||
}
|
||||
avatarHintLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(avatarImageView.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
|
||||
nicknameCard.snp.makeConstraints { make in
|
||||
make.top.equalTo(avatarContainer.snp.bottom).offset(32)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
}
|
||||
nicknameTitleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().inset(16)
|
||||
}
|
||||
nicknameField.snp.makeConstraints { make in
|
||||
make.top.equalTo(nicknameTitleLabel.snp.bottom).offset(8)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(16)
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
|
||||
saveButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
|
||||
avatarImageView.addGestureRecognizer(
|
||||
UITapGestureRecognizer(target: self, action: #selector(pickAvatar))
|
||||
)
|
||||
nicknameField.addTarget(self, action: #selector(nicknameChanged), for: .editingChanged)
|
||||
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
|
||||
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
nicknameField.text = viewModel.editingNickname
|
||||
|
||||
if let pendingData = viewModel.pendingAvatarData, let image = UIImage(data: pendingData) {
|
||||
avatarImageView.image = image
|
||||
} else {
|
||||
avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL)
|
||||
}
|
||||
|
||||
saveButton.isEnabled = !viewModel.isSaving
|
||||
avatarImageView.alpha = viewModel.isSaving ? 0.6 : 1
|
||||
nicknameField.isEnabled = !viewModel.isSaving
|
||||
}
|
||||
|
||||
@objc private func nicknameChanged() {
|
||||
viewModel.updateEditingNickname(nicknameField.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func pickAvatar() {
|
||||
guard !viewModel.isSaving else { return }
|
||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||
configuration.filter = .images
|
||||
configuration.selectionLimit = 1
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self
|
||||
present(picker, animated: true)
|
||||
}
|
||||
|
||||
@objc private func saveTapped() {
|
||||
nicknameField.resignFirstResponder()
|
||||
Task { await saveProfile() }
|
||||
}
|
||||
|
||||
private func saveProfile() async {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
showToast("当前景区信息缺失")
|
||||
return
|
||||
}
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.saveProfile(
|
||||
api: profileAPI,
|
||||
uploader: ossUploadService,
|
||||
scenicId: scenicId
|
||||
)
|
||||
showToast("保存成功")
|
||||
navigationController?.popViewController(animated: true)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileEditViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { return }
|
||||
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
|
||||
guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try self.viewModel.prepareAvatarImage(data: data)
|
||||
} catch {
|
||||
self.showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
@ -12,7 +11,6 @@ final class ProfileViewController: BaseViewController {
|
||||
|
||||
private let viewModel = ProfileViewModel()
|
||||
private let profileAPI = NetworkServices.shared.profileAPI
|
||||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
@ -35,7 +33,7 @@ final class ProfileViewController: BaseViewController {
|
||||
private let logoutButton = UIButton(type: .system)
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "个人信息"
|
||||
title = "我的"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
@ -87,18 +85,18 @@ final class ProfileViewController: BaseViewController {
|
||||
withdrawalRow.addTarget(self, action: #selector(withdrawalTapped), for: .touchUpInside)
|
||||
realNameRow.addTarget(self, action: #selector(realNameTapped), for: .touchUpInside)
|
||||
|
||||
nicknameField.addTarget(self, action: #selector(nicknameChanged), for: .editingChanged)
|
||||
|
||||
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(pickAvatar))
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
avatarImageView.addGestureRecognizer(avatarTap)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAccountDidSwitch),
|
||||
name: NotificationName.accountDidSwitch,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleProfileDidUpdate),
|
||||
name: NotificationName.userProfileDidUpdate,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -191,19 +189,9 @@ final class ProfileViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
nicknameField.text = viewModel.isEditingProfile ? viewModel.editingNickname : viewModel.displayNickname
|
||||
nicknameField.isEnabled = viewModel.isEditingProfile
|
||||
nicknameField.text = viewModel.displayNickname
|
||||
uidLabel.text = "UID:\(viewModel.displayUID)"
|
||||
|
||||
if let pendingData = viewModel.pendingAvatarData, let image = UIImage(data: pendingData) {
|
||||
avatarImageView.image = image
|
||||
} else {
|
||||
avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL)
|
||||
}
|
||||
|
||||
editButton.setTitle(viewModel.isEditingProfile ? "完成" : "编辑", for: .normal)
|
||||
editButton.isEnabled = !viewModel.isSaving
|
||||
avatarImageView.alpha = viewModel.isEditingProfile ? 1 : 1
|
||||
avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL)
|
||||
|
||||
nameRow.configure(value: viewModel.displayRealName)
|
||||
accountRow.configure(
|
||||
@ -289,26 +277,17 @@ final class ProfileViewController: BaseViewController {
|
||||
Task { await reloadProfile(showGlobalLoading: true) }
|
||||
}
|
||||
|
||||
@objc private func nicknameChanged() {
|
||||
viewModel.updateEditingNickname(nicknameField.text ?? "")
|
||||
@objc private func handleProfileDidUpdate() {
|
||||
viewModel.applyLocalProfileUpdate()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
@objc private func editTapped() {
|
||||
if viewModel.isEditingProfile {
|
||||
Task { await saveProfile() }
|
||||
} else {
|
||||
viewModel.beginEditing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func pickAvatar() {
|
||||
guard viewModel.isEditingProfile else { return }
|
||||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||
configuration.filter = .images
|
||||
configuration.selectionLimit = 1
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self
|
||||
present(picker, animated: true)
|
||||
let editVC = ProfileEditViewController(
|
||||
nickname: viewModel.displayNickname,
|
||||
avatarURL: viewModel.displayAvatarURL
|
||||
)
|
||||
navigationController?.pushViewController(editVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func accountSwitchTapped() {
|
||||
@ -382,26 +361,6 @@ final class ProfileViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func saveProfile() async {
|
||||
let scenicId = AppStore.shared.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
showToast("当前景区信息缺失")
|
||||
return
|
||||
}
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
do {
|
||||
try await viewModel.saveProfile(
|
||||
api: profileAPI,
|
||||
uploader: ossUploadService,
|
||||
scenicId: scenicId
|
||||
)
|
||||
showToast("保存成功")
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePassword(_ password: String) async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
@ -413,20 +372,3 @@ final class ProfileViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { return }
|
||||
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
|
||||
guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try self.viewModel.prepareAvatarImage(data: data)
|
||||
} catch {
|
||||
self.showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user