Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
383 lines
14 KiB
Swift
383 lines
14 KiB
Swift
//
|
||
// ProfileViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 我的 Tab 根页面,展示个人信息、账号状态与子流程入口。
|
||
final class ProfileViewController: BaseViewController {
|
||
|
||
private let viewModel = ProfileViewModel()
|
||
private let profileAPI = NetworkServices.shared.profileAPI
|
||
|
||
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)
|
||
private var hasLoadedProfileOnce = false
|
||
|
||
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
|
||
Task { @MainActor 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)
|
||
|
||
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) {
|
||
super.viewWillAppear(animated)
|
||
let showLoading = !hasLoadedProfileOnce
|
||
Task {
|
||
await reloadProfile(showGlobalLoading: showLoading)
|
||
if showLoading {
|
||
hasLoadedProfileOnce = true
|
||
}
|
||
}
|
||
}
|
||
|
||
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.displayNickname
|
||
uidLabel.text = "UID:\(viewModel.displayUID)"
|
||
avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL)
|
||
|
||
nameRow.configure(value: viewModel.displayRealName)
|
||
accountRow.configureAccountInfo(
|
||
storeName: viewModel.accountDisplayName,
|
||
accountTypeLabel: viewModel.accountTypeLabel,
|
||
isStoreAccount: viewModel.isStoreAccount
|
||
)
|
||
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
|
||
)
|
||
}
|
||
|
||
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(showGlobalLoading: false)
|
||
refreshControl.endRefreshing()
|
||
}
|
||
}
|
||
|
||
@objc private func handleAccountDidSwitch() {
|
||
Task { await reloadProfile(showGlobalLoading: true) }
|
||
}
|
||
|
||
@objc private func handleProfileDidUpdate() {
|
||
viewModel.applyLocalProfileUpdate()
|
||
applyViewModel()
|
||
}
|
||
|
||
@objc private func editTapped() {
|
||
let editVC = ProfileEditViewController(
|
||
nickname: viewModel.displayNickname,
|
||
avatarURL: viewModel.displayAvatarURL
|
||
)
|
||
navigationController?.pushViewController(editVC, 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(showGlobalLoading: Bool) async {
|
||
let shouldShowOverlay = showGlobalLoading && !refreshControl.isRefreshing
|
||
if shouldShowOverlay {
|
||
showLoading()
|
||
}
|
||
defer {
|
||
if shouldShowOverlay {
|
||
hideLoading()
|
||
}
|
||
}
|
||
do {
|
||
try await viewModel.reload(api: profileAPI)
|
||
applyViewModel()
|
||
} 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)
|
||
}
|
||
}
|
||
}
|