Files
suixinkan_uikit/suixinkan/UI/Profile/ProfileViewController.swift

433 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ProfileViewController.swift
// suixinkan
//
import PhotosUI
import SnapKit
import UIKit
/// Tab
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()
private let refreshControl = UIRefreshControl()
private let avatarImageView = UIImageView()
private let nicknameField = UITextField()
private let uidLabel = UILabel()
private let editButton = AppButton(title: "编辑", style: .primary)
private let infoCard = UIStackView()
private let nameRow = ProfileInfoRowView(title: "姓名")
private let accountRow = ProfileInfoRowView(title: "当前账号")
private let phoneRow = ProfileInfoRowView(title: "手机号")
private let withdrawalRow = ProfileInfoRowView(title: "提现设置")
private let passwordRow = ProfileInfoRowView(title: "修改密码")
private let realNameRow = ProfileInfoRowView(title: "认证状态")
private let statusRow = ProfileInfoRowView(title: "账号状态")
private let scenicRow = ProfileInfoRowView(title: "当前景区")
private let logoutButton = UIButton(type: .system)
override func setupNavigationBar() {
title = "个人信息"
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF7FAFF)
scrollView.alwaysBounceVertical = true
scrollView.refreshControl = refreshControl
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
contentStack.axis = .vertical
contentStack.spacing = 24
contentStack.layoutMargins = UIEdgeInsets(top: 16, left: 15, bottom: 24, right: 15)
contentStack.isLayoutMarginsRelativeArrangement = true
setupHeader()
setupInfoCard()
setupLogoutButton()
contentStack.addArrangedSubview(headerContainer)
contentStack.addArrangedSubview(infoCardWrapper)
contentStack.addArrangedSubview(logoutButton)
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(scrollView.snp.width)
}
logoutButton.snp.makeConstraints { make in
make.height.equalTo(46)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
self?.applyViewModel()
}
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside)
accountRow.addTarget(self, action: #selector(accountSwitchTapped), for: .touchUpInside)
passwordRow.addTarget(self, action: #selector(changePasswordTapped), for: .touchUpInside)
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
)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task { await reloadProfile(showLoading: false) }
}
private let headerContainer = UIView()
private let infoCardWrapper = UIView()
private func setupHeader() {
avatarImageView.layer.cornerRadius = 43
avatarImageView.clipsToBounds = true
avatarImageView.contentMode = .scaleAspectFill
nicknameField.font = .systemFont(ofSize: 18, weight: .medium)
nicknameField.textColor = AppColor.text333
nicknameField.borderStyle = .none
nicknameField.isEnabled = false
uidLabel.font = .systemFont(ofSize: 14, weight: .semibold)
uidLabel.textColor = UIColor(hex: 0x9CA3AF)
editButton.layer.cornerRadius = 18
editButton.snp.updateConstraints { make in
make.height.equalTo(36)
}
editButton.titleLabel?.font = .systemFont(ofSize: 12)
headerContainer.addSubview(avatarImageView)
headerContainer.addSubview(nicknameField)
headerContainer.addSubview(uidLabel)
headerContainer.addSubview(editButton)
avatarImageView.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.width.height.equalTo(86)
}
nicknameField.snp.makeConstraints { make in
make.leading.equalTo(avatarImageView.snp.trailing).offset(16)
make.trailing.lessThanOrEqualTo(editButton.snp.leading).offset(-8)
make.top.equalTo(avatarImageView).offset(12)
}
uidLabel.snp.makeConstraints { make in
make.leading.equalTo(nicknameField)
make.top.equalTo(nicknameField.snp.bottom).offset(8)
}
editButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(avatarImageView)
make.width.equalTo(80)
make.height.equalTo(36)
}
headerContainer.snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(86)
}
}
private func setupInfoCard() {
infoCard.axis = .vertical
infoCard.backgroundColor = .white
infoCard.layer.cornerRadius = 8
infoCard.clipsToBounds = true
infoCard.isLayoutMarginsRelativeArrangement = true
infoCard.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
infoCardWrapper.addSubview(infoCard)
infoCard.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
[
nameRow, accountRow, phoneRow, withdrawalRow,
passwordRow, realNameRow, statusRow, scenicRow
].forEach { infoCard.addArrangedSubview($0) }
nameRow.isUserInteractionEnabled = false
phoneRow.isUserInteractionEnabled = false
statusRow.isUserInteractionEnabled = false
scenicRow.isUserInteractionEnabled = false
scenicRow.showsDivider = false
}
private func setupLogoutButton() {
logoutButton.setTitle("退出登录", for: .normal)
logoutButton.setTitleColor(UIColor(hex: 0xEF4444), for: .normal)
logoutButton.titleLabel?.font = .systemFont(ofSize: 14)
logoutButton.backgroundColor = .white
logoutButton.layer.cornerRadius = 8
}
private func applyViewModel() {
nicknameField.text = viewModel.isEditingProfile ? viewModel.editingNickname : viewModel.displayNickname
nicknameField.isEnabled = viewModel.isEditingProfile
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
nameRow.configure(value: viewModel.displayRealName)
accountRow.configure(
value: viewModel.accountDisplayName,
accessory: .action("切换账号")
)
phoneRow.configure(value: viewModel.displayPhone)
withdrawalRow.isHidden = !viewModel.showPhotographerFields
realNameRow.isHidden = !viewModel.showPhotographerFields
if viewModel.showPhotographerFields {
configureWithdrawalRow()
configureRealNameRow()
}
passwordRow.configure(value: "点击修改密码", valueColor: UIColor(hex: 0x9CA3AF), accessory: .chevron)
statusRow.configure(
value: viewModel.accountStatusText,
badge: viewModel.accountStatusText == "--" ? nil : .accountActive
)
scenicRow.configure(
value: viewModel.currentScenicName,
badge: .scenic
)
if viewModel.isLoading, !refreshControl.isRefreshing {
showLoading()
} else {
hideLoading()
}
}
private func configureWithdrawalRow() {
let info = viewModel.bankCardInfo
let text = viewModel.bankCardDisplayText
var valueColor = UIColor(hex: 0x9CA3AF)
var badge: ProfileStatusBadgeView.Style?
if let info {
switch info.auditStatus {
case 2:
valueColor = AppColor.text333
case 3:
valueColor = UIColor(hex: 0xEF4444)
badge = .bankRejected
default:
valueColor = UIColor(hex: 0xFF7B00)
badge = .bankPending
}
}
withdrawalRow.configure(
value: text,
valueColor: valueColor,
accessory: .chevron,
badge: badge
)
_ = info
}
private func configureRealNameRow() {
if let info = viewModel.realNameInfo {
let style: ProfileStatusBadgeView.Style
switch info.auditStatus {
case 2: style = .auditApproved
case 3: style = .auditRejected
default: style = .auditPending
}
realNameRow.configure(
value: viewModel.realNameStatusText,
accessory: .chevron,
badge: style
)
} else {
realNameRow.configure(
value: "点击去实名认证",
valueColor: UIColor(hex: 0x9CA3AF),
accessory: .chevron
)
}
}
@objc private func refreshPulled() {
Task {
await reloadProfile(showLoading: false)
refreshControl.endRefreshing()
}
}
@objc private func handleAccountDidSwitch() {
Task { await reloadProfile(showLoading: true) }
}
@objc private func nicknameChanged() {
viewModel.updateEditingNickname(nicknameField.text ?? "")
}
@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)
}
@objc private func accountSwitchTapped() {
navigationController?.pushViewController(AccountSwitchViewController(), animated: true)
}
@objc private func changePasswordTapped() {
let alert = UIAlertController(title: "修改密码", message: "请输入新密码(至少 6 位)", preferredStyle: .alert)
alert.addTextField { field in
field.isSecureTextEntry = true
field.placeholder = "新密码"
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
guard let password = alert.textFields?.first?.text else { return }
Task { await self?.updatePassword(password) }
})
present(alert, animated: true)
}
@objc private func withdrawalTapped() {
if let bankCard = viewModel.bankCardInfo, bankCard.auditStatus != 0 {
navigationController?.pushViewController(
WithdrawalSettingsAuditViewController(bankCard: bankCard),
animated: true
)
return
}
if viewModel.realNameInfo?.verified != true {
showToast("请实名认证成功后再试")
return
}
navigationController?.pushViewController(WithdrawalSettingsViewController(), animated: true)
}
@objc private func realNameTapped() {
if viewModel.realNameInfo == nil {
navigationController?.pushViewController(RealNameAuthViewController(), animated: true)
} else if let info = viewModel.realNameInfo {
navigationController?.pushViewController(
RealNameAuthAuditViewController(info: info),
animated: true
)
}
}
@objc private func logoutTapped() {
let alert = UIAlertController(title: "确定退出登录?", message: "退出后将需要重新登录", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in
NotificationCenter.default.post(name: NotificationName.userDidLogout, object: nil)
})
present(alert, animated: true)
}
private func reloadProfile(showLoading: Bool) async {
if showLoading { self.showLoading() }
defer {
if showLoading { hideLoading() }
}
do {
try await viewModel.reload(api: profileAPI)
} catch {
showToast(error.localizedDescription)
}
}
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() }
do {
try await viewModel.updatePassword(password, api: profileAPI)
showToast("密码修改成功")
} catch {
showToast(error.localizedDescription)
}
}
}
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)
}
}
}
}
}