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:
2026-07-07 10:12:20 +08:00
parent 715393e78c
commit 3c4ca7eefb
23 changed files with 3186 additions and 170 deletions

View 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)
}
}
}
}
}