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:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
"git.ignoreLimitWarning": true,
|
||||
"cSpell.words": [
|
||||
"eparti"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
580
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal file
580
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal 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 实现")
|
||||
}
|
||||
}
|
252
YuMi/E-P/NewLogin/Controllers/EPLoginViewController.swift
Normal file
252
YuMi/E-P/NewLogin/Controllers/EPLoginViewController.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
20
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal file
20
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal 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)
|
||||
}
|
||||
|
288
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal file
288
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
|
52
YuMi/E-P/NewLogin/Models/EPLoginState.swift
Normal file
52
YuMi/E-P/NewLogin/Models/EPLoginState.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
102
YuMi/E-P/NewLogin/Services/EPLoginManager.swift
Normal file
102
YuMi/E-P/NewLogin/Services/EPLoginManager.swift
Normal 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
|
||||
|
||||
/// 获取 keyWindow(iOS 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
|
||||
}
|
||||
}
|
||||
|
277
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal file
277
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
131
YuMi/E-P/NewLogin/Views/EPLoginButton.swift
Normal file
131
YuMi/E-P/NewLogin/Views/EPLoginButton.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
302
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal file
302
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
115
YuMi/E-P/NewLogin/Views/EPPolicyLabel.swift
Normal file
115
YuMi/E-P/NewLogin/Views/EPPolicyLabel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 依赖链)
|
||||
|
Reference in New Issue
Block a user