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