实现我的 Tab、对齐 Android 流程并接入 OSS 上传
This commit is contained in:
@ -3,26 +3,430 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 我的 Tab 根页面。
|
||||
/// 我的 Tab 根页面,展示个人信息、账号状态与子流程入口。
|
||||
final class ProfileViewController: BaseViewController {
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
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() {
|
||||
title = "我的"
|
||||
titleLabel.text = "我的"
|
||||
titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.textColor = AppColor.text333
|
||||
view.addSubview(titleLabel)
|
||||
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() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user