添加登录流程、会话路由和对齐参考 UI 的主 Tab 框架
This commit is contained in:
224
suixinkan/UI/Login/LoginViewController.swift
Normal file
224
suixinkan/UI/Login/LoginViewController.swift
Normal file
@ -0,0 +1,224 @@
|
||||
//
|
||||
// LoginViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 登录页,UI 严格对齐 Android `LoginScreen`。
|
||||
final class LoginViewController: BaseViewController {
|
||||
|
||||
private let viewModel = LoginViewModel()
|
||||
|
||||
private let backgroundImageView = UIImageView()
|
||||
private let welcomeLabel = UILabel()
|
||||
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 var hasPlayedEntranceAnimation = false
|
||||
|
||||
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
|
||||
|
||||
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(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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
formCardView.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)
|
||||
}
|
||||
|
||||
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 bindActions() {
|
||||
accountField.onTextChange = { [weak self] text in
|
||||
self?.viewModel.updateAccount(text)
|
||||
}
|
||||
accountField.onReturnKey = { [weak self] in
|
||||
self?.passwordField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
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?()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
playEntranceAnimationIfNeeded()
|
||||
}
|
||||
|
||||
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?.showToast("登录功能待接入")
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playEntranceAnimationIfNeeded() {
|
||||
guard !hasPlayedEntranceAnimation else { return }
|
||||
hasPlayedEntranceAnimation = true
|
||||
|
||||
formCardView.transform = CGAffineTransform(translationX: 0, y: view.bounds.height)
|
||||
welcomeLabel.alpha = 0
|
||||
|
||||
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) {
|
||||
self.formCardView.transform = .identity
|
||||
self.welcomeLabel.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
@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?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user