将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
11 KiB
Swift
269 lines
11 KiB
Swift
//
|
||
// AccountSwitchViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
private typealias AccountSwitchSection = Int
|
||
private typealias AccountSwitchItem = String
|
||
|
||
/// 账号切换页面,展示当前登录主体下可进入的景区账号和门店账号。
|
||
final class AccountSwitchViewController: BaseViewController, UITableViewDelegate {
|
||
|
||
private let viewModel = AccountSwitchViewModel()
|
||
private let profileAPI = NetworkServices.shared.profileAPI
|
||
private let authAPI = NetworkServices.shared.authAPI
|
||
|
||
private lazy var tableView: UITableView = {
|
||
let table = UITableView(frame: .zero, style: .plain)
|
||
table.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||
table.separatorStyle = .none
|
||
table.delegate = self
|
||
table.register(AccountSwitchCell.self, forCellReuseIdentifier: AccountSwitchCell.reuseID)
|
||
return table
|
||
}()
|
||
|
||
private let confirmButton = AppButton(title: "确认切换")
|
||
|
||
private var tableDataSource: UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>!
|
||
|
||
override func setupNavigationBar() {
|
||
title = "账号切换"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||
configureTableDataSource()
|
||
view.addSubview(tableView)
|
||
view.addSubview(confirmButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
tableView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview()
|
||
make.bottom.equalTo(confirmButton.snp.top).offset(-12)
|
||
}
|
||
confirmButton.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(14)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
self?.applyTableSnapshot(reconfigure: true)
|
||
self?.updateConfirmButton()
|
||
}
|
||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||
Task { await loadAccounts() }
|
||
}
|
||
|
||
private func configureTableDataSource() {
|
||
tableDataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
|
||
guard let self else { return UITableViewCell() }
|
||
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
|
||
guard let account = self.viewModel.accounts.first(where: { $0.id == item }) else { return cell }
|
||
cell.configure(
|
||
account: account,
|
||
selected: account.id == self.viewModel.selectedAccountId,
|
||
isCurrent: account.isCurrent || self.isCurrentAccount(account)
|
||
)
|
||
return cell
|
||
}
|
||
applyTableSnapshot(animated: false)
|
||
}
|
||
|
||
private func buildSnapshot() -> NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem> {
|
||
var snapshot = NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem>()
|
||
snapshot.appendSections([0])
|
||
snapshot.appendItems(viewModel.accounts.map(\.id), toSection: 0)
|
||
return snapshot
|
||
}
|
||
|
||
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
|
||
var snapshot = buildSnapshot()
|
||
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
|
||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||
}
|
||
tableDataSource.apply(snapshot, animatingDifferences: animated)
|
||
}
|
||
|
||
private func updateConfirmButton() {
|
||
let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching
|
||
confirmButton.isEnabled = enabled
|
||
}
|
||
|
||
private var currentAccountId: String? {
|
||
let store = AppStore.shared
|
||
if store.session.currentStoreId > 0 {
|
||
return "\(V9StoreUser.accountTypeValue)_\(store.session.currentStoreId)"
|
||
}
|
||
if store.session.currentScenicId > 0 {
|
||
return "\(V9ScenicUser.accountTypeValue)_\(store.session.userId)"
|
||
}
|
||
return store.session.userId.isEmpty ? nil : store.session.userId
|
||
}
|
||
|
||
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {
|
||
viewModel.isCurrent(account, currentAccountId: currentAccountId)
|
||
}
|
||
|
||
private func loadAccounts(force: Bool = false) async {
|
||
showLoading()
|
||
defer { hideLoading() }
|
||
do {
|
||
try await viewModel.load(api: profileAPI, force: force, currentAccountId: currentAccountId)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
@objc private func confirmTapped() {
|
||
guard let account = viewModel.selectedAccount else { return }
|
||
if account.isCurrent || isCurrentAccount(account) {
|
||
navigationController?.popViewController(animated: true)
|
||
return
|
||
}
|
||
Task { await switchAccount(account) }
|
||
}
|
||
|
||
private func switchAccount(_ account: AccountSwitchAccount) async {
|
||
showLoading()
|
||
defer { hideLoading() }
|
||
do {
|
||
let response = try await viewModel.switchAccount(account, api: authAPI)
|
||
AuthSessionHelper.applyAccountSwitch(with: response, account: account)
|
||
showToast("账号已切换")
|
||
navigationController?.popViewController(animated: true)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
tableView.deselectRow(at: indexPath, animated: true)
|
||
guard let item = tableDataSource.itemIdentifier(for: indexPath),
|
||
let account = viewModel.accounts.first(where: { $0.id == item }) else { return }
|
||
viewModel.select(account)
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 92 }
|
||
}
|
||
|
||
private final class AccountSwitchCell: UITableViewCell {
|
||
static let reuseID = "AccountSwitchCell"
|
||
|
||
private let card = UIView()
|
||
private let avatarView = UIImageView()
|
||
private let titleStackView = UIStackView()
|
||
private let titleLabel = UILabel()
|
||
private let currentTagLabel = UILabel()
|
||
private let subtitleLabel = UILabel()
|
||
private let phoneLabel = UILabel()
|
||
private let tagLabel = UILabel()
|
||
private let checkView = UIImageView()
|
||
|
||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||
selectionStyle = .none
|
||
backgroundColor = .clear
|
||
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 12
|
||
contentView.addSubview(card)
|
||
|
||
avatarView.layer.cornerRadius = 25
|
||
avatarView.clipsToBounds = true
|
||
avatarView.contentMode = .scaleAspectFill
|
||
|
||
titleStackView.axis = .horizontal
|
||
titleStackView.alignment = .center
|
||
titleStackView.spacing = 6
|
||
|
||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||
titleLabel.numberOfLines = 1
|
||
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||
currentTagLabel.font = .systemFont(ofSize: 11, weight: .regular)
|
||
currentTagLabel.text = "当前"
|
||
currentTagLabel.textColor = AppColor.primary
|
||
currentTagLabel.textAlignment = .center
|
||
currentTagLabel.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||
currentTagLabel.layer.cornerRadius = 4
|
||
currentTagLabel.clipsToBounds = true
|
||
currentTagLabel.isHidden = true
|
||
currentTagLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||
currentTagLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||
subtitleLabel.font = .systemFont(ofSize: 13)
|
||
subtitleLabel.textColor = AppColor.text666
|
||
phoneLabel.font = .systemFont(ofSize: 12)
|
||
phoneLabel.textColor = UIColor(hex: 0x9AA1AA)
|
||
tagLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
||
tagLabel.textAlignment = .center
|
||
tagLabel.layer.cornerRadius = 4
|
||
tagLabel.clipsToBounds = true
|
||
|
||
card.addSubview(avatarView)
|
||
titleStackView.addArrangedSubview(titleLabel)
|
||
titleStackView.addArrangedSubview(currentTagLabel)
|
||
|
||
card.addSubview(titleStackView)
|
||
card.addSubview(subtitleLabel)
|
||
card.addSubview(phoneLabel)
|
||
card.addSubview(tagLabel)
|
||
card.addSubview(checkView)
|
||
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
|
||
}
|
||
avatarView.snp.makeConstraints { make in
|
||
make.leading.centerY.equalToSuperview().inset(14)
|
||
make.width.height.equalTo(50)
|
||
}
|
||
titleStackView.snp.makeConstraints { make in
|
||
make.leading.equalTo(avatarView.snp.trailing).offset(13)
|
||
make.top.equalTo(avatarView).offset(2)
|
||
make.trailing.lessThanOrEqualTo(tagLabel.snp.leading).offset(-8)
|
||
}
|
||
currentTagLabel.snp.makeConstraints { make in
|
||
make.height.equalTo(19)
|
||
make.width.greaterThanOrEqualTo(34)
|
||
}
|
||
subtitleLabel.snp.makeConstraints { make in
|
||
make.leading.equalTo(titleStackView)
|
||
make.top.equalTo(titleStackView.snp.bottom).offset(4)
|
||
}
|
||
phoneLabel.snp.makeConstraints { make in
|
||
make.leading.equalTo(titleLabel)
|
||
make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
|
||
}
|
||
tagLabel.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(14)
|
||
make.top.equalToSuperview().offset(14)
|
||
make.height.equalTo(22)
|
||
make.width.greaterThanOrEqualTo(40)
|
||
}
|
||
checkView.snp.makeConstraints { make in
|
||
make.trailing.bottom.equalToSuperview().inset(14)
|
||
make.width.height.equalTo(22)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) {
|
||
titleLabel.text = account.title
|
||
currentTagLabel.isHidden = !isCurrent
|
||
subtitleLabel.text = account.subtitle
|
||
phoneLabel.text = account.phone
|
||
tagLabel.text = account.isStoreUser ? "门店" : "景区"
|
||
tagLabel.textColor = account.isStoreUser ? UIColor(hex: 0x0F9F6E) : UIColor(hex: 0x7C3AED)
|
||
tagLabel.backgroundColor = account.isStoreUser ? UIColor(hex: 0xE8F8F1) : UIColor(hex: 0xF3ECFF)
|
||
checkView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||
checkView.tintColor = selected ? AppColor.primary : UIColor(hex: 0xB6BECA)
|
||
avatarView.loadRemoteImage(urlString: account.avatar)
|
||
}
|
||
}
|