// // AccountSelectionViewController.swift // suixinkan // import SnapKit import UIKit // MARK: - Diffable 标识 private typealias AccountSelectionSection = Int private typealias AccountSelectionItem = String @MainActor /// 多账号登录时的账号选择页,展示景区/门店账号列表并提交选择。 final class AccountSelectionViewController: UIViewController, UITableViewDelegate { private let payload: AccountSelectionPayload private var isLoading: Bool private let onCancel: () -> Void private let onConfirm: (AccountSwitchAccount) -> Void private var selectedAccountId: String? private let tableView = UITableView(frame: .zero, style: .plain) /// Diffable 数据源,驱动账号列表与选中态刷新。 private var tableDataSource: UITableViewDiffableDataSource! private let confirmButton = UIButton(type: .system) private let bottomBar = UIView() /// 初始化实例。 init( payload: AccountSelectionPayload, isLoading: Bool, onCancel: @escaping () -> Void, onConfirm: @escaping (AccountSwitchAccount) -> Void ) { self.payload = payload self.isLoading = isLoading self.onCancel = onCancel self.onConfirm = onConfirm super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = "选择账号" view.backgroundColor = UIColor(hex: 0xF5F7FB) navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelTapped)) isModalInPresentation = isLoading selectedAccountId = payload.accounts.first?.id configureTableView() configureTableDataSource() configureBottomBar() } /// 配置 Diffable 数据源。 private func configureTableDataSource() { tableDataSource = UITableViewDiffableDataSource( tableView: tableView ) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: AccountSelectionItem) -> UITableViewCell? in guard let self, let cell = tableView.dequeueReusableCell( withIdentifier: AccountSelectionCell.reuseIdentifier, for: indexPath ) as? AccountSelectionCell, let account = self.payload.accounts.first(where: { $0.id == item }) else { return UITableViewCell() } cell.configure(account: account, selected: account.id == self.selectedAccountId) return cell } applyTableSnapshot(animated: false) } /// 构建 Diffable snapshot。 private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(payload.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) } /// 更新Loading状态。 func updateLoading(_ loading: Bool) { isLoading = loading isModalInPresentation = loading confirmButton.isEnabled = canConfirm confirmButton.backgroundColor = canConfirm ? AppDesign.primary : UIColor(hex: 0xC9CED6) } private var selectedAccount: AccountSwitchAccount? { payload.accounts.first { $0.id == selectedAccountId } } private var canConfirm: Bool { selectedAccount != nil && !isLoading } /// 配置TableView展示内容。 private func configureTableView() { tableView.backgroundColor = .clear tableView.separatorStyle = .none tableView.delegate = self tableView.register(AccountSelectionCell.self, forCellReuseIdentifier: AccountSelectionCell.reuseIdentifier) view.addSubview(tableView) tableView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide).inset(90) } } /// 配置BottomBar展示内容。 private func configureBottomBar() { bottomBar.backgroundColor = .white let divider = UIView() divider.backgroundColor = UIColor.separator confirmButton.setTitle("进入系统", for: .normal) confirmButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold) confirmButton.setTitleColor(.white, for: .normal) confirmButton.layer.cornerRadius = AppMetrics.CornerRadius.button confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) updateLoading(isLoading) view.addSubview(bottomBar) bottomBar.addSubview(divider) bottomBar.addSubview(confirmButton) bottomBar.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() } divider.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(0.5) } confirmButton.snp.makeConstraints { make in make.top.equalTo(divider.snp.bottom).offset(AppMetrics.Spacing.medium) make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium) make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppMetrics.Spacing.medium) make.height.equalTo(AppMetrics.ControlSize.primaryButtonHeight) } } /// 点击cancel的处理逻辑。 @objc private func cancelTapped() { onCancel() dismiss(animated: true) } /// 点击confirm的处理逻辑。 @objc private func confirmTapped() { guard let selectedAccount else { return } onConfirm(selectedAccount) } } extension AccountSelectionViewController { /// UITableView 代理:处理行选中。 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return } selectedAccountId = item applyTableSnapshot(animated: false, reconfigure: true) confirmButton.isEnabled = canConfirm confirmButton.backgroundColor = canConfirm ? AppDesign.primary : UIColor(hex: 0xC9CED6) } } /// 账号选择列表 Cell。 private final class AccountSelectionCell: UITableViewCell { static let reuseIdentifier = "AccountSelectionCell" private let avatarView = UIView() private let avatarLabel = UILabel() private let titleLabel = UILabel() private let subtitleLabel = UILabel() private let phoneLabel = UILabel() private let typeTag = UILabel() private let currentTag = UILabel() private let checkmark = UIImageView() /// 初始化实例。 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.backgroundColor = .white contentView.layer.cornerRadius = AppMetrics.CornerRadius.button contentView.layer.masksToBounds = true avatarView.layer.cornerRadius = AppMetrics.ControlSize.primaryButtonHeight / 2 avatarLabel.font = .systemFont(ofSize: AppMetrics.FontSize.callout, weight: .semibold) titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold) titleLabel.textColor = AppDesign.textPrimary subtitleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.footnote) subtitleLabel.textColor = AppDesign.textSecondary phoneLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption) phoneLabel.textColor = UIColor(hex: 0x9AA1AA) typeTag.font = .systemFont(ofSize: 11, weight: .semibold) typeTag.textAlignment = .center typeTag.layer.cornerRadius = AppMetrics.Spacing.xxSmall typeTag.clipsToBounds = true currentTag.font = .systemFont(ofSize: 11, weight: .semibold) currentTag.textAlignment = .center currentTag.text = "当前" currentTag.textColor = AppDesign.primary currentTag.backgroundColor = AppDesign.primarySoft currentTag.layer.cornerRadius = AppMetrics.Spacing.xxSmall currentTag.clipsToBounds = true currentTag.isHidden = true checkmark.contentMode = .scaleAspectFit contentView.addSubview(avatarView) avatarView.addSubview(avatarLabel) contentView.addSubview(titleLabel) contentView.addSubview(subtitleLabel) contentView.addSubview(phoneLabel) contentView.addSubview(typeTag) contentView.addSubview(currentTag) contentView.addSubview(checkmark) avatarView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(AppMetrics.FontSize.subheadline) make.centerY.equalToSuperview() make.width.height.equalTo(AppMetrics.ControlSize.primaryButtonHeight) } avatarLabel.snp.makeConstraints { make in make.center.equalToSuperview() } titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(AppMetrics.FontSize.subheadline) make.leading.equalTo(avatarView.snp.trailing).offset(AppMetrics.FontSize.footnote) make.trailing.lessThanOrEqualTo(checkmark.snp.leading).offset(-8) } currentTag.snp.makeConstraints { make in make.leading.equalTo(titleLabel.snp.trailing).offset(AppMetrics.Spacing.xSmall) make.centerY.equalTo(titleLabel) make.height.equalTo(AppMetrics.Spacing.sheet) } subtitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.xxSmall) make.leading.equalTo(titleLabel) make.trailing.equalTo(checkmark.snp.leading).offset(-8) } phoneLabel.snp.makeConstraints { make in make.top.equalTo(subtitleLabel.snp.bottom).offset(AppMetrics.Spacing.xxSmall) make.leading.equalTo(titleLabel) make.bottom.equalToSuperview().inset(AppMetrics.FontSize.subheadline) } typeTag.snp.makeConstraints { make in make.trailing.equalTo(checkmark.snp.leading).offset(-AppMetrics.Spacing.xSmall) make.top.equalTo(titleLabel) make.height.equalTo(AppMetrics.Spacing.sheet) } checkmark.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(AppMetrics.FontSize.subheadline) make.centerY.equalToSuperview() make.width.height.equalTo(AppMetrics.ControlSize.passwordIcon) } } required init?(coder: NSCoder) { nil } /// 配置展示内容。 func configure(account: AccountSwitchAccount, selected: Bool) { titleLabel.text = account.title.isEmpty ? account.accountTypeLabel : account.title subtitleLabel.text = account.subtitle subtitleLabel.isHidden = account.subtitle.isEmpty phoneLabel.text = account.phone phoneLabel.isHidden = account.phone.isEmpty currentTag.isHidden = !account.isCurrent if account.isStoreUser { avatarView.backgroundColor = UIColor(hex: 0xE8F8F1) avatarLabel.text = "店" avatarLabel.textColor = UIColor(hex: 0x0F9F6E) typeTag.text = " 门店 " typeTag.textColor = UIColor(hex: 0x0F9F6E) typeTag.backgroundColor = UIColor(hex: 0xE8F8F1) } else { avatarView.backgroundColor = UIColor(hex: 0xF3ECFF) avatarLabel.text = "景" avatarLabel.textColor = UIColor(hex: 0x7C3AED) typeTag.text = " 景区 " typeTag.textColor = UIColor(hex: 0x7C3AED) typeTag.backgroundColor = UIColor(hex: 0xF3ECFF) } let symbolName = selected ? "checkmark.circle.fill" : "circle" checkmark.image = UIImage(systemName: symbolName) checkmark.tintColor = selected ? AppDesign.primary : UIColor(hex: 0xB6BECA) contentView.layer.borderWidth = selected ? 1.4 : 1 contentView.layer.borderColor = (selected ? AppDesign.primary : UIColor(hex: 0xE6ECF4)).cgColor } }