Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
332 lines
13 KiB
Swift
332 lines
13 KiB
Swift
//
|
||
// 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<AccountSelectionSection, AccountSelectionItem>!
|
||
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<AccountSelectionSection, AccountSelectionItem>(
|
||
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<AccountSelectionSection, AccountSelectionItem> {
|
||
var snapshot = NSDiffableDataSourceSnapshot<AccountSelectionSection, AccountSelectionItem>()
|
||
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
|
||
}
|
||
}
|