Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,291 @@
//
// AccountSelectionViewController.swift
// suixinkan
//
import SnapKit
import UIKit
@MainActor
/// /
final class AccountSelectionViewController: UIViewController {
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)
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")
}
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()
configureBottomBar()
}
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
}
private func configureTableView() {
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.dataSource = self
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)
}
}
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)
}
}
@objc private func cancelTapped() {
onCancel()
dismiss(animated: true)
}
@objc private func confirmTapped() {
guard let selectedAccount else { return }
onConfirm(selectedAccount)
}
}
extension AccountSelectionViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
payload.accounts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: AccountSelectionCell.reuseIdentifier,
for: indexPath
) as? AccountSelectionCell else {
return UITableViewCell()
}
let account = payload.accounts[indexPath.row]
cell.configure(account: account, selected: account.id == selectedAccountId)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedAccountId = payload.accounts[indexPath.row].id
tableView.reloadData()
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
}
}

View File

@ -0,0 +1,532 @@
//
// LoginViewController.swift
// suixinkan
//
import SnapKit
import UIKit
@MainActor
/// /
final class LoginViewController: UIViewController {
private let services: AppServices
private let viewModel = LoginViewModel()
private let scrollView = UIScrollView()
private let contentView = UIView()
private let backgroundImageView = UIImageView(image: UIImage(named: "LoginBackground"))
private let titleLabel = UILabel()
private let cardView = UIView()
private let usernameField = LoginInputField(iconName: "person", placeholder: "请输入手机号", isSecure: false)
private let passwordField = LoginInputField(iconName: "lock", placeholder: "请输入密码", isSecure: true)
private let privacyButton = UIButton(type: .custom)
private let privacyLabel = UILabel()
private let userAgreementButton = UIButton(type: .system)
private let privacyPolicyButton = UIButton(type: .system)
private let loginButton = UIButton(type: .system)
init(services: AppServices) {
self.services = services
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0x0B1220)
configureViews()
bindViewModel()
viewModel.applyPreferences(services.authSessionCoordinator.loginPreferences())
}
private func configureViews() {
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.clipsToBounds = true
titleLabel.text = "欢迎使用\n随心瞰商家版"
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.largeTitle, weight: .semibold)
titleLabel.textColor = .white
titleLabel.numberOfLines = 0
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppMetrics.CornerRadius.card
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.12
cardView.layer.shadowRadius = 18
cardView.layer.shadowOffset = CGSize(width: 0, height: 8)
usernameField.textField.keyboardType = .phonePad
usernameField.textField.textContentType = .telephoneNumber
usernameField.textField.autocorrectionType = .no
usernameField.textField.autocapitalizationType = .none
usernameField.textField.returnKeyType = .next
usernameField.textField.accessibilityIdentifier = "login.username"
usernameField.textField.delegate = self
usernameField.textField.addTarget(self, action: #selector(usernameChanged), for: .editingChanged)
passwordField.textField.autocorrectionType = .no
passwordField.textField.autocapitalizationType = .none
passwordField.textField.returnKeyType = .go
passwordField.textField.accessibilityIdentifier = "login.password"
passwordField.textField.delegate = self
passwordField.textField.addTarget(self, action: #selector(passwordChanged), for: .editingChanged)
passwordField.onToggleVisibility = { [weak self] in
self?.viewModel.showsPassword.toggle()
}
privacyButton.accessibilityIdentifier = "login.privacy"
privacyButton.addTarget(self, action: #selector(togglePrivacy), for: .touchUpInside)
configureAgreementText()
loginButton.setTitle("登录", for: .normal)
loginButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
loginButton.layer.cornerRadius = AppMetrics.CornerRadius.button
loginButton.accessibilityIdentifier = "login.submit"
loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
view.addSubview(backgroundImageView)
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(cardView)
cardView.addSubview(usernameField)
cardView.addSubview(passwordField)
cardView.addSubview(privacyButton)
cardView.addSubview(privacyLabel)
cardView.addSubview(loginButton)
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(contentView.safeAreaLayoutGuide).offset(72)
make.leading.trailing.equalToSuperview().inset(24)
}
titleLabel.accessibilityIdentifier = "login.title"
cardView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.xxLarge)
make.leading.trailing.equalToSuperview().inset(20)
make.bottom.equalToSuperview().inset(AppMetrics.Spacing.xxLarge)
}
usernameField.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.height.equalTo(AppMetrics.ControlSize.inputHeight)
}
passwordField.snp.makeConstraints { make in
make.top.equalTo(usernameField.snp.bottom).offset(AppMetrics.Spacing.xSmall)
make.leading.trailing.equalTo(usernameField)
make.height.equalTo(AppMetrics.ControlSize.inputHeight)
}
privacyButton.snp.makeConstraints { make in
make.top.equalTo(passwordField.snp.bottom).offset(AppMetrics.ControlSize.checkboxTapArea)
make.leading.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.width.height.equalTo(AppMetrics.ControlSize.checkboxTapArea)
}
privacyLabel.snp.makeConstraints { make in
make.leading.equalTo(privacyButton.snp.trailing)
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.top.equalTo(privacyButton).offset(AppMetrics.Spacing.xxSmall)
}
loginButton.snp.makeConstraints { make in
make.top.equalTo(privacyLabel.snp.bottom).offset(AppMetrics.Spacing.mediumLarge)
make.leading.trailing.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.height.equalTo(AppMetrics.ControlSize.primaryButtonHeight)
}
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
private func configureAgreementText() {
privacyLabel.numberOfLines = 0
privacyLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
userAgreementButton.setTitle("《用户协议》", for: .normal)
userAgreementButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
userAgreementButton.setTitleColor(UIColor(hex: 0x208BFF), for: .normal)
userAgreementButton.accessibilityIdentifier = "login.userAgreement"
userAgreementButton.addTarget(self, action: #selector(openUserAgreement), for: .touchUpInside)
privacyPolicyButton.setTitle("《隐私政策》", for: .normal)
privacyPolicyButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
privacyPolicyButton.setTitleColor(UIColor(hex: 0x208BFF), for: .normal)
privacyPolicyButton.accessibilityIdentifier = "login.privacyPolicy"
privacyPolicyButton.addTarget(self, action: #selector(openPrivacyPolicy), for: .touchUpInside)
let prefix = UILabel()
prefix.text = "已阅读并同意"
prefix.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
prefix.textColor = UIColor(hex: 0x666666)
let separator = UILabel()
separator.text = ""
separator.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
separator.textColor = UIColor(hex: 0x666666)
let row = UIStackView(arrangedSubviews: [prefix, userAgreementButton, separator, privacyPolicyButton])
row.axis = .horizontal
row.spacing = 0
row.alignment = .center
privacyLabel.addSubview(row)
row.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func bindViewModel() {
viewModel.onChange = { [weak self] in
self?.renderViewModel()
}
renderViewModel()
}
private func renderViewModel() {
if usernameField.textField.text != viewModel.username {
usernameField.textField.text = viewModel.username
}
if passwordField.textField.text != viewModel.password {
passwordField.textField.text = viewModel.password
}
passwordField.setSecureEntry(!viewModel.showsPassword)
let checkboxImage = viewModel.privacyChecked ? "LoginCheckboxChecked" : "LoginCheckboxUnchecked"
privacyButton.setImage(UIImage(named: checkboxImage), for: .normal)
loginButton.isEnabled = viewModel.canSubmit && !viewModel.isLoading
loginButton.backgroundColor = viewModel.canSubmit ? AppDesign.primary : .gray
loginButton.setTitleColor(.white, for: .normal)
loginButton.alpha = viewModel.isLoading ? 0.7 : 1
if viewModel.showsAgreementSheet {
viewModel.showsAgreementSheet = false
presentAgreementSheet()
}
if let payload = viewModel.pendingAccountSelection {
if let controller = accountSelectionController {
controller.updateLoading(viewModel.isSelectingAccount)
} else if presentedViewController == nil {
presentAccountSelection(payload)
}
} else if accountSelectionController != nil {
dismiss(animated: true)
accountSelectionController = nil
}
}
private weak var accountSelectionController: AccountSelectionViewController?
@objc private func usernameChanged() {
viewModel.username = usernameField.textField.text ?? ""
viewModel.normalizeUsernameCountryCodeIfNeeded()
services.toastCenter.dismiss()
}
@objc private func passwordChanged() {
viewModel.password = passwordField.textField.text ?? ""
services.toastCenter.dismiss()
}
@objc private func togglePrivacy() {
viewModel.privacyChecked.toggle()
}
@objc private func openUserAgreement() {
showToast("用户协议页面待接入")
}
@objc private func openPrivacyPolicy() {
showToast("隐私政策页面待接入")
}
@objc private func loginTapped() {
view.endEditing(true)
performLogin()
}
@objc private func dismissKeyboard() {
view.endEditing(true)
}
private func performLogin() {
if let validationError = viewModel.validateForLogin() {
if validationError == .privacyUnchecked {
presentAgreementSheet()
} else {
showToast(validationError.message)
if validationError.focusField == .username {
usernameField.textField.becomeFirstResponder()
} else if validationError.focusField == .password {
passwordField.textField.becomeFirstResponder()
}
}
return
}
Task {
do {
try await services.globalLoading.withLoading(message: "登录中...") {
let resolution = try await viewModel.login(authAPI: services.authAPI)
switch resolution {
case let .completed(response):
await completeLogin(with: response)
case .needsAccountSelection:
break
}
}
} catch is CancellationError {
return
} catch {
showToast(error.localizedDescription)
}
}
}
private func presentAgreementSheet() {
let controller = LoginAgreementConsentViewController(
onOpenAgreement: { [weak self] title in
self?.showToast("\(title)页面待接入")
},
onAgreeAndContinue: { [weak self] in
self?.viewModel.acceptAgreement()
self?.performLogin()
}
)
if let sheet = controller.sheetPresentationController {
sheet.detents = [.custom(resolver: { _ in 240 })]
sheet.prefersGrabberVisible = true
}
present(controller, animated: true)
}
private func presentAccountSelection(_ payload: AccountSelectionPayload) {
let controller = AccountSelectionViewController(
payload: payload,
isLoading: viewModel.isSelectingAccount,
onCancel: { [weak self] in
self?.viewModel.clearPendingAccountSelection()
self?.accountSelectionController = nil
},
onConfirm: { [weak self] account in
self?.selectAccount(account)
}
)
accountSelectionController = controller
let navigation = UINavigationController(rootViewController: controller)
navigation.modalPresentationStyle = .formSheet
present(navigation, animated: true)
}
private func selectAccount(_ account: AccountSwitchAccount) {
Task {
do {
try await services.globalLoading.withLoading(message: "账号切换中...") {
let response = try await viewModel.selectAccount(account, authAPI: services.authAPI)
await completeLogin(with: response)
accountSelectionController = nil
dismiss(animated: true)
}
} catch is CancellationError {
return
} catch {
showToast(error.localizedDescription)
}
}
}
private func completeLogin(with response: V9AuthResponse) async {
do {
try await services.authSessionCoordinator.completeLogin(
with: response,
username: viewModel.normalizedUsername,
privacyAgreementAccepted: viewModel.privacyChecked,
appSession: services.appSession,
accountContext: services.accountContext,
permissionContext: services.permissionContext,
profileAPI: services.profileAPI,
accountContextAPI: services.accountContextAPI
)
} catch {
showToast(error.localizedDescription)
}
}
}
extension LoginViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField === usernameField.textField {
passwordField.textField.becomeFirstResponder()
} else if viewModel.canSubmit, !viewModel.isLoading {
performLogin()
}
return true
}
}
///
private final class LoginInputField: UIView {
let textField = UITextField()
var onToggleVisibility: (() -> Void)?
private let toggleButton = UIButton(type: .custom)
private var isSecure = false
init(iconName: String, placeholder: String, isSecure: Bool) {
self.isSecure = isSecure
super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xF1F5F9)
layer.cornerRadius = AppMetrics.CornerRadius.input
layer.borderWidth = 1
layer.borderColor = UIColor(hex: 0xDDE3EA).cgColor
let icon = UIImageView(image: UIImage(systemName: iconName))
icon.tintColor = AppDesign.textSecondary
icon.contentMode = .scaleAspectFit
textField.placeholder = placeholder
textField.font = .systemFont(ofSize: AppMetrics.FontSize.body)
textField.textColor = AppDesign.textPrimary
textField.tintColor = AppDesign.primary
textField.borderStyle = .none
textField.isSecureTextEntry = isSecure
addSubview(icon)
addSubview(textField)
icon.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.centerY.equalToSuperview()
make.width.height.equalTo(AppMetrics.ControlSize.smallIcon)
}
if isSecure {
toggleButton.addTarget(self, action: #selector(toggleVisibility), for: .touchUpInside)
addSubview(toggleButton)
toggleButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.small)
make.centerY.equalToSuperview()
make.width.height.equalTo(AppMetrics.ControlSize.iconTapArea)
}
textField.snp.makeConstraints { make in
make.leading.equalTo(icon.snp.trailing).offset(AppMetrics.Spacing.xSmall)
make.trailing.equalTo(toggleButton.snp.leading)
make.centerY.equalToSuperview()
}
} else {
textField.snp.makeConstraints { make in
make.leading.equalTo(icon.snp.trailing).offset(AppMetrics.Spacing.xSmall)
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.centerY.equalToSuperview()
}
}
}
required init?(coder: NSCoder) {
nil
}
func setSecureEntry(_ secure: Bool) {
textField.isSecureTextEntry = secure
let imageName = secure ? "LoginPwdInvisible" : "LoginPwdVisible"
toggleButton.setImage(UIImage(named: imageName), for: .normal)
}
@objc private func toggleVisibility() {
onToggleVisibility?()
}
}
///
private final class LoginAgreementConsentViewController: UIViewController {
private let onOpenAgreement: (String) -> Void
private let onAgreeAndContinue: () -> Void
init(onOpenAgreement: @escaping (String) -> Void, onAgreeAndContinue: @escaping () -> Void) {
self.onOpenAgreement = onOpenAgreement
self.onAgreeAndContinue = onAgreeAndContinue
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
nil
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let titleLabel = UILabel()
titleLabel.text = "请先阅读并同意相关协议"
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
titleLabel.textColor = AppDesign.textPrimary
titleLabel.textAlignment = .center
let descriptionLabel = UILabel()
descriptionLabel.text = "为了保障你的账号安全和服务体验,请阅读并同意用户协议和隐私政策。"
descriptionLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
descriptionLabel.textColor = AppDesign.textSecondary
descriptionLabel.numberOfLines = 0
descriptionLabel.textAlignment = .center
let continueButton = UIButton(type: .system)
continueButton.setTitle("同意并继续", for: .normal)
continueButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
continueButton.backgroundColor = AppDesign.primary
continueButton.setTitleColor(.white, for: .normal)
continueButton.layer.cornerRadius = AppMetrics.CornerRadius.button
continueButton.accessibilityIdentifier = "login.agreement.continue"
continueButton.addTarget(self, action: #selector(continueTapped), for: .touchUpInside)
view.addSubview(titleLabel)
view.addSubview(descriptionLabel)
view.addSubview(continueButton)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppMetrics.Spacing.sheet)
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.large)
}
descriptionLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.small)
make.leading.trailing.equalTo(titleLabel)
}
continueButton.snp.makeConstraints { make in
make.leading.trailing.equalTo(titleLabel)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppMetrics.Spacing.mediumLarge)
make.height.equalTo(AppMetrics.ControlSize.sheetButtonHeight)
}
}
@objc private func continueTapped() {
dismiss(animated: true) { [onAgreeAndContinue] in
onAgreeAndContinue()
}
}
}