feat: 添加新的登录模块及相关组件

主要变更:
1. 新增 EPLoginViewController 和 EPLoginTypesViewController,提供新的登录界面和功能。
2. 引入 EPLoginInputView 和 EPLoginButton 组件,支持输入框和按钮的自定义。
3. 实现 EPLoginService 和 EPLoginManager,封装登录逻辑和 API 请求。
4. 添加 EPLoginConfig 和 EPLoginState,统一配置和状态管理。
5. 更新 Bridging Header,确保 Swift 和 Objective-C 代码的互操作性。

此更新旨在提升用户登录体验,简化登录流程,并提供更好的代码结构和可维护性。
This commit is contained in:
edwinQQQ
2025-10-13 15:40:43 +08:00
parent 26d9894830
commit 809cc44ca5
14 changed files with 3438 additions and 1213 deletions

View File

@@ -0,0 +1,302 @@
//
// EPLoginInputView.swift
// YuMi
//
// Created by AI on 2025-01-27.
// -
//
import UIKit
import SnapKit
///
struct EPLoginInputConfig {
var showAreaCode: Bool = false
var showCodeButton: Bool = false
var isSecure: Bool = false
var icon: String?
var placeholder: String
}
///
protocol EPLoginInputViewDelegate: AnyObject {
func inputViewDidRequestCode(_ inputView: EPLoginInputView)
func inputViewDidSelectArea(_ inputView: EPLoginInputView)
}
///
class EPLoginInputView: UIView {
// MARK: - Properties
weak var delegate: EPLoginInputViewDelegate?
private let stackView = UIStackView()
//
private let areaStackView = UIStackView()
private let areaCodeButton = UIButton(type: .custom)
private let areaArrowImageView = UIImageView()
private let areaTapButton = UIButton(type: .custom)
//
private let inputTextField = UITextField()
private let iconImageView = UIImageView()
//
private let eyeButton = UIButton(type: .custom)
//
private let codeButton = UIButton(type: .custom)
//
private var timer: DispatchSourceTimer?
private var countdownSeconds = 60
private var isCountingDown = false
//
private var config: EPLoginInputConfig?
///
var text: String {
return inputTextField.text ?? ""
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
stopCountdown()
}
// MARK: - Setup
private func setupUI() {
backgroundColor = EPLoginConfig.Colors.inputBackground
layer.cornerRadius = EPLoginConfig.Layout.inputCornerRadius
// Main StackView
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 8
addSubview(stackView)
setupAreaCodeView()
setupInputTextField()
setupEyeButton()
setupCodeButton()
stackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.inputHorizontalPadding)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.inputHorizontalPadding)
make.top.bottom.equalToSuperview()
}
//
areaStackView.isHidden = true
eyeButton.isHidden = true
codeButton.isHidden = true
iconImageView.isHidden = true
}
private func setupAreaCodeView() {
// StackView
areaStackView.axis = .horizontal
areaStackView.alignment = .center
areaStackView.distribution = .fill
areaStackView.spacing = 8
//
areaCodeButton.setTitle("+86", for: .normal)
areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal)
areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
areaCodeButton.isUserInteractionEnabled = false
//
areaArrowImageView.image = kImage("login_area_arrow")
areaArrowImageView.contentMode = .scaleAspectFit
areaArrowImageView.isUserInteractionEnabled = false
//
areaTapButton.addTarget(self, action: #selector(handleAreaTap), for: .touchUpInside)
areaStackView.addSubview(areaTapButton)
areaStackView.addArrangedSubview(areaCodeButton)
areaStackView.addArrangedSubview(areaArrowImageView)
stackView.addArrangedSubview(areaStackView)
areaTapButton.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
areaCodeButton.snp.makeConstraints { make in
make.width.lessThanOrEqualTo(60)
make.height.equalTo(stackView)
}
areaArrowImageView.snp.makeConstraints { make in
make.width.equalTo(12)
make.height.equalTo(8)
}
}
private func setupInputTextField() {
// Icon ()
iconImageView.contentMode = .scaleAspectFit
iconImageView.tintColor = EPLoginConfig.Colors.icon
stackView.addArrangedSubview(iconImageView)
iconImageView.snp.makeConstraints { make in
make.size.equalTo(EPLoginConfig.Layout.inputIconSize)
}
// TextField
inputTextField.textColor = EPLoginConfig.Colors.inputText
inputTextField.font = .systemFont(ofSize: 14)
inputTextField.tintColor = EPLoginConfig.Colors.primary
stackView.addArrangedSubview(inputTextField)
inputTextField.snp.makeConstraints { make in
make.height.equalTo(stackView)
}
}
private func setupEyeButton() {
eyeButton.setImage(UIImage(systemName: "eye.slash"), for: .normal)
eyeButton.setImage(UIImage(systemName: "eye"), for: .selected)
eyeButton.tintColor = EPLoginConfig.Colors.icon
eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside)
stackView.addArrangedSubview(eyeButton)
eyeButton.snp.makeConstraints { make in
make.width.equalTo(30)
}
}
private func setupCodeButton() {
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
codeButton.setTitleColor(.white, for: .normal)
codeButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
codeButton.titleLabel?.textAlignment = .center
codeButton.titleLabel?.numberOfLines = 2
codeButton.layer.cornerRadius = EPLoginConfig.Layout.codeButtonHeight / 2
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
codeButton.addTarget(self, action: #selector(handleCodeTap), for: .touchUpInside)
stackView.addArrangedSubview(codeButton)
codeButton.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.codeButtonWidth)
make.height.equalTo(EPLoginConfig.Layout.codeButtonHeight)
}
}
// MARK: - Configuration
///
func configure(with config: EPLoginInputConfig) {
self.config = config
//
areaStackView.isHidden = !config.showAreaCode
// Icon
if let iconName = config.icon {
iconImageView.image = UIImage(systemName: iconName)
iconImageView.isHidden = false
} else {
iconImageView.isHidden = true
}
// Placeholder
inputTextField.placeholder = config.placeholder
//
inputTextField.isSecureTextEntry = config.isSecure
eyeButton.isHidden = !config.isSecure
//
codeButton.isHidden = !config.showCodeButton
}
///
func setAreaCode(_ code: String) {
areaCodeButton.setTitle(code, for: .normal)
}
///
func clearInput() {
inputTextField.text = ""
}
// MARK: - Actions
@objc private func handleAreaTap() {
delegate?.inputViewDidSelectArea(self)
}
@objc private func handleEyeTap() {
eyeButton.isSelected.toggle()
inputTextField.isSecureTextEntry = !eyeButton.isSelected
}
@objc private func handleCodeTap() {
guard !isCountingDown else { return }
delegate?.inputViewDidRequestCode(self)
}
// MARK: - Countdown
///
func startCountdown() {
guard !isCountingDown else { return }
isCountingDown = true
countdownSeconds = 60
codeButton.isEnabled = false
codeButton.backgroundColor = EPLoginConfig.Colors.iconDisabled
let queue = DispatchQueue.main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: 1.0)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.countdownSeconds -= 1
if self.countdownSeconds <= 0 {
self.stopCountdown()
self.codeButton.setTitle(YMLocalizedString("XPLoginInputView1"), for: .normal)
} else {
self.codeButton.setTitle("\(self.countdownSeconds)s", for: .normal)
}
}
timer.resume()
self.timer = timer
}
///
func stopCountdown() {
guard let timer = timer else { return }
timer.cancel()
self.timer = nil
isCountingDown = false
codeButton.isEnabled = true
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
}
}