Compare commits
5 Commits
c0441f7853
...
e4f4557369
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e4f4557369 | ||
![]() |
02a8335d70 | ||
![]() |
809cc44ca5 | ||
![]() |
26d9894830 | ||
![]() |
e318aaeee4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ YuMi/Assets.xcassets/
|
|||||||
|
|
||||||
# Documentation files
|
# Documentation files
|
||||||
*.md
|
*.md
|
||||||
|
error message.txt
|
||||||
|
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 {
|
- (void)toLoginPage {
|
||||||
LoginViewController *lvc = [[LoginViewController alloc] init];
|
// 使用新的 Swift 登录页面
|
||||||
BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc];
|
EPLoginViewController *lvc = [[EPLoginViewController alloc] init];
|
||||||
|
BaseNavigationController *navigationController =
|
||||||
|
[[BaseNavigationController alloc] initWithRootViewController:lvc];
|
||||||
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
|
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||||
self.window.rootViewController = navigationController;
|
self.window.rootViewController = navigationController;
|
||||||
|
|
||||||
|
// 旧代码保留注释(便于回滚)
|
||||||
|
// LoginViewController *lvc = [[LoginViewController alloc] init];
|
||||||
|
// BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc];
|
||||||
|
// navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||||
|
// self.window.rootViewController = navigationController;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)toHomeTabbarPage {
|
- (void)toHomeTabbarPage {
|
||||||
|
713
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal file
713
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
//
|
||||||
|
// 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?
|
||||||
|
|
||||||
|
private var hasAddedGradient = false
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupUI()
|
||||||
|
configureForDisplayType()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// 添加渐变背景到 actionButton(只添加一次)
|
||||||
|
if !hasAddedGradient && actionButton.bounds.width > 0 {
|
||||||
|
actionButton.addGradientBackground(
|
||||||
|
with: [
|
||||||
|
EPLoginConfig.Colors.gradientStart,
|
||||||
|
EPLoginConfig.Colors.gradientEnd
|
||||||
|
],
|
||||||
|
start: CGPoint(x: 0, y: 0.5),
|
||||||
|
end: CGPoint(x: 1, y: 0.5),
|
||||||
|
cornerRadius: EPLoginConfig.Layout.uniformCornerRadius
|
||||||
|
)
|
||||||
|
hasAddedGradient = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.leading.equalToSuperview().offset(EPLoginConfig.Layout.uniformHorizontalPadding)
|
||||||
|
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.uniformHorizontalPadding)
|
||||||
|
make.top.equalTo(titleLabel.snp.bottom).offset(EPLoginConfig.Layout.inputTitleSpacing)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalTo(firstInputView)
|
||||||
|
make.top.equalTo(firstInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupActionButton() {
|
||||||
|
view.addSubview(actionButton)
|
||||||
|
actionButton.setTitle("Login", for: .normal)
|
||||||
|
actionButton.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
|
||||||
|
actionButton.layer.cornerRadius = EPLoginConfig.Layout.uniformCornerRadius
|
||||||
|
actionButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.buttonFontSize, weight: .semibold)
|
||||||
|
actionButton.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
|
||||||
|
|
||||||
|
// 初始状态:禁用按钮
|
||||||
|
actionButton.isEnabled = false
|
||||||
|
actionButton.alpha = 0.5
|
||||||
|
|
||||||
|
actionButton.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalTo(firstInputView)
|
||||||
|
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: "icon_login_id",
|
||||||
|
placeholder: "Please enter ID",
|
||||||
|
keyboardType: .numberPad // ID 使用数字键盘
|
||||||
|
))
|
||||||
|
firstInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.configure(with: EPLoginInputConfig(
|
||||||
|
showAreaCode: false,
|
||||||
|
showCodeButton: false,
|
||||||
|
isSecure: true,
|
||||||
|
icon: "icon_login_id",
|
||||||
|
placeholder: "Please enter password",
|
||||||
|
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
|
||||||
|
))
|
||||||
|
secondInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
keyboardType: .emailAddress // Email 使用邮箱键盘
|
||||||
|
))
|
||||||
|
firstInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.configure(with: EPLoginInputConfig(
|
||||||
|
showAreaCode: false,
|
||||||
|
showCodeButton: true,
|
||||||
|
isSecure: false,
|
||||||
|
icon: "number",
|
||||||
|
placeholder: "Please enter verification code",
|
||||||
|
keyboardType: .numberPad // 验证码使用数字键盘
|
||||||
|
))
|
||||||
|
secondInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
keyboardType: .numberPad // 手机号使用数字键盘
|
||||||
|
))
|
||||||
|
firstInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.configure(with: EPLoginInputConfig(
|
||||||
|
showAreaCode: false,
|
||||||
|
showCodeButton: true,
|
||||||
|
isSecure: false,
|
||||||
|
icon: "number",
|
||||||
|
placeholder: "Please enter verification code",
|
||||||
|
keyboardType: .numberPad // 验证码使用数字键盘
|
||||||
|
))
|
||||||
|
secondInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
keyboardType: .emailAddress // Email 使用邮箱键盘
|
||||||
|
))
|
||||||
|
firstInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.configure(with: EPLoginInputConfig(
|
||||||
|
showAreaCode: false,
|
||||||
|
showCodeButton: true,
|
||||||
|
isSecure: false,
|
||||||
|
icon: "number",
|
||||||
|
placeholder: "Please enter verification code",
|
||||||
|
keyboardType: .numberPad // 验证码使用数字键盘
|
||||||
|
))
|
||||||
|
secondInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
keyboardType: .numberPad // 手机号使用数字键盘
|
||||||
|
))
|
||||||
|
firstInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
secondInputView.configure(with: EPLoginInputConfig(
|
||||||
|
showAreaCode: false,
|
||||||
|
showCodeButton: true,
|
||||||
|
isSecure: false,
|
||||||
|
icon: "number",
|
||||||
|
placeholder: "Please enter verification code",
|
||||||
|
keyboardType: .numberPad // 验证码使用数字键盘
|
||||||
|
))
|
||||||
|
secondInputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
|
||||||
|
))
|
||||||
|
inputView.onTextChanged = { [weak self] _ in
|
||||||
|
self?.checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
view.addSubview(inputView)
|
||||||
|
|
||||||
|
inputView.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalTo(firstInputView)
|
||||||
|
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新调整 actionButton 位置
|
||||||
|
actionButton.snp.remakeConstraints { make in
|
||||||
|
make.leading.trailing.equalTo(firstInputView)
|
||||||
|
make.top.equalTo(inputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要人机验证
|
||||||
|
loadCaptchaWebView { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let type = (self.displayType == .phoneReset) ? 2 : 1 // 2=找回密码, 1=登录
|
||||||
|
|
||||||
|
self.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) {
|
||||||
|
if show {
|
||||||
|
actionButton.isEnabled = false
|
||||||
|
actionButton.alpha = 0.5
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
checkActionButtonStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查并更新按钮启用状态
|
||||||
|
private func checkActionButtonStatus() {
|
||||||
|
let isEnabled: Bool
|
||||||
|
|
||||||
|
switch displayType {
|
||||||
|
case .id:
|
||||||
|
let hasId = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
let hasPassword = !secondInputView.text.isEmpty
|
||||||
|
isEnabled = hasId && hasPassword
|
||||||
|
|
||||||
|
case .email, .phone:
|
||||||
|
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
let hasCode = !secondInputView.text.isEmpty
|
||||||
|
isEnabled = hasAccount && hasCode
|
||||||
|
|
||||||
|
case .emailReset, .phoneReset:
|
||||||
|
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
let hasCode = !secondInputView.text.isEmpty
|
||||||
|
let hasPassword = !(thirdInputView?.text.isEmpty ?? true)
|
||||||
|
isEnabled = hasAccount && hasCode && hasPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
actionButton.isEnabled = isEnabled
|
||||||
|
actionButton.alpha = isEnabled ? 1.0 : 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载人机验证 Captcha WebView
|
||||||
|
/// - Parameter completion: 验证成功后的回调
|
||||||
|
private func loadCaptchaWebView(completion: @escaping () -> Void) {
|
||||||
|
guard ClientConfig.share().shouldDisplayCaptcha else {
|
||||||
|
// 不需要验证,直接执行
|
||||||
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
view.endEditing(true)
|
||||||
|
|
||||||
|
let webVC = XPWebViewController(roomUID: nil)
|
||||||
|
webVC.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width * 0.8, height: UIScreen.main.bounds.width * 1.2)
|
||||||
|
webVC.view.backgroundColor = .clear
|
||||||
|
webVC.view.layer.cornerRadius = 12
|
||||||
|
webVC.view.layer.masksToBounds = true
|
||||||
|
webVC.isLoginStatus = false
|
||||||
|
webVC.isPush = false
|
||||||
|
webVC.hideNavigationBar()
|
||||||
|
webVC.url = URLWithType(.captchaSwitch)
|
||||||
|
|
||||||
|
webVC.verifyCaptcha = { result in
|
||||||
|
if result {
|
||||||
|
TTPopup.dismiss()
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TTPopup.popupView(webVC.view, style: .alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||||
|
setupUI()
|
||||||
|
loadPolicyStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal file
33
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 桥接 URLType 枚举常量
|
||||||
|
extension URLType {
|
||||||
|
static var captchaSwitch: URLType {
|
||||||
|
return URLType(rawValue: 113)! // kCaptchaSwitchPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DES 加密辅助函数
|
||||||
|
func encryptDES(_ plainText: String) -> String {
|
||||||
|
// 直接使用加密密钥(与 ObjC 版本保持一致)
|
||||||
|
let key = "1ea53d260ecf11e7b56e00163e046a26"
|
||||||
|
return DESEncrypt.encryptUseDES(plainText, key: key) ?? plainText
|
||||||
|
}
|
305
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal file
305
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
//
|
||||||
|
// 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 uniformHeight: CGFloat = 56
|
||||||
|
/// 输入框/按钮统一左右边距
|
||||||
|
static let uniformHorizontalPadding: CGFloat = 29
|
||||||
|
/// 输入框/按钮统一圆角
|
||||||
|
static let uniformCornerRadius: CGFloat = 28
|
||||||
|
/// 标准圆角半径(按钮/输入框)
|
||||||
|
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 = 56
|
||||||
|
/// 输入框圆角
|
||||||
|
static let inputCornerRadius: CGFloat = 28
|
||||||
|
/// 输入框左右内边距
|
||||||
|
static let inputHorizontalPadding: CGFloat = 24
|
||||||
|
/// 输入框 icon 尺寸
|
||||||
|
static let inputIconSize: CGFloat = 20
|
||||||
|
/// 输入框边框宽度
|
||||||
|
static let inputBorderWidth: CGFloat = 1
|
||||||
|
|
||||||
|
/// 验证码按钮宽度
|
||||||
|
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.white.withAlphaComponent(0.1)
|
||||||
|
static let inputText = UIColor(red: 0x1F/255.0, green: 0x1B/255.0, blue: 0x4F/255.0, alpha: 1.0)
|
||||||
|
static let inputBorder = UIColor.white
|
||||||
|
static let inputBorderFocused = UIColor.systemPurple
|
||||||
|
|
||||||
|
/// 渐变色(Login/Confirm按钮)
|
||||||
|
static let gradientStart = UIColor(red: 0xF8/255.0, green: 0x54/255.0, blue: 0xFC/255.0, alpha: 1.0) // #F854FC
|
||||||
|
static let gradientEnd = UIColor(red: 0x50/255.0, green: 0x0F/255.0, blue: 0xFF/255.0, alpha: 1.0) // #500FFF
|
||||||
|
|
||||||
|
/// 验证码按钮颜色
|
||||||
|
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 = "erban-client"
|
||||||
|
/// Grant Type
|
||||||
|
static let grantType = "password"
|
||||||
|
/// 版本号
|
||||||
|
static let version = "1"
|
||||||
|
|
||||||
|
/// 验证码类型:登录
|
||||||
|
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 iconPasswordSee = "icon_password_see"
|
||||||
|
static let iconPasswordUnsee = "icon_password_unsee"
|
||||||
|
|
||||||
|
/// 图标 - 返回
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
304
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal file
304
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密邮箱
|
||||||
|
let encryptedEmail = encryptDES(email)
|
||||||
|
|
||||||
|
Api.emailGetCode({ (data, code, msg) in
|
||||||
|
if code == 200 {
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
failure(Int(code), msg ?? "发送邮箱验证码失败")
|
||||||
|
}
|
||||||
|
}, emailAddress: encryptedEmail, 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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密手机号
|
||||||
|
let encryptedPhone = encryptDES(phone)
|
||||||
|
|
||||||
|
Api.phoneSmsCode({ (data, code, msg) in
|
||||||
|
if code == 200 {
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
failure(Int(code), msg ?? "发送手机验证码失败")
|
||||||
|
}
|
||||||
|
}, mobile: encryptedPhone, type: String(type), phoneAreaCode: areaCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密 ID 和密码
|
||||||
|
let encryptedId = encryptDES(id)
|
||||||
|
let encryptedPassword = encryptDES(password)
|
||||||
|
|
||||||
|
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: encryptedId,
|
||||||
|
password: encryptedPassword,
|
||||||
|
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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密邮箱
|
||||||
|
let encryptedEmail = encryptDES(email)
|
||||||
|
|
||||||
|
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: encryptedEmail,
|
||||||
|
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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密手机号
|
||||||
|
let encryptedPhone = encryptDES(phone)
|
||||||
|
|
||||||
|
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: encryptedPhone,
|
||||||
|
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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密邮箱和新密码
|
||||||
|
let encryptedEmail = encryptDES(email)
|
||||||
|
let encryptedPassword = encryptDES(newPassword)
|
||||||
|
|
||||||
|
Api.resetPassword(email: { (data, code, msg) in
|
||||||
|
if code == 200 {
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
failure(Int(code), msg ?? "重置密码失败")
|
||||||
|
}
|
||||||
|
}, email: encryptedEmail, newPwd: encryptedPassword, 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) {
|
||||||
|
|
||||||
|
// 🔐 DES 加密手机号和新密码
|
||||||
|
let encryptedPhone = encryptDES(phone)
|
||||||
|
let encryptedPassword = encryptDES(newPassword)
|
||||||
|
|
||||||
|
Api.resetPassword(phone: { (data, code, msg) in
|
||||||
|
if code == 200 {
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
failure(Int(code), msg ?? "重置密码失败")
|
||||||
|
}
|
||||||
|
}, phone: encryptedPhone, newPwd: encryptedPassword, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
313
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal file
313
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
//
|
||||||
|
// EPLoginInputView.swift
|
||||||
|
// YuMi
|
||||||
|
//
|
||||||
|
// Created by AI on 2025-01-27.
|
||||||
|
// 登录输入框组件 - 支持区号、验证码、密码切换等完整功能
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
/// 输入框配置
|
||||||
|
struct EPLoginInputConfig {
|
||||||
|
var showAreaCode: Bool = false
|
||||||
|
var showCodeButton: Bool = false
|
||||||
|
var isSecure: Bool = false
|
||||||
|
var icon: String?
|
||||||
|
var placeholder: String
|
||||||
|
var keyboardType: UIKeyboardType = .default
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 输入框代理
|
||||||
|
protocol EPLoginInputViewDelegate: AnyObject {
|
||||||
|
func inputViewDidRequestCode(_ inputView: EPLoginInputView)
|
||||||
|
func inputViewDidSelectArea(_ inputView: EPLoginInputView)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录输入框组件
|
||||||
|
class EPLoginInputView: UIView {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
weak var delegate: EPLoginInputViewDelegate?
|
||||||
|
|
||||||
|
/// 输入内容变化回调
|
||||||
|
var onTextChanged: ((String) -> Void)?
|
||||||
|
|
||||||
|
private let stackView = UIStackView()
|
||||||
|
|
||||||
|
// 区号区域
|
||||||
|
private let areaStackView = UIStackView()
|
||||||
|
private let areaCodeButton = UIButton(type: .custom)
|
||||||
|
private let areaArrowImageView = UIImageView()
|
||||||
|
private let areaTapButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
private let inputTextField = UITextField()
|
||||||
|
private let iconImageView = UIImageView()
|
||||||
|
|
||||||
|
// 眼睛按钮(密码可见性切换)
|
||||||
|
private let eyeButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
// 验证码按钮
|
||||||
|
private let codeButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
private var timer: DispatchSourceTimer?
|
||||||
|
private var countdownSeconds = 60
|
||||||
|
private var isCountingDown = false
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
private var config: EPLoginInputConfig?
|
||||||
|
|
||||||
|
/// 获取输入内容
|
||||||
|
var text: String {
|
||||||
|
return inputTextField.text ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
stopCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = EPLoginConfig.Colors.inputBackground
|
||||||
|
layer.cornerRadius = EPLoginConfig.Layout.inputCornerRadius
|
||||||
|
layer.borderWidth = EPLoginConfig.Layout.inputBorderWidth
|
||||||
|
layer.borderColor = EPLoginConfig.Colors.inputBorder.cgColor
|
||||||
|
|
||||||
|
// Main StackView
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.distribution = .fill
|
||||||
|
stackView.spacing = 8
|
||||||
|
addSubview(stackView)
|
||||||
|
|
||||||
|
setupAreaCodeView()
|
||||||
|
setupInputTextField()
|
||||||
|
setupEyeButton()
|
||||||
|
setupCodeButton()
|
||||||
|
|
||||||
|
stackView.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.inputHorizontalPadding)
|
||||||
|
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.inputHorizontalPadding)
|
||||||
|
make.top.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认隐藏所有可选组件
|
||||||
|
areaStackView.isHidden = true
|
||||||
|
eyeButton.isHidden = true
|
||||||
|
codeButton.isHidden = true
|
||||||
|
iconImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupAreaCodeView() {
|
||||||
|
// 区号 StackView
|
||||||
|
areaStackView.axis = .horizontal
|
||||||
|
areaStackView.alignment = .center
|
||||||
|
areaStackView.distribution = .fill
|
||||||
|
areaStackView.spacing = 8
|
||||||
|
|
||||||
|
// 区号按钮
|
||||||
|
areaCodeButton.setTitle("+86", for: .normal)
|
||||||
|
areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal)
|
||||||
|
areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
|
areaCodeButton.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
// 箭头图标
|
||||||
|
areaArrowImageView.image = kImage("login_area_arrow")
|
||||||
|
areaArrowImageView.contentMode = .scaleAspectFit
|
||||||
|
areaArrowImageView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
// 点击区域按钮
|
||||||
|
areaTapButton.addTarget(self, action: #selector(handleAreaTap), for: .touchUpInside)
|
||||||
|
|
||||||
|
areaStackView.addSubview(areaTapButton)
|
||||||
|
areaStackView.addArrangedSubview(areaCodeButton)
|
||||||
|
areaStackView.addArrangedSubview(areaArrowImageView)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(areaStackView)
|
||||||
|
|
||||||
|
areaTapButton.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
areaCodeButton.snp.makeConstraints { make in
|
||||||
|
make.width.lessThanOrEqualTo(60)
|
||||||
|
make.height.equalTo(stackView)
|
||||||
|
}
|
||||||
|
|
||||||
|
areaArrowImageView.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(12)
|
||||||
|
make.height.equalTo(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupInputTextField() {
|
||||||
|
// Icon (可选)
|
||||||
|
iconImageView.contentMode = .scaleAspectFit
|
||||||
|
iconImageView.tintColor = EPLoginConfig.Colors.icon
|
||||||
|
stackView.addArrangedSubview(iconImageView)
|
||||||
|
|
||||||
|
iconImageView.snp.makeConstraints { make in
|
||||||
|
make.size.equalTo(EPLoginConfig.Layout.inputIconSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextField
|
||||||
|
inputTextField.textColor = EPLoginConfig.Colors.textLight
|
||||||
|
inputTextField.font = .systemFont(ofSize: 14)
|
||||||
|
inputTextField.tintColor = EPLoginConfig.Colors.textLight
|
||||||
|
inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||||||
|
stackView.addArrangedSubview(inputTextField)
|
||||||
|
|
||||||
|
inputTextField.snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(stackView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func textFieldDidChange() {
|
||||||
|
onTextChanged?(inputTextField.text ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupEyeButton() {
|
||||||
|
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordUnsee), for: .normal)
|
||||||
|
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordSee), for: .selected)
|
||||||
|
eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside)
|
||||||
|
stackView.addArrangedSubview(eyeButton)
|
||||||
|
|
||||||
|
eyeButton.snp.makeConstraints { make in
|
||||||
|
make.size.equalTo(24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupCodeButton() {
|
||||||
|
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
|
||||||
|
codeButton.setTitleColor(.white, for: .normal)
|
||||||
|
codeButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
codeButton.titleLabel?.textAlignment = .center
|
||||||
|
codeButton.titleLabel?.numberOfLines = 2
|
||||||
|
codeButton.layer.cornerRadius = EPLoginConfig.Layout.codeButtonHeight / 2
|
||||||
|
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
|
||||||
|
codeButton.addTarget(self, action: #selector(handleCodeTap), for: .touchUpInside)
|
||||||
|
stackView.addArrangedSubview(codeButton)
|
||||||
|
|
||||||
|
codeButton.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(EPLoginConfig.Layout.codeButtonWidth)
|
||||||
|
make.height.equalTo(EPLoginConfig.Layout.codeButtonHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
/// 配置输入框
|
||||||
|
func configure(with config: EPLoginInputConfig) {
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
// 区号
|
||||||
|
areaStackView.isHidden = !config.showAreaCode
|
||||||
|
|
||||||
|
// Icon - 默认隐藏,不再使用
|
||||||
|
iconImageView.isHidden = true
|
||||||
|
|
||||||
|
// Placeholder(60% 白色)
|
||||||
|
inputTextField.attributedPlaceholder = NSAttributedString(
|
||||||
|
string: config.placeholder,
|
||||||
|
attributes: [NSAttributedString.Key.foregroundColor: UIColor.white.withAlphaComponent(0.6)]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 键盘类型
|
||||||
|
inputTextField.keyboardType = config.keyboardType
|
||||||
|
|
||||||
|
// 密码模式
|
||||||
|
inputTextField.isSecureTextEntry = config.isSecure
|
||||||
|
eyeButton.isHidden = !config.isSecure
|
||||||
|
|
||||||
|
// 验证码按钮
|
||||||
|
codeButton.isHidden = !config.showCodeButton
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置区号
|
||||||
|
func setAreaCode(_ code: String) {
|
||||||
|
areaCodeButton.setTitle(code, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清空输入
|
||||||
|
func clearInput() {
|
||||||
|
inputTextField.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
522
YuMi/E-P/NewMine/Controllers/EPEditSettingViewController.swift
Normal file
522
YuMi/E-P/NewMine/Controllers/EPEditSettingViewController.swift
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
//
|
||||||
|
// EPEditSettingViewController.swift
|
||||||
|
// YuMi
|
||||||
|
//
|
||||||
|
// Created by AI on 2025-01-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Photos
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
/// 设置编辑页面
|
||||||
|
/// 支持头像更新、昵称修改和退出登录功能
|
||||||
|
class EPEditSettingViewController: UIViewController {
|
||||||
|
|
||||||
|
// MARK: - UI Components
|
||||||
|
|
||||||
|
private lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||||
|
tableView.backgroundColor = UIColor(hex: "#0C0527")
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingCell")
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var profileImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.cornerRadius = 50
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
imageView.backgroundColor = .systemGray5
|
||||||
|
imageView.isUserInteractionEnabled = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
private var settingItems: [SettingItem] = []
|
||||||
|
private var userInfo: UserInfoModel?
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||||
|
setupUI()
|
||||||
|
setupData()
|
||||||
|
loadUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// 显示导航栏
|
||||||
|
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||||
|
navigationController?.navigationBar.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor.white,
|
||||||
|
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
|
||||||
|
]
|
||||||
|
navigationController?.navigationBar.barTintColor = UIColor(hex: "#0C0527")
|
||||||
|
navigationController?.navigationBar.tintColor = .white
|
||||||
|
navigationController?.navigationBar.isTranslucent = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
view.backgroundColor = UIColor(hex: "#0C0527")
|
||||||
|
title = YMLocalizedString("EPEditSetting.Title")
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
|
||||||
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加头像点击手势
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
|
||||||
|
profileImageView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupData() {
|
||||||
|
settingItems = [
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.PersonalInfo"),
|
||||||
|
action: { [weak self] in self?.handleReservedAction("PersonalInfo") }
|
||||||
|
),
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.Help"),
|
||||||
|
action: { [weak self] in self?.handleReservedAction("Help") }
|
||||||
|
),
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.ClearCache"),
|
||||||
|
action: { [weak self] in self?.handleReservedAction("ClearCache") }
|
||||||
|
),
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.CheckUpdate"),
|
||||||
|
action: { [weak self] in self?.handleReservedAction("CheckUpdate") }
|
||||||
|
),
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.Logout"),
|
||||||
|
style: .default,
|
||||||
|
action: { [weak self] in self?.showLogoutConfirm() }
|
||||||
|
),
|
||||||
|
SettingItem(
|
||||||
|
title: YMLocalizedString("EPEditSetting.AboutUs"),
|
||||||
|
action: { [weak self] in self?.handleReservedAction("AboutUs") }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadUserInfo() {
|
||||||
|
// 获取当前用户信息
|
||||||
|
guard let uid = AccountInfoStorage.instance().getUid(), !uid.isEmpty else {
|
||||||
|
print("[EPEditSetting] 未登录,无法获取用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 调用API获取用户详细信息
|
||||||
|
// 这里暂时创建默认的UserInfoModel用于显示
|
||||||
|
let tempUserInfo = UserInfoModel()
|
||||||
|
tempUserInfo.nick = "User"
|
||||||
|
tempUserInfo.avatar = ""
|
||||||
|
userInfo = tempUserInfo
|
||||||
|
|
||||||
|
updateProfileImage()
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProfileImage() {
|
||||||
|
guard let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty else {
|
||||||
|
profileImageView.image = UIImage(systemName: "person.circle.fill")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用SDWebImage加载头像
|
||||||
|
if let url = URL(string: avatarUrl) {
|
||||||
|
profileImageView.sd_setImage(with: url, placeholderImage: UIImage(systemName: "person.circle.fill"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
@objc private func profileImageTapped() {
|
||||||
|
showAvatarSelectionSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func openSettings() {
|
||||||
|
// 预留设置按钮功能
|
||||||
|
handleReservedAction("Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAvatarSelectionSheet() {
|
||||||
|
let alert = UIAlertController(title: YMLocalizedString("EPEditSetting.EditNickname"), message: nil, preferredStyle: .actionSheet)
|
||||||
|
|
||||||
|
// 拍照选项
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Camera"), style: .default) { [weak self] _ in
|
||||||
|
self?.checkCameraPermissionAndPresent()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 相册选项
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.PhotoLibrary"), style: .default) { [weak self] _ in
|
||||||
|
self?.checkPhotoLibraryPermissionAndPresent()
|
||||||
|
})
|
||||||
|
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||||
|
|
||||||
|
// iPad支持
|
||||||
|
if let popover = alert.popoverPresentationController {
|
||||||
|
popover.sourceView = profileImageView
|
||||||
|
popover.sourceRect = profileImageView.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkCameraPermissionAndPresent() {
|
||||||
|
YYUtility.checkCameraAvailable { [weak self] in
|
||||||
|
self?.presentImagePicker(sourceType: .camera)
|
||||||
|
} denied: { [weak self] in
|
||||||
|
self?.showPermissionAlert(title: "Camera Access", message: "Please allow camera access in Settings")
|
||||||
|
} restriction: { [weak self] in
|
||||||
|
self?.showPermissionAlert(title: "Camera Restricted", message: "Camera access is restricted on this device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkPhotoLibraryPermissionAndPresent() {
|
||||||
|
YYUtility.checkAssetsLibrayAvailable { [weak self] in
|
||||||
|
self?.presentImagePicker(sourceType: .photoLibrary)
|
||||||
|
} denied: { [weak self] in
|
||||||
|
self?.showPermissionAlert(title: "Photo Library Access", message: "Please allow photo library access in Settings")
|
||||||
|
} restriction: { [weak self] in
|
||||||
|
self?.showPermissionAlert(title: "Photo Library Restricted", message: "Photo library access is restricted on this device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentImagePicker(sourceType: UIImagePickerController.SourceType) {
|
||||||
|
let imagePicker = UIImagePickerController()
|
||||||
|
imagePicker.delegate = self
|
||||||
|
imagePicker.sourceType = sourceType
|
||||||
|
imagePicker.allowsEditing = true
|
||||||
|
present(imagePicker, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showPermissionAlert(title: String, message: String) {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
|
||||||
|
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
UIApplication.shared.open(settingsURL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showNicknameEditAlert() {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: YMLocalizedString("EPEditSetting.EditNickname"),
|
||||||
|
message: nil,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
|
||||||
|
alert.addTextField { [weak self] textField in
|
||||||
|
textField.text = self?.userInfo?.nick ?? ""
|
||||||
|
textField.placeholder = YMLocalizedString("EPEditSetting.EnterNickname")
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Confirm"), style: .default) { [weak self] _ in
|
||||||
|
guard let newNickname = alert.textFields?.first?.text, !newNickname.isEmpty else { return }
|
||||||
|
self?.updateNickname(newNickname)
|
||||||
|
})
|
||||||
|
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateNickname(_ newNickname: String) {
|
||||||
|
// 构建UserInfoModel并调用更新方法
|
||||||
|
let userInfo = UserInfoModel()
|
||||||
|
userInfo.nick = newNickname
|
||||||
|
|
||||||
|
// 调用Presenter方法 (桥接到OC)
|
||||||
|
let presenter = XPMineUserInfoEditPresenter()
|
||||||
|
presenter.getUserInfoEditDataSource(withUserInfo: userInfo)
|
||||||
|
|
||||||
|
// 更新本地显示
|
||||||
|
self.userInfo?.nick = newNickname
|
||||||
|
tableView.reloadData()
|
||||||
|
|
||||||
|
print("[EPEditSetting] 昵称更新为: \(newNickname)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showLogoutConfirm() {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: YMLocalizedString("EPEditSetting.LogoutConfirm"),
|
||||||
|
message: nil,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Logout"), style: .destructive) { [weak self] _ in
|
||||||
|
self?.performLogout()
|
||||||
|
})
|
||||||
|
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performLogout() {
|
||||||
|
guard let account = AccountInfoStorage.instance().accountModel else {
|
||||||
|
print("[EPEditSetting] 账号信息不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用登出API
|
||||||
|
Api.logoutCurrentAccount({ [weak self] (data, code, msg) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// 清除本地数据
|
||||||
|
AccountInfoStorage.instance().saveAccountInfo(nil)
|
||||||
|
AccountInfoStorage.instance().saveTicket(nil)
|
||||||
|
|
||||||
|
// 跳转登录页
|
||||||
|
self?.navigateToLogin()
|
||||||
|
}
|
||||||
|
}, access_token: account.access_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToLogin() {
|
||||||
|
let loginVC = EPLoginViewController()
|
||||||
|
let nav = UINavigationController(rootViewController: loginVC)
|
||||||
|
|
||||||
|
if let window = UIApplication.shared.windows.first {
|
||||||
|
window.rootViewController = nav
|
||||||
|
window.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[EPEditSetting] 已跳转到登录页面")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleReservedAction(_ title: String) {
|
||||||
|
print("[\(title)] - 功能预留,待后续实现")
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
|
||||||
|
// 显示占位提示
|
||||||
|
let alert = UIAlertController(title: "Coming Soon", message: "This feature will be available in the next update.", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||||||
|
|
||||||
|
extension EPEditSettingViewController: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return 2 // 头像昵称section + 设置项section
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
if section == 0 {
|
||||||
|
return 2 // 头像 + 昵称
|
||||||
|
} else {
|
||||||
|
return settingItems.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath)
|
||||||
|
cell.backgroundColor = UIColor(hex: "#0C0527")
|
||||||
|
cell.textLabel?.textColor = .white
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
|
||||||
|
if indexPath.section == 0 {
|
||||||
|
// 头像昵称section
|
||||||
|
if indexPath.row == 0 {
|
||||||
|
// 头像行
|
||||||
|
cell.textLabel?.text = YMLocalizedString("EPEditSetting.Avatar")
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
|
||||||
|
// 添加头像图片
|
||||||
|
if cell.contentView.subviews.contains(profileImageView) {
|
||||||
|
profileImageView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
cell.contentView.addSubview(profileImageView)
|
||||||
|
profileImageView.snp.makeConstraints { make in
|
||||||
|
make.trailing.equalToSuperview().offset(-50)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.size.equalTo(100)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 昵称行
|
||||||
|
cell.textLabel?.text = YMLocalizedString("EPEditSetting.Nickname")
|
||||||
|
cell.detailTextLabel?.text = userInfo?.nick ?? "未设置"
|
||||||
|
cell.detailTextLabel?.textColor = .lightGray
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 设置项section
|
||||||
|
let item = settingItems[indexPath.row]
|
||||||
|
cell.textLabel?.text = item.title
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
|
||||||
|
if item.style == .default {
|
||||||
|
cell.textLabel?.textColor = .systemRed
|
||||||
|
} else {
|
||||||
|
cell.textLabel?.textColor = .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
if indexPath.section == 0 && indexPath.row == 0 {
|
||||||
|
return 120 // 头像行更高
|
||||||
|
}
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
if indexPath.section == 0 {
|
||||||
|
if indexPath.row == 0 {
|
||||||
|
// 头像点击
|
||||||
|
showAvatarSelectionSheet()
|
||||||
|
} else {
|
||||||
|
// 昵称点击
|
||||||
|
showNicknameEditAlert()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 设置项点击
|
||||||
|
let item = settingItems[indexPath.row]
|
||||||
|
item.action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||||
|
return section == 0 ? 20 : 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: "#0C0527")
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: "#0C0527")
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIImagePickerControllerDelegate & UINavigationControllerDelegate
|
||||||
|
|
||||||
|
extension EPEditSettingViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
|
||||||
|
guard let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage else {
|
||||||
|
print("[EPEditSetting] 未能获取选择的图片")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新头像显示
|
||||||
|
profileImageView.image = image
|
||||||
|
|
||||||
|
// 上传头像到腾讯云
|
||||||
|
uploadAvatar(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAvatar(_ image: UIImage) {
|
||||||
|
// 压缩图片
|
||||||
|
guard let imageData = image.jpegData(compressionQuality: 0.5) else {
|
||||||
|
print("[EPEditSetting] 图片压缩失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
let format = "jpg"
|
||||||
|
let name = "image/\(UUID().uuidString).\(format)"
|
||||||
|
|
||||||
|
// 上传到腾讯云
|
||||||
|
UploadFile.share().qCloudUploadImage(imageData, named: name, success: { [weak self] (key, resp) in
|
||||||
|
print("[EPEditSetting] 头像上传成功: \(key)")
|
||||||
|
|
||||||
|
// 调用API更新头像
|
||||||
|
self?.updateAvatarAPI(avatarUrl: key)
|
||||||
|
|
||||||
|
}, failure: { (resCode, message) in
|
||||||
|
print("[EPEditSetting] 头像上传失败: \(message)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAvatarAPI(avatarUrl: String) {
|
||||||
|
// 调用API更新头像
|
||||||
|
Api.userV2UploadAvatar({ [weak self] (data, code, msg) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if code == 200 {
|
||||||
|
print("[EPEditSetting] 头像更新成功")
|
||||||
|
// 更新本地用户信息
|
||||||
|
self?.userInfo?.avatar = avatarUrl
|
||||||
|
} else {
|
||||||
|
print("[EPEditSetting] 头像更新失败: \(String(describing: msg))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, avatarUrl: avatarUrl, needPay: NSNumber(value: false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Models
|
||||||
|
|
||||||
|
private struct SettingItem {
|
||||||
|
let title: String
|
||||||
|
let style: UITableViewCell.SelectionStyle
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(title: String, style: UITableViewCell.SelectionStyle = .default, action: @escaping () -> Void) {
|
||||||
|
self.title = title
|
||||||
|
self.style = style
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIColor Extension
|
||||||
|
|
||||||
|
private extension UIColor {
|
||||||
|
convenience init(hex: String) {
|
||||||
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var int: UInt64 = 0
|
||||||
|
Scanner(string: hex).scanHexInt64(&int)
|
||||||
|
let a, r, g, b: UInt64
|
||||||
|
switch hex.count {
|
||||||
|
case 3: // RGB (12-bit)
|
||||||
|
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||||
|
case 6: // RGB (24-bit)
|
||||||
|
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
case 8: // ARGB (32-bit)
|
||||||
|
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
default:
|
||||||
|
(a, r, g, b) = (1, 1, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
red: CGFloat(r) / 255,
|
||||||
|
green: CGFloat(g) / 255,
|
||||||
|
blue: CGFloat(b) / 255,
|
||||||
|
alpha: CGFloat(a) / 255
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,8 @@
|
|||||||
#import "EPMineAPIHelper.h"
|
#import "EPMineAPIHelper.h"
|
||||||
#import "AccountInfoStorage.h"
|
#import "AccountInfoStorage.h"
|
||||||
#import "UserInfoModel.h"
|
#import "UserInfoModel.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import "YuMi-Swift.h" // 导入Swift桥接
|
||||||
|
|
||||||
@interface EPMineViewController ()
|
@interface EPMineViewController ()
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
|
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
|
|
||||||
NSLog(@"[EPMineViewController] 个人主页加载完成");
|
NSLog(@"[EPMineViewController] viewDidLoad 完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillAppear:(BOOL)animated {
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
@@ -56,17 +58,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
- (void)setupUI {
|
|
||||||
// 先设置纯色背景作为兜底,避免白色闪烁
|
|
||||||
self.view.backgroundColor = [UIColor clearColor];
|
|
||||||
|
|
||||||
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
|
- (void)setupUI {
|
||||||
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
// 背景渐变色
|
||||||
bgImageView.clipsToBounds = YES;
|
self.view.backgroundColor = [UIColor colorWithRed:0.047 green:0.020 blue:0.153 alpha:1.0]; // #0C0527
|
||||||
[self.view addSubview:bgImageView];
|
|
||||||
[bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
||||||
make.edges.mas_equalTo(self.view);
|
|
||||||
}];
|
|
||||||
|
|
||||||
[self setupHeaderView];
|
[self setupHeaderView];
|
||||||
[self setupMomentListView];
|
[self setupMomentListView];
|
||||||
@@ -75,76 +70,88 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)setupHeaderView {
|
- (void)setupHeaderView {
|
||||||
self.headerView = [[EPMineHeaderView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 300)];
|
self.headerView = [[EPMineHeaderView alloc] initWithFrame:CGRectZero];
|
||||||
|
|
||||||
[self.view addSubview:self.headerView];
|
[self.view addSubview:self.headerView];
|
||||||
|
|
||||||
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
// 使用约束布局
|
||||||
make.top.equalTo(self.view).offset(20);
|
self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
make.leading.trailing.equalTo(self.view);
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
make.height.equalTo(@300);
|
[self.headerView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||||||
}];
|
[self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||||
|
[self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||||
|
[self.headerView.heightAnchor constraintEqualToConstant:320]
|
||||||
|
]];
|
||||||
|
|
||||||
|
// 监听设置按钮点击事件
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(openSettings)
|
||||||
|
name:@"EPMineHeaderSettingsButtonTapped"
|
||||||
|
object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setupMomentListView {
|
- (void)setupMomentListView {
|
||||||
|
self.momentListView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
|
||||||
[self.view addSubview:self.momentListView];
|
[self.view addSubview:self.momentListView];
|
||||||
|
|
||||||
[self.momentListView mas_makeConstraints:^(MASConstraintMaker *make) {
|
// 使用约束布局
|
||||||
make.top.equalTo(self.headerView.mas_bottom).offset(10);
|
self.momentListView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
make.leading.trailing.bottom.equalTo(self.view);
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
}];
|
[self.momentListView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
|
||||||
|
[self.momentListView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||||||
|
[self.momentListView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||||||
|
[self.momentListView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
|
||||||
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Data Loading
|
// MARK: - Data Loading
|
||||||
|
|
||||||
- (void)loadUserDetailInfo {
|
- (void)loadUserDetailInfo {
|
||||||
NSString *uid = [[AccountInfoStorage instance] getUid];
|
NSString *uid = [[AccountInfoStorage instance] getUid];
|
||||||
if (!uid.length) {
|
if (!uid || uid.length == 0) {
|
||||||
NSLog(@"[EPMineViewController] 未登录,无法获取用户信息");
|
NSLog(@"[EPMineViewController] 用户未登录");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
[self.apiHelper getUserDetailInfoWithUid:uid completion:^(UserInfoModel * _Nullable userInfo) {
|
[self.apiHelper getUserDetailInfoWithUid:uid
|
||||||
|
completion:^(UserInfoModel * _Nullable userInfo) {
|
||||||
__strong typeof(weakSelf) self = weakSelf;
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!userInfo) {
|
||||||
|
NSLog(@"[EPMineViewController] 加载用户信息失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.userInfo = userInfo;
|
self.userInfo = userInfo;
|
||||||
|
[self updateHeaderWithUserInfo:userInfo];
|
||||||
|
|
||||||
// 更新头部视图
|
// 如果有动态信息,直接使用
|
||||||
NSDictionary *userInfoDict = @{
|
if (userInfo.dynamicInfo && userInfo.dynamicInfo.count > 0) {
|
||||||
@"nickname": userInfo.nick ?: @"未设置昵称",
|
[self.momentListView loadWithDynamicInfo:userInfo.dynamicInfo refreshCallback:^{
|
||||||
@"avatar": userInfo.avatar ?: @"",
|
[self loadUserDetailInfo]; // 刷新时重新加载
|
||||||
@"uid": userInfo.uid > 0 ? @(userInfo.uid).stringValue : @"",
|
}];
|
||||||
@"followers": @(userInfo.fansNum),
|
}
|
||||||
@"following": @(userInfo.followNum),
|
|
||||||
};
|
|
||||||
[self.headerView updateWithUserInfo:userInfoDict];
|
|
||||||
|
|
||||||
// 使用本地数组模式显示用户动态
|
|
||||||
[self.momentListView loadWithDynamicInfo:userInfo.dynamicInfo refreshCallback:^{
|
|
||||||
[self loadUserDetailInfo];
|
|
||||||
}];
|
|
||||||
|
|
||||||
NSLog(@"[EPMineViewController] 用户详情加载成功: %@ (动态数: %lu)",
|
|
||||||
userInfo.nick, (unsigned long)userInfo.dynamicInfo.count);
|
|
||||||
|
|
||||||
} failure:^(NSInteger code, NSString * _Nullable msg) {
|
} failure:^(NSInteger code, NSString * _Nullable msg) {
|
||||||
NSLog(@"[EPMineViewController] 用户详情加载失败: code=%ld, msg=%@", (long)code, msg);
|
NSLog(@"[EPMineViewController] 加载用户信息失败: %@", msg);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)updateHeaderWithUserInfo:(UserInfoModel *)userInfo {
|
||||||
|
NSDictionary *userInfoDict = @{
|
||||||
|
@"nickname": userInfo.nick ?: @"未设置昵称",
|
||||||
|
@"uid": [NSString stringWithFormat:@"%ld", (long)userInfo.uid],
|
||||||
|
@"avatar": userInfo.avatar ?: @"",
|
||||||
|
@"following": @(userInfo.followNum),
|
||||||
|
@"followers": @(userInfo.fansNum)
|
||||||
|
};
|
||||||
|
|
||||||
|
[self.headerView updateWithUserInfo:userInfoDict];
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Lazy Loading
|
// MARK: - Lazy Loading
|
||||||
|
|
||||||
- (EPMineHeaderView *)headerView {
|
|
||||||
if (!_headerView) {
|
|
||||||
_headerView = [[EPMineHeaderView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 300)];
|
|
||||||
}
|
|
||||||
return _headerView;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (EPMomentListView *)momentListView {
|
- (EPMomentListView *)momentListView {
|
||||||
if (!_momentListView) {
|
if (!_momentListView) {
|
||||||
_momentListView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
|
_momentListView = [[EPMomentListView alloc] init];
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
_momentListView.onSelectMoment = ^(NSInteger index) {
|
_momentListView.onSelectMoment = ^(NSInteger index) {
|
||||||
__strong typeof(weakSelf) self = weakSelf;
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
@@ -162,4 +169,16 @@
|
|||||||
return _apiHelper;
|
return _apiHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
- (void)openSettings {
|
||||||
|
EPEditSettingViewController *settingsVC = [[EPEditSettingViewController alloc] init];
|
||||||
|
[self.navigationController pushViewController:settingsVC animated:YES];
|
||||||
|
NSLog(@"[EPMineViewController] 打开设置页面");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@@ -159,7 +159,8 @@
|
|||||||
|
|
||||||
- (void)settingsButtonTapped {
|
- (void)settingsButtonTapped {
|
||||||
NSLog(@"[EPMineHeaderView] 设置按钮点击");
|
NSLog(@"[EPMineHeaderView] 设置按钮点击");
|
||||||
// TODO: 发送通知或回调给父视图
|
// 发送通知给父视图
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:@"EPMineHeaderSettingsButtonTapped" object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
@@ -1,9 +1,16 @@
|
|||||||
//
|
//
|
||||||
// EPMomentAPIHelper.h
|
// EPMomentAPIHelper_Deprecated.h
|
||||||
// YuMi
|
// YuMi
|
||||||
//
|
//
|
||||||
// Created by AI on 2025-10-10.
|
// Created by AI on 2025-10-10.
|
||||||
//
|
//
|
||||||
|
// ⚠️ DEPRECATED: 已被 EPMomentAPISwiftHelper.swift 替代
|
||||||
|
// 原因:
|
||||||
|
// 1. 继承 BaseMvpPresenter 会引起 Bridging Header 依赖链问题
|
||||||
|
// 2. Swift 版本更简洁、类型安全
|
||||||
|
// 3. 功能已完整迁移到 Swift 版本
|
||||||
|
//
|
||||||
|
// 保留此文件仅供参考,后续可删除
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "BaseMvpPresenter.h"
|
#import "BaseMvpPresenter.h"
|
@@ -1,12 +1,14 @@
|
|||||||
//
|
//
|
||||||
// EPMomentAPIHelper.m
|
// EPMomentAPIHelper_Deprecated.m
|
||||||
// YuMi
|
// YuMi
|
||||||
//
|
//
|
||||||
// Created by AI on 2025-10-10.
|
// Created by AI on 2025-10-10.
|
||||||
//
|
//
|
||||||
|
// ⚠️ DEPRECATED: 已被 EPMomentAPISwiftHelper.swift 替代
|
||||||
|
// 保留此文件仅供参考,后续可删除
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import "EPMomentAPIHelper.h"
|
#import "EPMomentAPIHelper_Deprecated.h"
|
||||||
#import "Api+Moments.h"
|
#import "Api+Moments.h"
|
||||||
#import "AccountInfoStorage.h"
|
#import "AccountInfoStorage.h"
|
||||||
#import "BaseModel.h"
|
#import "BaseModel.h"
|
@@ -8,9 +8,40 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// 动态 API 封装(Swift 现代化版本)
|
/// 动态 API 封装(Swift 现代化版本)
|
||||||
/// 与现有 OC 版本 EPMomentAPIHelper 并存,供对比评估
|
/// 统一封装列表获取和发布功能,完全替代 OC 版本
|
||||||
@objc class EPMomentAPISwiftHelper: NSObject {
|
@objc class EPMomentAPISwiftHelper: NSObject {
|
||||||
|
|
||||||
|
/// 拉取最新动态列表
|
||||||
|
/// - Parameters:
|
||||||
|
/// - nextID: 下一页 ID,首次传空字符串
|
||||||
|
/// - completion: 成功回调 (动态列表, 下一页ID)
|
||||||
|
/// - failure: 失败回调 (错误码, 错误信息)
|
||||||
|
@objc func fetchLatestMomentsWithNextID(
|
||||||
|
_ nextID: String,
|
||||||
|
completion: @escaping ([MomentsInfoModel], String) -> Void,
|
||||||
|
failure: @escaping (Int, String) -> Void
|
||||||
|
) {
|
||||||
|
let pageSize = "20"
|
||||||
|
let types = "0,2" // 图片+文字
|
||||||
|
|
||||||
|
Api.momentsLatestList({ (data, code, msg) in
|
||||||
|
if code == 200, let dict = data?.data as? NSDictionary {
|
||||||
|
// 从返回数据中提取原始 dictionary 数组
|
||||||
|
if let listArray = dict["dynamicList"] as? NSArray {
|
||||||
|
// MJExtension 在 Swift 中的正确用法(返回 NSMutableArray)
|
||||||
|
let modelsArray = MomentsInfoModel.mj_objectArray(withKeyValuesArray: listArray)
|
||||||
|
let nextID = dict["nextDynamicId"] as? String ?? ""
|
||||||
|
// 将 NSMutableArray 转换为 NSArray 传递给 OC
|
||||||
|
completion(modelsArray as? [MomentsInfoModel] ?? [], nextID)
|
||||||
|
} else {
|
||||||
|
completion([], "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failure(Int(code), msg ?? "请求失败")
|
||||||
|
}
|
||||||
|
}, dynamicId: nextID, pageSize: pageSize, types: types)
|
||||||
|
}
|
||||||
|
|
||||||
/// 发布动态
|
/// 发布动态
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - type: "0"=纯文本, "2"=图片
|
/// - type: "0"=纯文本, "2"=图片
|
||||||
|
@@ -6,12 +6,18 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import "EPMomentAPIHelper.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@class EPMomentAPISwiftHelper;
|
||||||
@class MomentsInfoModel;
|
@class MomentsInfoModel;
|
||||||
|
|
||||||
|
/// 推荐/我的动态列表数据源类型
|
||||||
|
typedef NS_ENUM(NSInteger, EPMomentListSourceType) {
|
||||||
|
EPMomentListSourceTypeRecommend = 0,
|
||||||
|
EPMomentListSourceTypeMine = 1
|
||||||
|
};
|
||||||
|
|
||||||
/// 承载 Moments 列表与分页刷新的视图
|
/// 承载 Moments 列表与分页刷新的视图
|
||||||
@interface EPMomentListView : UIView
|
@interface EPMomentListView : UIView
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
#import "EPMomentListView.h"
|
#import "EPMomentListView.h"
|
||||||
#import "EPMomentCell.h"
|
#import "EPMomentCell.h"
|
||||||
#import <MJRefresh/MJRefresh.h>
|
#import <MJRefresh/MJRefresh.h>
|
||||||
|
#import "YuMi-Swift.h"
|
||||||
|
|
||||||
|
|
||||||
@interface EPMomentListView () <UITableViewDelegate, UITableViewDataSource>
|
@interface EPMomentListView () <UITableViewDelegate, UITableViewDataSource>
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
@property (nonatomic, strong) UITableView *tableView;
|
@property (nonatomic, strong) UITableView *tableView;
|
||||||
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
||||||
@property (nonatomic, strong) NSMutableArray *mutableRawList;
|
@property (nonatomic, strong) NSMutableArray *mutableRawList;
|
||||||
@property (nonatomic, strong) EPMomentAPIHelper *api;
|
@property (nonatomic, strong) EPMomentAPISwiftHelper *api;
|
||||||
@property (nonatomic, assign) BOOL isLoading;
|
@property (nonatomic, assign) BOOL isLoading;
|
||||||
@property (nonatomic, copy) NSString *nextID;
|
@property (nonatomic, copy) NSString *nextID;
|
||||||
@property (nonatomic, assign) BOOL isLocalMode;
|
@property (nonatomic, assign) BOOL isLocalMode;
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
self = [super initWithFrame:frame];
|
self = [super initWithFrame:frame];
|
||||||
if (self) {
|
if (self) {
|
||||||
self.backgroundColor = [UIColor clearColor];
|
self.backgroundColor = [UIColor clearColor];
|
||||||
_api = [[EPMomentAPIHelper alloc] init];
|
_api = [[EPMomentAPISwiftHelper alloc] init];
|
||||||
_mutableRawList = [NSMutableArray array];
|
_mutableRawList = [NSMutableArray array];
|
||||||
_sourceType = EPMomentListSourceTypeRecommend;
|
_sourceType = EPMomentListSourceTypeRecommend;
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
|
|
||||||
@kWeakify(self);
|
@kWeakify(self);
|
||||||
[self.api fetchLatestMomentsWithNextID:self.nextID
|
[self.api fetchLatestMomentsWithNextID:self.nextID
|
||||||
completion:^(NSArray<MomentsInfoModel *> * _Nullable list, NSString * _Nonnull nextMomentID) {
|
completion:^(NSArray<MomentsInfoModel *> * _Nonnull list, NSString * _Nonnull nextMomentID) {
|
||||||
@kStrongify(self);
|
@kStrongify(self);
|
||||||
[self endLoading];
|
[self endLoading];
|
||||||
if (list.count > 0) {
|
if (list.count > 0) {
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
[self.tableView.mj_footer endRefreshing];
|
[self.tableView.mj_footer endRefreshing];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} failure:^(NSInteger code, NSString * _Nullable msg) {
|
} failure:^(NSInteger code, NSString * _Nonnull msg) {
|
||||||
@kStrongify(self);
|
@kStrongify(self);
|
||||||
[self endLoading];
|
[self endLoading];
|
||||||
// TODO: 完全没有数据情况下,后续补充数据异常页面
|
// TODO: 完全没有数据情况下,后续补充数据异常页面
|
||||||
|
@@ -28,8 +28,8 @@
|
|||||||
/// @param phone 手机号
|
/// @param phone 手机号
|
||||||
/// @param password 验证码
|
/// @param password 验证码
|
||||||
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion phone:(NSString *)phone password:(NSString *)password client_secret:(NSString *)client_secret version:(NSString *)version client_id:(NSString *)client_id grant_type:(NSString *)grant_type {
|
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion phone:(NSString *)phone password:(NSString *)password client_secret:(NSString *)client_secret version:(NSString *)version client_id:(NSString *)client_id grant_type:(NSString *)grant_type {
|
||||||
NSString * fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="];///oauth/token
|
|
||||||
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,password,client_secret,version, client_id, grant_type, nil];
|
[self makeRequest:@"oauth/token" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,password,client_secret,version, client_id, grant_type, nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重置手机号登录密码
|
/// 重置手机号登录密码
|
||||||
|
@@ -29,14 +29,49 @@
|
|||||||
// MARK: - Image Upload & Progress HUD
|
// MARK: - Image Upload & Progress HUD
|
||||||
#import "MBProgressHUD.h"
|
#import "MBProgressHUD.h"
|
||||||
|
|
||||||
|
// MARK: - Base Model & Types
|
||||||
|
#import "PIBaseModel.h"
|
||||||
|
#import "YUMINNNN.h"
|
||||||
|
|
||||||
// MARK: - API & Models
|
// MARK: - API & Models
|
||||||
#import "Api+Moments.h"
|
#import "Api+Moments.h"
|
||||||
#import "Api+Mine.h"
|
#import "Api+Mine.h"
|
||||||
#import "AccountInfoStorage.h"
|
#import "AccountInfoStorage.h"
|
||||||
|
#import "MomentsInfoModel.h"
|
||||||
|
#import "MomentsListInfoModel.h"
|
||||||
|
#import "UserInfoModel.h"
|
||||||
|
#import "XPMineUserInfoEditPresenter.h"
|
||||||
|
#import "UploadFile.h"
|
||||||
|
#import "YYUtility.h"
|
||||||
|
#import "SDWebImage.h"
|
||||||
|
|
||||||
// MARK: - Utilities
|
// MARK: - Utilities
|
||||||
#import "UIImage+Utils.h"
|
#import "UIImage+Utils.h"
|
||||||
#import "NSString+Utils.h"
|
#import "NSString+Utils.h"
|
||||||
|
#import "UIView+GradientLayer.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
// MARK: - Login - Navigation & Web
|
||||||
|
#import "BaseNavigationController.h"
|
||||||
|
#import "XPWebViewController.h"
|
||||||
|
|
||||||
|
// MARK: - Login - Utilities
|
||||||
|
#import "YUMIMacroUitls.h" // YMLocalizedString
|
||||||
|
#import "YUMIHtmlUrl.h" // URLWithType
|
||||||
|
#import "YUMIConstant.h" // KeyWithType, KeyType_PasswordEncode
|
||||||
|
#import "DESEncrypt.h" // DES加密工具
|
||||||
|
|
||||||
|
// MARK: - Login - Models (Phase 2 使用,先添加)
|
||||||
|
#import "AccountInfoStorage.h"
|
||||||
|
#import "AccountModel.h"
|
||||||
|
|
||||||
|
// MARK: - Login - APIs (Phase 2)
|
||||||
|
#import "Api+Login.h"
|
||||||
|
#import "Api+Main.h"
|
||||||
|
|
||||||
|
// MARK: - Login - Captcha & Config
|
||||||
|
#import "ClientConfig.h"
|
||||||
|
#import "TTPopup.h"
|
||||||
|
|
||||||
// 注意:
|
// 注意:
|
||||||
// 1. EPMomentViewController 和 EPMineViewController 直接继承 UIViewController
|
// 1. EPMomentViewController 和 EPMineViewController 直接继承 UIViewController
|
||||||
|
@@ -4256,3 +4256,23 @@ ineHeadView12" = "الحمل";
|
|||||||
"20.20.62_text_22" = "لقد شغّلت شاشة ميكروفون CP.";
|
"20.20.62_text_22" = "لقد شغّلت شاشة ميكروفون CP.";
|
||||||
"20.20.62_text_23" = "لقد أوقفت شاشة ميكروفون CP. شاشة ميكروفون CP غير مرئية في هذه الغرفة. انقر لتفعيلها مرة أخرى.";
|
"20.20.62_text_23" = "لقد أوقفت شاشة ميكروفون CP. شاشة ميكروفون CP غير مرئية في هذه الغرفة. انقر لتفعيلها مرة أخرى.";
|
||||||
"20.20.62_text_24" = "لقد أوقفت وضع التربو.";
|
"20.20.62_text_24" = "لقد أوقفت وضع التربو.";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
|
@@ -4046,3 +4046,23 @@
|
|||||||
"20.20.62_text_22" = "You have turn on the CP Mic Display.";
|
"20.20.62_text_22" = "You have turn on the CP Mic Display.";
|
||||||
"20.20.62_text_23" = "You have turn off the CP Mic Display. The CP Mic Display is not visible in this room. Click to enable it again.";
|
"20.20.62_text_23" = "You have turn off the CP Mic Display. The CP Mic Display is not visible in this room. Click to enable it again.";
|
||||||
"20.20.62_text_24" = "You have turned off Turbo Mode.";
|
"20.20.62_text_24" = "You have turned off Turbo Mode.";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
|
@@ -3717,6 +3717,26 @@
|
|||||||
"RoomBoom_5" = "Nombre de la sala:";//"Room name:";
|
"RoomBoom_5" = "Nombre de la sala:";//"Room name:";
|
||||||
"RoomBoom_6" = "Hora de reinicio: 0:00 (GMT+3) diariamente";
|
"RoomBoom_6" = "Hora de reinicio: 0:00 (GMT+3) diariamente";
|
||||||
"RoomBoom_7" = " Clasificación de Colaboradores";
|
"RoomBoom_7" = " Clasificación de Colaboradores";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
"RoomBoom_8" = "Super Jackpot";
|
"RoomBoom_8" = "Super Jackpot";
|
||||||
"RoomBoom_9" = "Reset time: 0:00 (GMT+8) daily";
|
"RoomBoom_9" = "Reset time: 0:00 (GMT+8) daily";
|
||||||
"RoomBoom_10" = "The rewards are for reference only. The specific gifts are determined by your contribution value and luck.";
|
"RoomBoom_10" = "The rewards are for reference only. The specific gifts are determined by your contribution value and luck.";
|
||||||
|
@@ -3337,3 +3337,23 @@
|
|||||||
"20.20.62_text_22" = "Você ativou a Exibição do Microfone CP.";
|
"20.20.62_text_22" = "Você ativou a Exibição do Microfone CP.";
|
||||||
"20.20.62_text_23" = "Você desativou a Exibição do Microfone CP. A Exibição do Microfone CP não está visível nesta sala. Clique para ativá-la novamente.";
|
"20.20.62_text_23" = "Você desativou a Exibição do Microfone CP. A Exibição do Microfone CP não está visível nesta sala. Clique para ativá-la novamente.";
|
||||||
"20.20.62_text_24" = "Você desativou o Modo Turbo.";
|
"20.20.62_text_24" = "Você desativou o Modo Turbo.";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
|
@@ -3716,6 +3716,26 @@
|
|||||||
"RoomBoom_5" = "";//"Название комнаты:";
|
"RoomBoom_5" = "";//"Название комнаты:";
|
||||||
"RoomBoom_6" = "Время сброса: 0:00 (GMT+3) ежедневно";
|
"RoomBoom_6" = "Время сброса: 0:00 (GMT+3) ежедневно";
|
||||||
"RoomBoom_7" = " Рейтинг спонсоров";
|
"RoomBoom_7" = " Рейтинг спонсоров";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
"RoomBoom_8" = "Супер джекпот";
|
"RoomBoom_8" = "Супер джекпот";
|
||||||
"RoomBoom_9" = "Время сброса: 0:00 (GMT+8) ежедневно";
|
"RoomBoom_9" = "Время сброса: 0:00 (GMT+8) ежедневно";
|
||||||
"RoomBoom_10" = "Награды приведены для справки. Конкретные подарки определяются вашим вкладом и удачей.";
|
"RoomBoom_10" = "Награды приведены для справки. Конкретные подарки определяются вашим вкладом и удачей.";
|
||||||
|
@@ -3837,3 +3837,23 @@
|
|||||||
"20.20.62_text_22" = "CP Mikrofon Ekranını açtınız.";
|
"20.20.62_text_22" = "CP Mikrofon Ekranını açtınız.";
|
||||||
"20.20.62_text_23" = "CP Mikrofon Ekranını kapattınız. CP Mikrofon Ekranı bu odada görünmüyor. Tekrar etkinleştirmek için tıklayın.";
|
"20.20.62_text_23" = "CP Mikrofon Ekranını kapattınız. CP Mikrofon Ekranı bu odada görünmüyor. Tekrar etkinleştirmek için tıklayın.";
|
||||||
"20.20.62_text_24" = "Turbo Modu'nu kapattınız.";
|
"20.20.62_text_24" = "Turbo Modu'nu kapattınız.";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
|
@@ -3717,6 +3717,26 @@ Tasdiqlangandan so'ng, sekretar sizga uni chop etishda yordam beradi va sizni xa
|
|||||||
"1.0.18_1" = "Bepul";
|
"1.0.18_1" = "Bepul";
|
||||||
"1.0.18_2" = "Pay";
|
"1.0.18_2" = "Pay";
|
||||||
"1.0.18_3" = "Maxsus";
|
"1.0.18_3" = "Maxsus";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
"1.0.18_4" = "Yangi yaratish";
|
"1.0.18_4" = "Yangi yaratish";
|
||||||
"1.0.18_5" = "Siz maksimal 6 ta fonni moslashtirishingiz mumkin.";
|
"1.0.18_5" = "Siz maksimal 6 ta fonni moslashtirishingiz mumkin.";
|
||||||
"1.0.18_6" = "Maxsus fon sifatida bir vaqtning o'zida maksimal 6 ta rasm yuklashingiz mumkin. \nFon yaratilgandan so'ng, uni bekor qilib bo'lmaydi. \nYuklangan fonni 24 soat ichida tekshiramiz. \nAgar fon rad etilsa, sizga tangalarni qaytarib beramiz.";
|
"1.0.18_6" = "Maxsus fon sifatida bir vaqtning o'zida maksimal 6 ta rasm yuklashingiz mumkin. \nFon yaratilgandan so'ng, uni bekor qilib bo'lmaydi. \nYuklangan fonni 24 soat ichida tekshiramiz. \nAgar fon rad etilsa, sizga tangalarni qaytarib beramiz.";
|
||||||
|
@@ -3707,3 +3707,23 @@
|
|||||||
"20.20.62_text_22" = "您已開啟 CP 麥克風顯示。";
|
"20.20.62_text_22" = "您已開啟 CP 麥克風顯示。";
|
||||||
"20.20.62_text_23" = "您已關閉 CP 麥克風顯示。此房間中不顯示 CP 麥克風顯示。點選可重新啟用。";
|
"20.20.62_text_23" = "您已關閉 CP 麥克風顯示。此房間中不顯示 CP 麥克風顯示。點選可重新啟用。";
|
||||||
"20.20.62_text_24" = "您已關閉 Turbo 模式。";
|
"20.20.62_text_24" = "您已關閉 Turbo 模式。";
|
||||||
|
|
||||||
|
// EPEditSetting - 设置页面多语言Key
|
||||||
|
"EPEditSetting.Title" = "Edit";
|
||||||
|
"EPEditSetting.Avatar" = "Avatar";
|
||||||
|
"EPEditSetting.Nickname" = "Nickname";
|
||||||
|
"EPEditSetting.PersonalInfo" = "Personal Information and Permissions";
|
||||||
|
"EPEditSetting.Help" = "Help";
|
||||||
|
"EPEditSetting.ClearCache" = "Clear Cache";
|
||||||
|
"EPEditSetting.CheckUpdate" = "Check for Updates";
|
||||||
|
"EPEditSetting.AboutUs" = "About Us";
|
||||||
|
"EPEditSetting.Logout" = "Log out of account";
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
"EPEditSetting.Camera" = "Take Photo";
|
||||||
|
"EPEditSetting.PhotoLibrary" = "Choose from Album";
|
||||||
|
"EPEditSetting.EditNickname" = "Edit Nickname";
|
||||||
|
"EPEditSetting.EnterNickname" = "Enter new nickname";
|
||||||
|
"EPEditSetting.LogoutConfirm" = "Are you sure you want to log out?";
|
||||||
|
"EPEditSetting.Cancel" = "Cancel";
|
||||||
|
"EPEditSetting.Confirm" = "Confirm";
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user