// // LoginViewController.swift // suixinkan // import SnapKit import UIKit /// 登录页,UI 严格对齐 Android `LoginScreen`。 final class LoginViewController: BaseViewController { private let viewModel = LoginViewModel() private let authAPI = NetworkServices.shared.authAPI private let backgroundImageView = UIImageView() private let welcomeLabel = UILabel() private let formCardShadowView = UIView() private let formCardView = UIView() private let topFormStack = UIStackView() private let accountField = LoginTextField(placeholder: "请输入账号") private let passwordField = PasswordInputField() private let agreementRow = AgreementRowView() private let loginButton = AppButton(title: "登录", style: .primary) private let loginByCodeButton = AppButton(title: "验证码登录", style: .secondary) private let registerContainer = UIStackView() private let registerPrefixLabel = UILabel() private let registerButton = UIButton(type: .system) private weak var accountSelectionController: AccountSelectionViewController? override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } override func setupNavigationBar() { navigationController?.setNavigationBarHidden(true, animated: false) } override func setupUI() { view.backgroundColor = .black backgroundImageView.image = UIImage(named: "img_login_bg") backgroundImageView.contentMode = .scaleAspectFill backgroundImageView.clipsToBounds = true welcomeLabel.text = "欢迎使用\n随心瞰商家版" welcomeLabel.numberOfLines = 2 welcomeLabel.font = .systemFont(ofSize: 28, weight: .semibold) welcomeLabel.textColor = .white formCardShadowView.backgroundColor = .clear formCardShadowView.layer.shadowColor = UIColor.black.cgColor formCardShadowView.layer.shadowOpacity = 0.14 formCardShadowView.layer.shadowOffset = CGSize(width: 0, height: 8) formCardShadowView.layer.shadowRadius = 20 formCardView.backgroundColor = .white formCardView.layer.cornerRadius = 16 formCardView.clipsToBounds = true topFormStack.axis = .vertical topFormStack.spacing = 0 topFormStack.alignment = .fill loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) loginByCodeButton.addTarget(self, action: #selector(loginByCodeButtonTapped), for: .touchUpInside) registerContainer.axis = .horizontal registerContainer.alignment = .center registerContainer.spacing = 0 registerPrefixLabel.text = "没有账号?" registerPrefixLabel.font = .systemFont(ofSize: 14, weight: .medium) registerPrefixLabel.textColor = AppColor.text333 registerButton.setTitle("去注册", for: .normal) registerButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) registerButton.setTitleColor(AppColor.primary, for: .normal) registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside) registerContainer.addArrangedSubview(registerPrefixLabel) registerContainer.addArrangedSubview(registerButton) view.addSubview(backgroundImageView) view.addSubview(welcomeLabel) view.addSubview(formCardShadowView) formCardShadowView.addSubview(formCardView) formCardView.addSubview(topFormStack) formCardView.addSubview(registerContainer) topFormStack.addArrangedSubview(accountField) topFormStack.setCustomSpacing(8, after: accountField) topFormStack.addArrangedSubview(passwordField) topFormStack.setCustomSpacing(24, after: passwordField) topFormStack.addArrangedSubview(agreementRow) topFormStack.setCustomSpacing(16, after: agreementRow) topFormStack.addArrangedSubview(loginButton) topFormStack.setCustomSpacing(8, after: loginButton) topFormStack.addArrangedSubview(loginByCodeButton) viewModel.applyStoredPreferences() if let username = viewModel.normalizedUsername.nonEmpty ?? viewModel.account.nonEmpty { accountField.text = username } agreementRow.isChecked = viewModel.isPrivacyChecked bindViewModel() updateUI() } override func setupConstraints() { backgroundImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } welcomeLabel.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).offset(48) make.leading.trailing.equalToSuperview().inset(24) } formCardShadowView.snp.makeConstraints { make in make.top.equalTo(welcomeLabel.snp.bottom).offset(48) make.leading.trailing.equalToSuperview().inset(24) make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24) } formCardView.snp.makeConstraints { make in make.edges.equalToSuperview() } topFormStack.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview().inset(16) make.bottom.equalToSuperview().inset(16) } registerContainer.snp.makeConstraints { make in make.centerX.equalToSuperview() make.bottom.equalToSuperview().inset(16) } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() formCardShadowView.layer.shadowPath = UIBezierPath( roundedRect: formCardShadowView.bounds, cornerRadius: 16 ).cgPath } override func bindActions() { accountField.onTextChange = { [weak self] text in self?.viewModel.updateAccount(text) } accountField.onReturnKey = { [weak self] in self?.passwordField.becomeFirstResponder() } accountField.onEditingDidEnd = { [weak self] in self?.viewModel.normalizeUsernameCountryCodeIfNeeded() if let username = self?.viewModel.normalizedUsername { self?.accountField.text = username } } passwordField.onTextChange = { [weak self] text in self?.viewModel.updatePassword(text) } passwordField.onReturnKey = { [weak self] in self?.view.endEditing(true) self?.viewModel.login() } agreementRow.onCheckedChange = { [weak self] in self?.viewModel.togglePrivacyChecked() } agreementRow.onUserAgreementTap = { [weak self] in self?.viewModel.onUserAgreement?() } agreementRow.onPrivacyPolicyTap = { [weak self] in self?.viewModel.onPrivacyPolicy?() } setupDismissKeyboardGesture() } private func setupDismissKeyboardGesture() { let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tap.cancelsTouchesInView = false tap.delegate = self view.addGestureRecognizer(tap) } @objc private func dismissKeyboard() { view.endEditing(true) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Task { await viewModel.loadAppConfig(authAPI: authAPI) } } private func bindViewModel() { viewModel.onStateChange = { [weak self] in self?.updateUI() } viewModel.onShowMessage = { [weak self] message in self?.showToast(message) } viewModel.onLogin = { [weak self] in self?.performLogin() } viewModel.onLoginByCode = { [weak self] in self?.showToast("验证码登录待接入") } viewModel.onRegister = { [weak self] in self?.showToast("注册待接入") } viewModel.onUserAgreement = { [weak self] in self?.showToast("用户协议待接入") } viewModel.onPrivacyPolicy = { [weak self] in self?.showToast("隐私政策待接入") } } private func updateUI() { agreementRow.isChecked = viewModel.isPrivacyChecked loginButton.isEnabled = viewModel.isLoginEnabled && !viewModel.isLoading loginButton.alpha = viewModel.isLoading ? 0.7 : 1 let showRegisterSection = viewModel.enableRegister loginByCodeButton.isHidden = !showRegisterSection registerContainer.isHidden = !showRegisterSection topFormStack.snp.remakeConstraints { make in make.top.leading.trailing.equalToSuperview().inset(16) if showRegisterSection { make.bottom.lessThanOrEqualTo(registerContainer.snp.top).offset(-16) } else { make.bottom.equalToSuperview().inset(16) } } 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 func performLogin() { viewModel.normalizeUsernameCountryCodeIfNeeded() accountField.text = viewModel.normalizedUsername Task { showLoading() defer { hideLoading() } do { let resolution = try await viewModel.login(authAPI: authAPI) switch resolution { case let .completed(response): completeLogin(with: response) case .needsAccountSelection: break } } catch is CancellationError { return } catch { showToast(error.localizedDescription) } } } 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 { accountSelectionController?.updateLoading(true) defer { accountSelectionController?.updateLoading(false) } do { let response = try await viewModel.selectAccount(account, authAPI: authAPI) completeLogin(with: response) accountSelectionController = nil dismiss(animated: true) } catch is CancellationError { return } catch { showToast(error.localizedDescription) } } } private func completeLogin(with response: V9AuthResponse) { AuthSessionHelper.completeLogin( with: response, username: viewModel.normalizedUsername, privacyAgreementAccepted: viewModel.isPrivacyChecked ) } @objc private func loginButtonTapped() { view.endEditing(true) viewModel.login() } @objc private func loginByCodeButtonTapped() { view.endEditing(true) viewModel.onLoginByCode?() } @objc private func registerButtonTapped() { view.endEditing(true) viewModel.onRegister?() } } extension LoginViewController: UIGestureRecognizerDelegate { /// 点击输入框、按钮时不触发收键盘,避免干扰正常交互。 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { var candidate: UIView? = touch.view while let view = candidate { if view is UITextField || view is UIButton { return false } candidate = view.superview } return true } } private extension String { var nonEmpty: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }