
主要变更: 1. 将 EPLoginTypesViewController 继承自 BaseViewController,提升代码结构。 2. 简化表单验证逻辑,仅检查输入是否为空,减少对 EPLoginValidator 的依赖。 3. 更新错误处理方式,使用 showErrorToast 替代 showAlert,提升用户体验。 4. 在 EPLoginService 中直接使用字符串常量替代 grantType 变量,简化代码。 此更新旨在提升代码可读性和用户交互体验,确保登录流程更加流畅。
319 lines
9.9 KiB
Swift
319 lines
9.9 KiB
Swift
//
|
||
// 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
|
||
var keyboardType: UIKeyboardType = .default
|
||
}
|
||
|
||
/// 输入框代理
|
||
protocol EPLoginInputViewDelegate: AnyObject {
|
||
func inputViewDidRequestCode(_ inputView: EPLoginInputView)
|
||
func inputViewDidSelectArea(_ inputView: EPLoginInputView)
|
||
}
|
||
|
||
/// 登录输入框组件
|
||
class EPLoginInputView: UIView {
|
||
|
||
// MARK: - Properties
|
||
|
||
weak var delegate: EPLoginInputViewDelegate?
|
||
|
||
/// 输入内容变化回调
|
||
var onTextChanged: ((String) -> Void)?
|
||
|
||
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
|
||
layer.borderWidth = EPLoginConfig.Layout.inputBorderWidth
|
||
layer.borderColor = EPLoginConfig.Colors.inputBorder.cgColor
|
||
|
||
// 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.textLight
|
||
inputTextField.font = .systemFont(ofSize: 14)
|
||
inputTextField.tintColor = EPLoginConfig.Colors.textLight
|
||
inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||
stackView.addArrangedSubview(inputTextField)
|
||
|
||
inputTextField.snp.makeConstraints { make in
|
||
make.height.equalTo(stackView)
|
||
}
|
||
}
|
||
|
||
@objc private func textFieldDidChange() {
|
||
onTextChanged?(inputTextField.text ?? "")
|
||
}
|
||
|
||
private func setupEyeButton() {
|
||
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordUnsee), for: .normal)
|
||
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordSee), for: .selected)
|
||
eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside)
|
||
stackView.addArrangedSubview(eyeButton)
|
||
|
||
eyeButton.snp.makeConstraints { make in
|
||
make.size.equalTo(24)
|
||
}
|
||
}
|
||
|
||
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 - 默认隐藏,不再使用
|
||
iconImageView.isHidden = true
|
||
|
||
// Placeholder(60% 白色)
|
||
inputTextField.attributedPlaceholder = NSAttributedString(
|
||
string: config.placeholder,
|
||
attributes: [NSAttributedString.Key.foregroundColor: UIColor.white.withAlphaComponent(0.6)]
|
||
)
|
||
|
||
// 键盘类型
|
||
inputTextField.keyboardType = config.keyboardType
|
||
|
||
// 密码模式
|
||
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 = ""
|
||
}
|
||
|
||
/// 弹出键盘(自动聚焦输入框)
|
||
func displayKeyboard() {
|
||
inputTextField.becomeFirstResponder()
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
|