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

@@ -1,3 +1,6 @@
{
"git.ignoreLimitWarning": true
"git.ignoreLimitWarning": true,
"cSpell.words": [
"eparti"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -136,10 +136,18 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
}
- (void)toLoginPage {
LoginViewController *lvc = [[LoginViewController alloc] init];
BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc];
// 使 Swift
EPLoginViewController *lvc = [[EPLoginViewController alloc] init];
BaseNavigationController *navigationController =
[[BaseNavigationController alloc] initWithRootViewController:lvc];
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
self.window.rootViewController = navigationController;
// 便
// LoginViewController *lvc = [[LoginViewController alloc] init];
// BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc];
// navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
// self.window.rootViewController = navigationController;
}
- (void)toHomeTabbarPage {

View File

@@ -0,0 +1,580 @@
//
// EPLoginTypesViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
class EPLoginTypesViewController: UIViewController {
// MARK: - Properties
var displayType: EPLoginDisplayType = .id
private let loginService = EPLoginService()
private let validator = EPLoginValidator()
private let backgroundImageView = UIImageView()
private let titleLabel = UILabel()
private let backButton = UIButton(type: .system)
private let firstInputView = EPLoginInputView()
private let secondInputView = EPLoginInputView()
private var thirdInputView: EPLoginInputView?
private let actionButton = UIButton(type: .system)
private var forgotPasswordButton: UIButton?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureForDisplayType()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
// MARK: - Setup
private func setupUI() {
setupBackground()
setupNavigationBar()
setupTitle()
setupInputViews()
setupActionButton()
}
private func setupBackground() {
view.addSubview(backgroundImageView)
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupNavigationBar() {
view.addSubview(backButton)
backButton.setImage(UIImage(systemName: EPLoginConfig.Images.iconBack), for: .normal)
backButton.tintColor = EPLoginConfig.Colors.textLight
backButton.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
make.size.equalTo(EPLoginConfig.Layout.backButtonSize)
}
}
private func setupTitle() {
view.addSubview(titleLabel)
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.titleFontSize, weight: .bold)
titleLabel.textColor = EPLoginConfig.Colors.textLight
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide).offset(100)
}
}
private func setupInputViews() {
view.addSubview(firstInputView)
view.addSubview(secondInputView)
firstInputView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(EPLoginConfig.Layout.inputTitleSpacing)
make.width.equalTo(EPLoginConfig.Layout.buttonWidth)
make.height.equalTo(EPLoginConfig.Layout.buttonHeight)
}
secondInputView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(firstInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
make.width.equalTo(EPLoginConfig.Layout.buttonWidth)
make.height.equalTo(EPLoginConfig.Layout.buttonHeight)
}
}
private func setupActionButton() {
view.addSubview(actionButton)
actionButton.setTitle("Login", for: .normal)
actionButton.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
actionButton.backgroundColor = EPLoginConfig.Colors.primary
actionButton.layer.cornerRadius = EPLoginConfig.Layout.cornerRadius
actionButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.buttonFontSize, weight: .semibold)
actionButton.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
actionButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
make.width.equalTo(EPLoginConfig.Layout.buttonWidth)
make.height.equalTo(EPLoginConfig.Layout.buttonHeight)
}
}
// MARK: - Configuration
private func configureForDisplayType() {
switch displayType {
case .id:
titleLabel.text = YMLocalizedString("1.0.37_text_26") // ID Login
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "person",
placeholder: "Please enter ID"
))
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: true,
icon: "lock",
placeholder: "Please enter password"
))
actionButton.setTitle("Login", for: .normal)
//
setupForgotPasswordButton()
case .email:
titleLabel.text = YMLocalizedString("20.20.51_text_1") // Email Login
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "envelope",
placeholder: "Please enter email"
))
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code"
))
secondInputView.delegate = self
actionButton.setTitle("Login", for: .normal)
case .phone:
titleLabel.text = "Phone Login"
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "phone",
placeholder: "Please enter phone"
))
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code"
))
secondInputView.delegate = self
actionButton.setTitle("Login", for: .normal)
case .emailReset:
titleLabel.text = "Recover Password"
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "envelope",
placeholder: "Please enter email"
))
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code"
))
secondInputView.delegate = self
//
setupThirdInputView()
actionButton.setTitle("Confirm", for: .normal)
case .phoneReset:
titleLabel.text = "Recover Password"
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "phone",
placeholder: "Please enter phone"
))
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code"
))
secondInputView.delegate = self
//
setupThirdInputView()
actionButton.setTitle("Confirm", for: .normal)
}
}
private func setupForgotPasswordButton() {
let button = UIButton(type: .system)
button.setTitle("Forgot Password?", for: .normal)
button.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
button.addTarget(self, action: #selector(handleForgotPassword), for: .touchUpInside)
view.addSubview(button)
button.snp.makeConstraints { make in
make.trailing.equalTo(secondInputView)
make.top.equalTo(secondInputView.snp.bottom).offset(8)
}
forgotPasswordButton = button
}
private func setupThirdInputView() {
let inputView = EPLoginInputView()
inputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: true,
icon: EPLoginConfig.Images.iconLock,
placeholder: "6-16 Digits + English Letters"
))
view.addSubview(inputView)
inputView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
make.width.equalTo(EPLoginConfig.Layout.buttonWidth)
make.height.equalTo(EPLoginConfig.Layout.buttonHeight)
}
// actionButton
actionButton.snp.remakeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(inputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
make.width.equalTo(EPLoginConfig.Layout.buttonWidth)
make.height.equalTo(EPLoginConfig.Layout.buttonHeight)
}
thirdInputView = inputView
}
// MARK: - Actions
@objc private func handleBack() {
navigationController?.popViewController(animated: true)
}
@objc private func handleAction() {
view.endEditing(true)
//
switch displayType {
case .id:
handleIDLogin()
case .email:
handleEmailLogin()
case .phone:
handlePhoneLogin()
case .emailReset:
handleEmailResetPassword()
case .phoneReset:
handlePhoneResetPassword()
}
}
@objc private func handleForgotPassword() {
let vc = EPLoginTypesViewController()
vc.displayType = .emailReset
navigationController?.pushViewController(vc, animated: true)
}
// MARK: -
private func handleIDLogin() {
let id = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let password = secondInputView.text
//
guard !id.isEmpty else {
showAlert("请输入用户ID")
return
}
guard !password.isEmpty else {
showAlert("请输入密码")
return
}
//
showLoading(true)
loginService.loginWithID(id: id, password: password) { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] ID登录成功: \(accountModel.uid ?? "")")
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("登录失败: \(msg)")
}
}
}
private func handleEmailLogin() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
//
guard validator.validateEmail(email) else {
showAlert("请输入正确的邮箱地址")
return
}
guard validator.validateCode(code) else {
showAlert("请输入6位数字验证码")
return
}
showLoading(true)
loginService.loginWithEmail(email: email, code: code) { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] 邮箱登录成功: \(accountModel.uid ?? "")")
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("登录失败: \(msg)")
}
}
}
private func handlePhoneLogin() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
//
guard validator.validatePhone(phone) else {
showAlert("请输入正确的手机号")
return
}
guard validator.validateCode(code) else {
showAlert("请输入6位数字验证码")
return
}
showLoading(true)
loginService.loginWithPhone(phone: phone, code: code, areaCode: "+86") { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] 手机登录成功: \(accountModel.uid ?? "")")
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("登录失败: \(msg)")
}
}
}
private func handleEmailResetPassword() {
guard let thirdInput = thirdInputView else { return }
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
let newPassword = thirdInput.text
//
guard validator.validateEmail(email) else {
showAlert("请输入正确的邮箱地址")
return
}
guard validator.validateCode(code) else {
showAlert("请输入6位数字验证码")
return
}
guard validator.validatePassword(newPassword) else {
showAlert("密码需6-16位包含字母和数字")
return
}
showLoading(true)
loginService.resetEmailPassword(email: email, code: code, newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("密码重置成功", completion: {
self?.navigationController?.popViewController(animated: true)
})
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("重置失败: \(msg)")
}
}
}
private func handlePhoneResetPassword() {
guard let thirdInput = thirdInputView else { return }
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
let newPassword = thirdInput.text
//
guard validator.validatePhone(phone) else {
showAlert("请输入正确的手机号")
return
}
guard validator.validateCode(code) else {
showAlert("请输入6位数字验证码")
return
}
guard validator.validatePassword(newPassword) else {
showAlert("密码需6-16位包含字母和数字")
return
}
showLoading(true)
loginService.resetPhonePassword(phone: phone, code: code, areaCode: "+86", newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("密码重置成功", completion: {
self?.navigationController?.popViewController(animated: true)
})
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showAlert("重置失败: \(msg)")
}
}
}
// MARK: -
private func sendEmailCode() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard validator.validateEmail(email) else {
showAlert("请输入正确的邮箱地址")
return
}
let type = (displayType == .emailReset) ? 2 : 1 // 2=, 1=
loginService.sendEmailCode(email: email, type: type) { [weak self] in
DispatchQueue.main.async {
self?.secondInputView.startCountdown()
self?.showAlert("验证码已发送")
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showAlert("发送失败: \(msg)")
}
}
}
private func sendPhoneCode() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard validator.validatePhone(phone) else {
showAlert("请输入正确的手机号")
return
}
let type = (displayType == .phoneReset) ? 2 : 1 // 2=, 1=
loginService.sendPhoneCode(phone: phone, areaCode: "+86", type: type) { [weak self] in
DispatchQueue.main.async {
self?.secondInputView.startCountdown()
self?.showAlert("验证码已发送")
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showAlert("发送失败: \(msg)")
}
}
}
private func sendEmailResetCode() {
sendEmailCode() //
}
private func sendPhoneResetCode() {
sendPhoneCode() //
}
// MARK: - UI Helpers
private func showLoading(_ show: Bool) {
actionButton.isEnabled = !show
if show {
actionButton.setTitle("Loading...", for: .normal)
} else {
switch displayType {
case .id, .email, .phone:
actionButton.setTitle("Login", for: .normal)
case .emailReset, .phoneReset:
actionButton.setTitle("Confirm", for: .normal)
}
}
}
private func showAlert(_ message: String, completion: (() -> Void)? = nil) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
completion?()
})
present(alert, animated: true)
}
}
// MARK: - EPLoginInputViewDelegate
extension EPLoginTypesViewController: EPLoginInputViewDelegate {
func inputViewDidRequestCode(_ inputView: EPLoginInputView) {
if inputView == secondInputView {
if displayType == .email || displayType == .emailReset {
sendEmailCode()
} else if displayType == .phone || displayType == .phoneReset {
sendPhoneCode()
}
}
}
func inputViewDidSelectArea(_ inputView: EPLoginInputView) {
//
print("[EPLogin] Area selection - 占位Phase 2 实现")
}
}

View File

@@ -0,0 +1,252 @@
//
// EPLoginViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
@objc class EPLoginViewController: UIViewController {
// MARK: - Properties
private let backgroundImageView = UIImageView()
private let logoImageView = UIImageView()
private let epartiTitleLabel = UILabel()
private let idLoginButton = EPLoginButton()
private let emailLoginButton = EPLoginButton()
private let agreeCheckbox = UIButton(type: .custom)
private let policyLabel = EPPolicyLabel()
private let feedbackButton = UIButton(type: .custom)
#if DEBUG
private let debugButton = UIButton(type: .custom)
#endif
private let policySelectedKey = EPLoginConfig.Keys.policyAgreed
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
loadPolicyStatus()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// navigationController?.setNavigationBarHidden(false, animated: animated)
}
// MARK: - Setup
private func setupUI() {
setupBackground()
setupLogo()
setupLoginButtons()
setupPolicyArea()
setupNavigationBar()
}
private func setupBackground() {
view.addSubview(backgroundImageView)
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupLogo() {
view.addSubview(logoImageView)
logoImageView.image = kImage(EPLoginConfig.Images.loginBg)
logoImageView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view)
make.height.equalTo(EPLoginConfig.Layout.logoHeight)
}
// E-PARTI
view.addSubview(epartiTitleLabel)
epartiTitleLabel.text = "E-PARTI"
epartiTitleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.epartiTitleFontSize, weight: .bold)
epartiTitleLabel.textColor = EPLoginConfig.Colors.textLight
epartiTitleLabel.transform = CGAffineTransform(a: 1, b: 0, c: -0.2, d: 1, tx: 0, ty: 0) //
epartiTitleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.epartiTitleLeading)
make.bottom.equalTo(logoImageView.snp.bottom).offset(EPLoginConfig.Layout.epartiTitleBottomOffset)
}
}
private func setupLoginButtons() {
//
idLoginButton.configure(
icon: EPLoginConfig.Images.iconLoginId,
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.idLogin)
)
idLoginButton.delegate = self
emailLoginButton.configure(
icon: EPLoginConfig.Images.iconLoginEmail,
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.emailLogin)
)
emailLoginButton.delegate = self
// StackView
let stackView = UIStackView(arrangedSubviews: [idLoginButton, emailLoginButton])
stackView.axis = .vertical
stackView.spacing = EPLoginConfig.Layout.loginButtonSpacing
stackView.distribution = .fillEqually
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.loginButtonHorizontalPadding)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.loginButtonHorizontalPadding)
make.top.equalTo(logoImageView.snp.bottom)
}
idLoginButton.snp.makeConstraints { make in
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
}
emailLoginButton.snp.makeConstraints { make in
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
}
}
private func setupPolicyArea() {
view.addSubview(agreeCheckbox)
view.addSubview(policyLabel)
agreeCheckbox.setImage(kImage("login_privace_select"), for: .selected)
agreeCheckbox.setImage(kImage("login_privace_unselect"), for: .normal)
agreeCheckbox.addTarget(self, action: #selector(togglePolicyCheckbox), for: .touchUpInside)
policyLabel.onUserAgreementTapped = { [weak self] in
self?.openPolicy(url: "https://example.com/user-agreement")
}
policyLabel.onPrivacyPolicyTapped = { [weak self] in
self?.openPolicy(url: "https://example.com/privacy-policy")
}
agreeCheckbox.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.horizontalPadding)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30)
make.size.equalTo(EPLoginConfig.Layout.checkboxSize)
}
policyLabel.snp.makeConstraints { make in
make.leading.equalTo(agreeCheckbox.snp.trailing).offset(8)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.horizontalPadding)
make.centerY.equalTo(agreeCheckbox)
}
}
private func setupNavigationBar() {
view.addSubview(feedbackButton)
feedbackButton.setTitle(YMLocalizedString(EPLoginConfig.LocalizedKeys.feedback), for: .normal)
feedbackButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
feedbackButton.backgroundColor = EPLoginConfig.Colors.backgroundTransparent
feedbackButton.layer.cornerRadius = EPLoginConfig.Layout.feedbackButtonCornerRadius
feedbackButton.addTarget(self, action: #selector(handleFeedback), for: .touchUpInside)
feedbackButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
make.height.equalTo(EPLoginConfig.Layout.feedbackButtonHeight)
}
#if DEBUG
view.addSubview(debugButton)
debugButton.setTitle("切换环境", for: .normal)
debugButton.setTitleColor(.blue, for: .normal)
debugButton.addTarget(self, action: #selector(handleDebug), for: .touchUpInside)
debugButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
}
#endif
}
// MARK: - Actions
private func handleIDLogin() {
let vc = EPLoginTypesViewController()
vc.displayType = .id
navigationController?.pushViewController(vc, animated: true)
}
private func handleEmailLogin() {
let vc = EPLoginTypesViewController()
vc.displayType = .email
navigationController?.pushViewController(vc, animated: true)
}
@objc private func togglePolicyCheckbox() {
agreeCheckbox.isSelected.toggle()
UserDefaults.standard.set(agreeCheckbox.isSelected, forKey: policySelectedKey)
}
@objc private func handleFeedback() {
print("[EPLogin] Feedback - 占位Phase 2 实现")
}
#if DEBUG
@objc private func handleDebug() {
print("[EPLogin] Debug - 占位Phase 2 实现")
}
#endif
private func openPolicy(url: String) {
let webVC = XPWebViewController(roomUID: nil)
webVC.url = url
navigationController?.pushViewController(webVC, animated: true)
}
// MARK: - Helpers
private func loadPolicyStatus() {
agreeCheckbox.isSelected = UserDefaults.standard.bool(forKey: policySelectedKey)
//
if !UserDefaults.standard.bool(forKey: EPLoginConfig.Keys.hasLaunchedBefore) {
agreeCheckbox.isSelected = true
UserDefaults.standard.set(true, forKey: policySelectedKey)
UserDefaults.standard.set(true, forKey: EPLoginConfig.Keys.hasLaunchedBefore)
}
}
private func checkPolicyAgreed() -> Bool {
if !agreeCheckbox.isSelected {
// Phase 2:
print("[EPLogin] Please agree to policy first")
return false
}
return true
}
}
// MARK: - EPLoginButtonDelegate
extension EPLoginViewController: EPLoginButtonDelegate {
func loginButtonDidTap(_ button: EPLoginButton) {
guard checkPolicyAgreed() else { return }
if button == idLoginButton {
handleIDLogin()
} else if button == emailLoginButton {
handleEmailLogin()
}
}
}

View File

@@ -0,0 +1,20 @@
//
// EPLoginBridge.swift
// YuMi
//
// Created by AI on 2025-01-27.
// Objective-C Swift
//
import UIKit
/// kImage
func kImage(_ name: String) -> UIImage? {
return UIImage(named: name)
}
/// YMLocalizedString
func YMLocalizedString(_ key: String) -> String {
return Bundle.ymLocalizedString(forKey: key)
}

View File

@@ -0,0 +1,288 @@
//
// EPLoginConfig.swift
// YuMi
//
// Created by AI on 2025-01-27.
// -
//
import UIKit
///
struct EPLoginConfig {
// MARK: - Layout
struct Layout {
///
static let buttonWidth: CGFloat = 294
///
static let buttonHeight: CGFloat = 46
///
static let loginButtonHeight: CGFloat = 56
///
static let loginButtonSpacing: CGFloat = 24
///
static let loginButtonHorizontalPadding: CGFloat = 30
/// /
static let cornerRadius: CGFloat = 23
/// Logo
static let logoHeight: CGFloat = 400
/// Logo
static let logoTopOffset: CGFloat = 80
/// E-PARTI
static let epartiTitleFontSize: CGFloat = 56
/// E-PARTI view leading
static let epartiTitleLeading: CGFloat = 40
/// E-PARTI logoImage bottom
static let epartiTitleBottomOffset: CGFloat = -30
///
static let inputVerticalSpacing: CGFloat = 16
///
static let inputTitleSpacing: CGFloat = 60
///
static let buttonTopSpacing: CGFloat = 40
///
static let horizontalPadding: CGFloat = 40
///
static let compactHorizontalPadding: CGFloat = 16
///
static let titleFontSize: CGFloat = 28
///
static let buttonFontSize: CGFloat = 16
///
static let inputFontSize: CGFloat = 14
///
static let smallFontSize: CGFloat = 12
///
static let iconSize: CGFloat = 24
///
static let loginButtonIconSize: CGFloat = 30
///
static let loginButtonIconLeading: CGFloat = 33
///
static let iconLeading: CGFloat = 15
///
static let iconTextSpacing: CGFloat = 12
/// Checkbox
static let checkboxSize: CGFloat = 18
///
static let backButtonSize: CGFloat = 44
/// Feedback
static let feedbackButtonHeight: CGFloat = 22
static let feedbackButtonCornerRadius: CGFloat = 10.5
///
static let inputHeight: CGFloat = 52
///
static let inputCornerRadius: CGFloat = 26
///
static let inputHorizontalPadding: CGFloat = 24
/// icon
static let inputIconSize: CGFloat = 20
///
static let codeButtonWidth: CGFloat = 102
///
static let codeButtonHeight: CGFloat = 38
}
// MARK: - Colors
struct Colors {
///
static let primary = UIColor.systemPurple
///
static let background = UIColor.white
static let backgroundTransparent = UIColor.white.withAlphaComponent(0.5)
///
static let text = UIColor.darkText
static let textSecondary = UIColor.darkGray
static let textLight = UIColor.white
///
static let icon = UIColor.darkGray
static let iconDisabled = UIColor.gray
///
static let inputBackground = UIColor(red: 0xF3/255.0, green: 0xF5/255.0, blue: 0xFA/255.0, alpha: 1.0)
static let inputText = UIColor(red: 0x1F/255.0, green: 0x1B/255.0, blue: 0x4F/255.0, alpha: 1.0)
static let inputBorder = UIColor.lightGray.withAlphaComponent(0.3)
static let inputBorderFocused = UIColor.systemPurple
///
static let codeButtonBackground = UIColor(red: 0x91/255.0, green: 0x68/255.0, blue: 0xFA/255.0, alpha: 1.0)
///
static let buttonEnabled = UIColor.systemPurple
static let buttonDisabled = UIColor.lightGray
///
static let error = UIColor.systemRed
static let success = UIColor.systemGreen
///
static let link = UIColor.black
static let linkUnderline = UIColor.black
}
// MARK: - Animation
struct Animation {
///
static let duration: TimeInterval = 0.3
///
static let shortDuration: TimeInterval = 0.15
///
static let longDuration: TimeInterval = 0.5
///
static let springDamping: CGFloat = 0.75
///
static let springVelocity: CGFloat = 0.5
///
static let buttonPressScale: CGFloat = 0.95
///
static let shakeOffset: CGFloat = 10
///
static let shakeCount: Int = 3
}
// MARK: - Validation
struct Validation {
///
static let passwordMinLength = 6
///
static let passwordMaxLength = 16
///
static let codeLength = 6
///
static let phoneMinLength = 10
///
static let phoneMaxLength = 15
///
static let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
///
static let phoneRegex = "^[0-9]{10,15}$"
}
// MARK: - Timing
struct Timing {
///
static let codeCountdownSeconds = 60
/// Toast
static let toastDuration: TimeInterval = 2.0
///
static let requestTimeout: TimeInterval = 30.0
}
// MARK: - API
struct API {
/// Client Secret
static let clientSecret = "uyzjdhds"
/// Client ID
static let clientId = "1"
/// Grant Type
static let grantType = "sms_code"
///
static let version = "1.0.31"
///
static let codeTypeLogin = 1
///
static let codeTypeReset = 2
}
// MARK: - UserDefaults Keys
struct Keys {
///
static let policyAgreed = "HadAgreePrivacy"
///
static let hasLaunchedBefore = "HasLaunchedBefore"
}
// MARK: - Images
struct Images {
///
static let background = "vc_bg"
/// Logo
static let loginBg = "login_bg"
/// - ID
static let iconLoginId = "icon_login_id"
/// - Email
static let iconLoginEmail = "icon_login_email"
/// -
static let iconPerson = "person.circle"
static let iconPersonFill = "person"
/// -
static let iconEmail = "envelope.circle"
static let iconEmailFill = "envelope"
/// -
static let iconPhone = "phone.circle"
static let iconPhoneFill = "phone"
/// - Apple
static let iconApple = "apple.logo"
/// -
static let iconLock = "lock"
/// -
static let iconNumber = "number"
/// -
static let iconBack = "chevron.left"
/// -
static let iconEyeSlash = "eye.slash"
/// -
static let iconEye = "eye"
/// Checkbox -
static let checkboxEmpty = "circle"
/// Checkbox -
static let checkboxFilled = "checkmark.circle"
}
// MARK: - Localized Strings Keys
struct LocalizedKeys {
/// ID
static let idLogin = "1.0.37_text_26"
///
static let emailLogin = "20.20.51_text_1"
///
static let policyFullText = "XPLoginViewController6"
///
static let userAgreement = "XPLoginViewController7"
///
static let privacyPolicy = "XPLoginViewController9"
///
static let feedback = "XPMineFeedbackViewController0"
}
}

View File

@@ -0,0 +1,52 @@
//
// EPLoginState.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import Foundation
///
enum EPLoginDisplayType {
case id // ID +
case email // +
case phone // +
case emailReset //
case phoneReset //
}
/// Phase 2
class EPLoginValidator {
/// 6-16+
func validatePassword(_ password: String) -> Bool {
guard password.count >= 6 && password.count <= 16 else { return false }
let hasLetter = password.rangeOfCharacter(from: .letters) != nil
let hasDigit = password.rangeOfCharacter(from: .decimalDigits) != nil
return hasLetter && hasDigit
}
///
func validateEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
/// 6
func validateCode(_ code: String) -> Bool {
guard code.count == 6 else { return false }
return code.allSatisfy { $0.isNumber }
}
///
func validatePhone(_ phone: String) -> Bool {
let phoneRegex = "^[0-9]{10,15}$"
let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
return phonePredicate.evaluate(with: phone)
}
}

View File

@@ -0,0 +1,102 @@
//
// EPLoginManager.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
/// Swift
/// PILoginManager
@objc class EPLoginManager: NSObject {
// MARK: - Login Success Navigation
///
/// - Parameter viewController:
static func jumpToHome(from viewController: UIViewController) {
// 1.
guard let accountModel = AccountInfoStorage.instance().getCurrentAccountInfo() else {
print("[EPLoginManager] 账号信息不完整,无法继续")
return
}
let accessToken = accountModel.access_token
guard !accessToken.isEmpty else {
print("[EPLoginManager] access_token 为空,无法继续")
return
}
// 2. ticket
let loginService = EPLoginService()
loginService.requestTicket(accessToken: accessToken) { ticket in
// 3. ticket
AccountInfoStorage.instance().saveTicket(ticket)
// 4. EPTabBarController
DispatchQueue.main.async {
let epTabBar = EPTabBarController.create()
epTabBar.refreshTabBarWithIsLogin(true)
//
if let window = getKeyWindow() {
window.rootViewController = epTabBar
window.makeKeyAndVisible()
}
print("[EPLoginManager] 登录成功,已切换到 EPTabBarController")
}
} failure: { code, msg in
print("[EPLoginManager] 请求 Ticket 失败: \(code) - \(msg)")
// Ticket
DispatchQueue.main.async {
let epTabBar = EPTabBarController.create()
epTabBar.refreshTabBarWithIsLogin(true)
if let window = getKeyWindow() {
window.rootViewController = epTabBar
window.makeKeyAndVisible()
}
print("[EPLoginManager] Ticket 请求失败,仍跳转到首页")
}
}
}
/// Apple Login
/// - Parameter viewController:
static func loginWithApple(from viewController: UIViewController) {
print("[EPLoginManager] Apple Login - 占位Phase 2 实现")
// log
}
// MARK: - Helper Methods
/// keyWindowiOS 13+
private static func getKeyWindow() -> UIWindow? {
if #available(iOS 13.0, *) {
for windowScene in UIApplication.shared.connectedScenes {
if let windowScene = windowScene as? UIWindowScene,
windowScene.activationState == .foregroundActive {
for window in windowScene.windows {
if window.isKeyWindow {
return window
}
}
// keyWindow window
return windowScene.windows.first
}
}
} else {
// iOS 13 使
return UIApplication.shared.keyWindow
}
return nil
}
}

View File

@@ -0,0 +1,277 @@
//
// EPLoginService.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import Foundation
/// Swift
/// API OC LoginPresenter
@objc class EPLoginService: NSObject {
// MARK: - Constants
private let clientSecret = EPLoginConfig.API.clientSecret
private let clientId = EPLoginConfig.API.clientId
private let grantType = EPLoginConfig.API.grantType
private let version = EPLoginConfig.API.version
// MARK: - Private Helper Methods
/// AccountModel
/// - Parameters:
/// - data: API
/// - code:
/// - completion:
/// - failure:
private func parseAndSaveAccount(data: BaseModel?,
code: Int64,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
if code == 200 {
if let accountDict = data?.data as? NSDictionary,
let accountModel = AccountModel.mj_object(withKeyValues: accountDict) {
//
AccountInfoStorage.instance().saveAccountInfo(accountModel)
completion(accountModel)
} else {
failure(Int(code), "账号信息解析失败")
}
} else {
failure(Int(code), "操作失败")
}
}
// MARK: - Request Ticket
/// Ticket
/// - Parameters:
/// - accessToken: 访
/// - completion: (ticket)
/// - failure: (, )
@objc func requestTicket(accessToken: String,
completion: @escaping (String) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.requestTicket({ (data, code, msg) in
if code == 200, let dict = data?.data as? NSDictionary {
if let tickets = dict["tickets"] as? NSArray,
let firstTicket = tickets.firstObject as? NSDictionary,
let ticket = firstTicket["ticket"] as? String {
completion(ticket)
} else {
failure(Int(code), "Ticket 解析失败")
}
} else {
failure(Int(code), msg ?? "请求 Ticket 失败")
}
}, access_token: accessToken, issue_type: "multi")
}
// MARK: - Send Verification Code
///
/// - Parameters:
/// - email:
/// - type: (1=, 2=)
/// - completion:
/// - failure:
@objc func sendEmailCode(email: String,
type: Int,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
Api.emailGetCode({ (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? "发送邮箱验证码失败")
}
}, emailAddress: email, type: NSNumber(value: type))
}
///
/// - Parameters:
/// - phone:
/// - areaCode:
/// - type: (1=, 2=)
/// - completion:
/// - failure:
@objc func sendPhoneCode(phone: String,
areaCode: String,
type: Int,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
// Api+Login
// Api+Login.h
print("[EPLoginService] sendPhoneCode - 需要确认实际的 API 接口")
failure(-1, "手机验证码接口待确认")
}
// MARK: - Login Methods
/// ID +
/// - Parameters:
/// - id: ID
/// - password:
/// - completion: (AccountModel)
/// - failure:
@objc func loginWithID(id: String,
password: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.login(password: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? "登录失败")
})
},
phone: id,
password: password,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: grantType)
}
/// +
/// - Parameters:
/// - email:
/// - code:
/// - completion: (AccountModel)
/// - failure:
@objc func loginWithEmail(email: String,
code: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.login(code: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? "登录失败")
})
},
email: email,
code: code,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: grantType)
}
/// +
/// - Parameters:
/// - phone:
/// - code:
/// - areaCode:
/// - completion: (AccountModel)
/// - failure:
@objc func loginWithPhone(phone: String,
code: String,
areaCode: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.login(code: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? "登录失败")
})
},
phone: phone,
code: code,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: grantType,
phoneAreaCode: areaCode)
}
// MARK: - Reset Password
///
/// - Parameters:
/// - email:
/// - code:
/// - newPassword:
/// - completion:
/// - failure:
@objc func resetEmailPassword(email: String,
code: String,
newPassword: String,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
Api.resetPassword(email: { (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? "重置密码失败")
}
}, email: email, newPwd: newPassword, code: code)
}
///
/// - Parameters:
/// - phone:
/// - code:
/// - areaCode:
/// - newPassword:
/// - completion:
/// - failure:
@objc func resetPhonePassword(phone: String,
code: String,
areaCode: String,
newPassword: String,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
Api.resetPassword(phone: { (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? "重置密码失败")
}
}, phone: phone, newPwd: newPassword, smsCode: code, phoneAreaCode: areaCode)
}
// MARK: - Phone Quick Login ()
/// UI
/// - Parameters:
/// - accessToken: 访
/// - token:
/// - completion: (AccountModel)
/// - failure:
@objc func phoneQuickLogin(accessToken: String,
token: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.phoneQuickLogin({ [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? "快速登录失败")
})
},
accessToken: accessToken,
token: token)
}
}

View File

@@ -0,0 +1,131 @@
//
// EPLoginButton.swift
// YuMi
//
// Created by AI on 2025-01-27.
// - 使 StackView icon + title
//
import UIKit
import SnapKit
///
protocol EPLoginButtonDelegate: AnyObject {
func loginButtonDidTap(_ button: EPLoginButton)
}
///
class EPLoginButton: UIControl {
// MARK: - Properties
weak var delegate: EPLoginButtonDelegate?
private let stackView = UIStackView()
private let iconImageView = UIImageView()
private let titleLabel = UILabel()
private let leftSpacer = UIView()
private let rightSpacer = UIView()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup
private func setupUI() {
backgroundColor = EPLoginConfig.Colors.background
layer.cornerRadius = EPLoginConfig.Layout.cornerRadius
// StackView
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 0
stackView.isUserInteractionEnabled = false
addSubview(stackView)
// Icon
iconImageView.contentMode = .scaleAspectFit
// Title
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.inputFontSize, weight: .semibold)
titleLabel.textColor = EPLoginConfig.Colors.text
titleLabel.textAlignment = .center
// Spacers - title
leftSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
rightSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
// : [Leading 33] + [Icon] + [Flexible Spacer] + [Title] + [Flexible Spacer] + [Trailing 33]
let leadingPadding = UIView()
let trailingPadding = UIView()
stackView.addArrangedSubview(leadingPadding)
stackView.addArrangedSubview(iconImageView)
stackView.addArrangedSubview(leftSpacer)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(rightSpacer)
stackView.addArrangedSubview(trailingPadding)
//
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
leadingPadding.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
}
iconImageView.snp.makeConstraints { make in
make.size.equalTo(EPLoginConfig.Layout.loginButtonIconSize)
}
trailingPadding.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
}
// leftSpacer rightSpacer title
leftSpacer.snp.makeConstraints { make in
make.width.equalTo(rightSpacer)
}
//
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
// MARK: - Configuration
///
/// - Parameters:
/// - icon:
/// - title:
func configure(icon: String, title: String) {
iconImageView.image = kImage(icon)
titleLabel.text = title
}
// MARK: - Actions
@objc private func handleTap() {
delegate?.loginButtonDidTap(self)
}
// MARK: - Touch Feedback
override var isHighlighted: Bool {
didSet {
UIView.animate(withDuration: 0.1) {
self.alpha = self.isHighlighted ? 0.7 : 1.0
}
}
}
}

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)
}
}

View File

@@ -0,0 +1,115 @@
//
// EPPolicyLabel.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
class EPPolicyLabel: UILabel {
// MARK: - Properties
var onUserAgreementTapped: (() -> Void)?
var onPrivacyPolicyTapped: (() -> Void)?
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - Setup
private func setup() {
numberOfLines = 0
isUserInteractionEnabled = true
// 使 YMLocalizedString
let fullText = YMLocalizedString("XPLoginViewController6")
let userAgreementText = YMLocalizedString("XPLoginViewController7")
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
let attributedString = NSMutableAttributedString(string: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor,
value: UIColor.darkGray,
range: NSRange(location: 0, length: fullText.count))
attributedString.addAttribute(NSAttributedString.Key.font,
value: UIFont.systemFont(ofSize: 12),
range: NSRange(location: 0, length: fullText.count))
//
if let userRange = fullText.range(of: userAgreementText) {
let nsRange = NSRange(userRange, in: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.black, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
}
//
if let privacyRange = fullText.range(of: privacyPolicyText) {
let nsRange = NSRange(privacyRange, in: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.black, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
}
attributedText = attributedString
//
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tapGesture)
}
// MARK: - Actions
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let text = self.text else { return }
let userAgreementText = YMLocalizedString("XPLoginViewController7")
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText ?? NSAttributedString())
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
let locationOfTouchInLabel = gesture.location(in: self)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2,
y: (bounds.height - textBoundingBox.height) / 2)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
//
if let userRange = text.range(of: userAgreementText) {
let nsRange = NSRange(userRange, in: text)
if NSLocationInRange(indexOfCharacter, nsRange) {
onUserAgreementTapped?()
return
}
}
if let privacyRange = text.range(of: privacyPolicyText) {
let nsRange = NSRange(privacyRange, in: text)
if NSLocationInRange(indexOfCharacter, nsRange) {
onPrivacyPolicyTapped?()
return
}
}
}
}

View File

@@ -45,6 +45,21 @@
#import "NSString+Utils.h"
#import <MJExtension/MJExtension.h>
// MARK: - Login - Navigation & Web
#import "BaseNavigationController.h"
#import "XPWebViewController.h"
// MARK: - Login - Utilities
#import "YUMIMacroUitls.h" // YMLocalizedString
// MARK: - Login - Models (Phase 2 使用,先添加)
#import "AccountInfoStorage.h"
#import "AccountModel.h"
// MARK: - Login - APIs (Phase 2)
#import "Api+Login.h"
#import "Api+Main.h"
// 注意:
// 1. EPMomentViewController 和 EPMineViewController 直接继承 UIViewController
// 2. 不继承 BaseViewController避免 ClientConfig → PIBaseModel 依赖链)