// // AccountSwitchViewController.swift // suixinkan // import SnapKit import UIKit // MARK: - Diffable 标识 private typealias AccountSwitchSection = Int private typealias AccountSwitchItem = String /// 账号切换列表空态标识。 private enum AccountSwitchItemID { static let empty = "accountSwitch:empty" } /// 账号切换页面,展示当前登录主体下可进入的景区账号和门店账号。 final class AccountSwitchViewController: UIViewController, UITableViewDelegate { private let viewModel = AccountSwitchViewModel() 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 lazy var confirmButton: UIButton = { let button = makePrimaryButton(title: "确认切换") button.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) return button }() /// Diffable 数据源,驱动账号列表与选中态刷新。 private var tableDataSource: UITableViewDiffableDataSource! /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = "账号切换" view.backgroundColor = UIColor(hex: 0xF5F7FB) configureTableDataSource() view.addSubview(tableView) view.addSubview(confirmButton) 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) make.height.equalTo(50) } viewModel.onChange = { [weak self] in self?.applyTableSnapshot(reconfigure: true) self?.updateConfirmButton() } Task { await loadAccounts() } } /// 配置 Diffable 数据源。 private func configureTableDataSource() { tableDataSource = UITableViewDiffableDataSource( tableView: tableView ) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: AccountSwitchItem) -> UITableViewCell? in guard let self else { return UITableViewCell() } if item == AccountSwitchItemID.empty { let cell = UITableViewCell() cell.selectionStyle = .none cell.backgroundColor = .clear cell.contentView.subviews.forEach { $0.removeFromSuperview() } let empty = makeEmptyStateView( title: "暂无可切换账号", message: "当前登录账号下没有其他可切换账号。", systemImage: "person.crop.circle.badge.exclamationmark" ) cell.contentView.addSubview(empty) empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) } return cell } guard let account = self.viewModel.accounts.first(where: { $0.id == item }) else { return UITableViewCell() } let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell cell.configure( account: account, selected: self.viewModel.selectedAccountId == account.id, isCurrent: account.isCurrent || self.isCurrentAccount(account) ) return cell } applyTableSnapshot(animated: false) } /// 构建 Diffable snapshot。 private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) if viewModel.accounts.isEmpty, !viewModel.loading { snapshot.appendItems([AccountSwitchItemID.empty], toSection: 0) } else { snapshot.appendItems(viewModel.accounts.map(\.id), toSection: 0) } return snapshot } /// 应用 snapshot 刷新列表。 private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) { var snapshot = buildTableSnapshot() if reconfigure, !snapshot.itemIdentifiers.isEmpty { snapshot.reconfigureItems(snapshot.itemIdentifiers) } tableDataSource.apply(snapshot, animatingDifferences: animated) } /// 更新ConfirmButton状态。 private func updateConfirmButton() { let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching confirmButton.isEnabled = enabled confirmButton.alpha = enabled ? 1 : 0.5 } /// 加载Accounts数据。 private func loadAccounts(force: Bool = false) async { do { try await appServices.globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") { try await self.viewModel.load( api: self.appServices.profileAPI, force: force, currentAccountId: self.currentAccountId ) } } catch { showToast(error.localizedDescription) } } private var currentAccountId: String? { guard let current = appServices.accountContext.profile else { return nil } if let store = appServices.accountContext.currentStore { return "\(V9StoreUser.accountTypeValue)_\(store.id)" } if let scenic = appServices.accountContext.currentScenic { return "\(V9ScenicUser.accountTypeValue)_\(scenic.id)" } return current.userId.isEmpty ? nil : current.userId } /// 判断isCurrentAccount条件。 private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool { viewModel.isCurrent(account, currentAccountId: currentAccountId) } /// 点击confirm的处理逻辑。 @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) } } /// switch账号相关逻辑。 private func switchAccount(_ account: AccountSwitchAccount) async { do { let response = try await appServices.globalLoading.withLoading(message: "切换中...") { try await self.viewModel.switchAccount(account, api: self.appServices.authAPI) } let username = nonEmpty(appServices.accountContext.profile?.phone) ?? nonEmpty(account.phone) ?? "" try await appServices.authSessionCoordinator.completeLogin( with: response, username: username, privacyAgreementAccepted: appServices.authSessionCoordinator.loginPreferences().privacyAgreementAccepted, appSession: appServices.appSession, accountContext: appServices.accountContext, permissionContext: appServices.permissionContext, profileAPI: appServices.profileAPI, accountContextAPI: appServices.accountContextAPI ) appServices.appNavigator.resetAllStacks() showToast("账号已切换") navigationController?.popToRootViewController(animated: true) } catch { showToast(error.localizedDescription) } } /// non空相关逻辑。 private func nonEmpty(_ value: String?) -> String? { let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? nil : text } /// UITableView 代理:处理行选中。 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let item = tableDataSource.itemIdentifier(for: indexPath), item != AccountSwitchItemID.empty, let account = viewModel.accounts.first(where: { $0.id == item }) else { return } viewModel.select(account) } /// UITableView 代理:行高。 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return 92 } return item == AccountSwitchItemID.empty ? 280 : 92 } } /// AccountSwitch列表或网格 Cell,负责单项内容展示。 private final class AccountSwitchCell: UITableViewCell { static let reuseID = "AccountSwitchCell" private let avatarView = UIImageView() private let titleLabel = 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 let card = UIView() card.backgroundColor = .white card.layer.cornerRadius = 12 contentView.addSubview(card) avatarView.layer.cornerRadius = 25 avatarView.clipsToBounds = true avatarView.contentMode = .scaleAspectFill titleLabel.font = .systemFont(ofSize: 16, weight: .semibold) subtitleLabel.font = .systemFont(ofSize: 13) subtitleLabel.textColor = AppDesignUIKit.textSecondary 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) card.addSubview(titleLabel) 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) } titleLabel.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) } subtitleLabel.snp.makeConstraints { make in make.leading.equalTo(titleLabel) make.top.equalTo(titleLabel.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.equalToSuperview().inset(14) make.bottom.equalToSuperview().inset(14) make.width.height.equalTo(22) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } /// 配置展示内容。 func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) { let title = account.title.trimmingCharacters(in: .whitespacesAndNewlines) titleLabel.text = title.isEmpty ? account.accountTypeLabel : title 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 ? AppDesignUIKit.primary : UIColor(hex: 0xB6BECA) avatarView.loadRemoteAvatar(urlString: account.avatar) if isCurrent { titleLabel.text = (titleLabel.text ?? "") + " (当前)" } } }