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

394 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 settingRow = ProfileInfoRowView(title: "设置中心")
private let logoutButton = UIButton(type: .system)
private var hasLoadedProfileOnce = false
override func setupNavigationBar() {
title = "我的"
}
override func viewDidLoad() {
super.viewDidLoad()
applyViewModel()
}
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)
settingRow.addTarget(self, action: #selector(settingTapped), 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 && !viewModel.hasCachedBasicInfo
Task {
await reloadProfile(showGlobalLoading: 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,
settingRow
].forEach { infoCard.addArrangedSubview($0) }
nameRow.isUserInteractionEnabled = false
phoneRow.isUserInteractionEnabled = false
statusRow.isUserInteractionEnabled = false
scenicRow.isUserInteractionEnabled = false
settingRow.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
)
settingRow.configure(value: nil, accessory: .chevron)
}
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 settingTapped() {
navigationController?.pushViewController(SettingViewController(), 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)
}
}
}