5 Commits

Author SHA1 Message Date
edwinQQQ
e4f4557369 feat: 添加设置编辑页面及相关功能
主要变更:
1. 新增 EPEditSettingViewController,提供用户头像更新、昵称修改和退出登录功能。
2. 在 Bridging Header 中引入 UserInfoModel、XPMineUserInfoEditPresenter 等新模块,以支持设置页面的功能。
3. 更新多语言文件,添加设置页面相关的本地化字符串。

此更新旨在提升用户体验,简化用户信息管理流程。
2025-10-13 19:20:11 +08:00
edwinQQQ
02a8335d70 feat: 更新登录模块以支持验证码和渐变背景
主要变更:
1. 在 EPLoginTypesViewController 中添加了渐变背景到 actionButton,提升视觉效果。
2. 实现了输入框状态检查功能,确保在输入有效信息时启用登录按钮。
3. 更新了输入框配置,支持不同类型的键盘输入(如数字键盘和邮箱键盘)。
4. 在 EPLoginService 中添加了对手机号和邮箱的 DES 加密,增强安全性。
5. 更新了 EPLoginConfig,统一输入框和按钮的样式设置。

此更新旨在提升用户体验,确保登录过程的安全性和流畅性。
2025-10-13 17:49:09 +08:00
edwinQQQ
809cc44ca5 feat: 添加新的登录模块及相关组件
主要变更:
1. 新增 EPLoginViewController 和 EPLoginTypesViewController,提供新的登录界面和功能。
2. 引入 EPLoginInputView 和 EPLoginButton 组件,支持输入框和按钮的自定义。
3. 实现 EPLoginService 和 EPLoginManager,封装登录逻辑和 API 请求。
4. 添加 EPLoginConfig 和 EPLoginState,统一配置和状态管理。
5. 更新 Bridging Header,确保 Swift 和 Objective-C 代码的互操作性。

此更新旨在提升用户登录体验,简化登录流程,并提供更好的代码结构和可维护性。
2025-10-13 15:40:43 +08:00
edwinQQQ
26d9894830 feat: 更新 Bridging Header 和错误信息文件以支持新模型
主要变更:
1. 在 Bridging Header 中添加了对 PIBaseModel 和 MomentsInfoModel 的引用,以支持新的数据模型。
2. 更新了 error message.txt 文件,增加了详细的编译错误信息,帮助开发者快速定位问题。
3. 在 .gitignore 中添加了 error message.txt,以避免将错误信息文件纳入版本控制。

此更新旨在提升代码的可维护性和调试效率,确保新模型的顺利集成。
2025-10-11 19:06:08 +08:00
edwinQQQ
e318aaeee4 feat: 添加 EPMomentAPIHelper_Deprecated 以支持旧版 API
主要变更:
1. 新增 EPMomentAPIHelper_Deprecated.h 和 EPMomentAPIHelper_Deprecated.m 文件,提供与旧版 Objective-C API 的兼容性。
2. 该文件已被 EPMomentAPISwiftHelper.swift 替代,保留仅供参考,后续可删除。
3. 更新 EPMomentListView 以使用新的 Swift 版本 API,提升代码的现代化和类型安全。

此更新旨在确保旧版 API 的平滑过渡,同时鼓励使用新的 Swift 实现。
2025-10-11 18:43:25 +08:00
33 changed files with 4734 additions and 1312 deletions

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ YuMi/Assets.xcassets/
# Documentation files # Documentation files
*.md *.md
error message.txt

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -136,10 +136,18 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
} }
- (void)toLoginPage { - (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 {

View 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 实现")
}
}

View File

@@ -0,0 +1,252 @@
//
// EPLoginViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
@objc class EPLoginViewController: UIViewController {
// MARK: - Properties
private let backgroundImageView = UIImageView()
private let logoImageView = UIImageView()
private let epartiTitleLabel = UILabel()
private let idLoginButton = EPLoginButton()
private let emailLoginButton = EPLoginButton()
private let agreeCheckbox = UIButton(type: .custom)
private let policyLabel = EPPolicyLabel()
private let feedbackButton = UIButton(type: .custom)
#if DEBUG
private let debugButton = UIButton(type: .custom)
#endif
private let policySelectedKey = EPLoginConfig.Keys.policyAgreed
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
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()
}
}
}

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

View 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"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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)
}
}

View File

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

View File

@@ -0,0 +1,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
// Placeholder60%
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)
}
}

View File

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

View File

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

View File

@@ -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;
self.userInfo = userInfo; if (!userInfo) {
NSLog(@"[EPMineViewController] 加载用户信息失败");
return;
}
// self.userInfo = userInfo;
[self updateHeaderWithUserInfo:userInfo];
// 使
if (userInfo.dynamicInfo && userInfo.dynamicInfo.count > 0) {
[self.momentListView loadWithDynamicInfo:userInfo.dynamicInfo refreshCallback:^{
[self loadUserDetailInfo]; //
}];
}
} failure:^(NSInteger code, NSString * _Nullable msg) {
NSLog(@"[EPMineViewController] 加载用户信息失败: %@", msg);
}];
}
- (void)updateHeaderWithUserInfo:(UserInfoModel *)userInfo {
NSDictionary *userInfoDict = @{ NSDictionary *userInfoDict = @{
@"nickname": userInfo.nick ?: @"未设置昵称", @"nickname": userInfo.nick ?: @"未设置昵称",
@"uid": [NSString stringWithFormat:@"%ld", (long)userInfo.uid],
@"avatar": userInfo.avatar ?: @"", @"avatar": userInfo.avatar ?: @"",
@"uid": userInfo.uid > 0 ? @(userInfo.uid).stringValue : @"",
@"followers": @(userInfo.fansNum),
@"following": @(userInfo.followNum), @"following": @(userInfo.followNum),
@"followers": @(userInfo.fansNum)
}; };
[self.headerView updateWithUserInfo:userInfoDict]; [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) {
NSLog(@"[EPMineViewController] 用户详情加载失败: code=%ld, msg=%@", (long)code, msg);
}];
} }
// 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

View File

@@ -159,7 +159,8 @@
- (void)settingsButtonTapped { - (void)settingsButtonTapped {
NSLog(@"[EPMineHeaderView] 设置按钮点击"); NSLog(@"[EPMineHeaderView] 设置按钮点击");
// TODO: //
[[NSNotificationCenter defaultCenter] postNotificationName:@"EPMineHeaderSettingsButtonTapped" object:nil];
} }
@end @end

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"=

View File

@@ -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

View File

@@ -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:

View File

@@ -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];
} }
/// ///

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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.";

View File

@@ -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";

View File

@@ -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" = "Награды приведены для справки. Конкретные подарки определяются вашим вкладом и удачей.";

View File

@@ -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";

View File

@@ -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.";

View File

@@ -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