8 Commits

Author SHA1 Message Date
edwinQQQ
de8627a230 feat: 更新 EPLoginTypesViewController 和 EPLoginInputView 以增强布局和用户体验
主要变更:
1. 在 EPLoginTypesViewController 中添加了对多个 UI 组件的约束设置,确保布局更加灵活。
2. 更新了标题标签的文本内容,使用本地化字符串替代硬编码文本,提升国际化支持。
3. 在 EPLoginInputView 中为多个组件添加了自动布局支持,确保在不同屏幕尺寸下的适配性。

此更新旨在提升用户界面的可用性和美观性,确保更好的用户体验。
2025-10-14 17:46:37 +08:00
edwinQQQ
9466b65b40 refactor: 更新 EPLoginTypesViewController 以简化表单验证和错误处理
主要变更:
1. 将 EPLoginTypesViewController 继承自 BaseViewController,提升代码结构。
2. 简化表单验证逻辑,仅检查输入是否为空,减少对 EPLoginValidator 的依赖。
3. 更新错误处理方式,使用 showErrorToast 替代 showAlert,提升用户体验。
4. 在 EPLoginService 中直接使用字符串常量替代 grantType 变量,简化代码。

此更新旨在提升代码可读性和用户交互体验,确保登录流程更加流畅。
2025-10-14 16:47:47 +08:00
edwinQQQ
955cc3622f feat: 更新 EPEditSettingViewController 以增强用户信息管理功能
主要变更:
1. 在 EPEditSettingViewController 中添加了用户头像和相机图标的布局,提升用户界面友好性。
2. 引入 EPMineAPIHelper 以支持头像更新功能,简化 API 调用。
3. 优化了导航栏的显示和隐藏逻辑,确保用户体验流畅。
4. 更新了 UITableView 的数据源和布局,确保信息展示清晰。

此更新旨在提升用户体验,简化用户信息的管理和更新流程。
2025-10-14 14:46:08 +08:00
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
41 changed files with 5030 additions and 1431 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

@@ -56,6 +56,11 @@
value = "disable" value = "disable"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "SWIFT_DISABLE_SAFETY_CHECKS"
value = "YES"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction

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,721 @@
//
// EPLoginTypesViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
class EPLoginTypesViewController: BaseViewController {
// MARK: - Properties
var displayType: EPLoginDisplayType = .id
private let loginService = EPLoginService()
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.translatesAutoresizingMaskIntoConstraints = false
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupNavigationBar() {
view.addSubview(backButton)
backButton.translatesAutoresizingMaskIntoConstraints = false
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.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.titleFontSize, weight: .bold)
titleLabel.textColor = EPLoginConfig.Colors.textLight
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(backButton) //
}
}
private func setupInputViews() {
firstInputView.translatesAutoresizingMaskIntoConstraints = false
secondInputView.translatesAutoresizingMaskIntoConstraints = false
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.translatesAutoresizingMaskIntoConstraints = false
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 = YMLocalizedString("20.20.51_text_20")
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 = YMLocalizedString("20.20.51_text_20")
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.translatesAutoresizingMaskIntoConstraints = false
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.translatesAutoresizingMaskIntoConstraints = false
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 {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !password.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
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)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handleEmailLogin() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
//
guard !email.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.loginWithEmail(email: email, code: code) { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] 邮箱登录成功: \(accountModel.uid)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handlePhoneLogin() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
//
guard !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
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)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(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 !email.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
guard !newPassword.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.resetEmailPassword(email: email, code: code, newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
self?.navigationController?.popViewController(animated: true)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(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 !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
guard !newPassword.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.resetPhonePassword(phone: phone, code: code, areaCode: "+86", newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
self?.navigationController?.popViewController(animated: true)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
// MARK: -
private func sendEmailCode() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
//
guard !email.isEmpty else {
secondInputView.stopCountdown()
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?.secondInputView.displayKeyboard()
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.secondInputView.stopCountdown()
self?.showErrorToast(msg)
}
}
}
private func sendPhoneCode() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
//
guard !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
secondInputView.stopCountdown()
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?.secondInputView.displayKeyboard()
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.secondInputView.stopCountdown()
self?.showErrorToast(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
}
/// 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,303 @@
//
// 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 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: "password")
}
/// +
/// - 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: "email")
}
/// +
/// - 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: "password",
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,322 @@
//
// 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
stackView.translatesAutoresizingMaskIntoConstraints = false
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
areaStackView.translatesAutoresizingMaskIntoConstraints = false
//
areaCodeButton.setTitle("+86", for: .normal)
areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal)
areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
areaCodeButton.isUserInteractionEnabled = false
areaCodeButton.translatesAutoresizingMaskIntoConstraints = false
//
areaArrowImageView.image = kImage("login_area_arrow")
areaArrowImageView.contentMode = .scaleAspectFit
areaArrowImageView.isUserInteractionEnabled = false
areaArrowImageView.translatesAutoresizingMaskIntoConstraints = false
//
areaTapButton.translatesAutoresizingMaskIntoConstraints = 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)
}
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
iconImageView.translatesAutoresizingMaskIntoConstraints = false
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.translatesAutoresizingMaskIntoConstraints = false
inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
stackView.addArrangedSubview(inputTextField)
}
@objc private func textFieldDidChange() {
onTextChanged?(inputTextField.text ?? "")
}
private func setupEyeButton() {
eyeButton.translatesAutoresizingMaskIntoConstraints = false
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.translatesAutoresizingMaskIntoConstraints = false
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 = ""
}
///
func displayKeyboard() {
inputTextField.becomeFirstResponder()
}
// MARK: - Actions
@objc private func handleAreaTap() {
delegate?.inputViewDidSelectArea(self)
}
@objc private func handleEyeTap() {
eyeButton.isSelected.toggle()
inputTextField.isSecureTextEntry = !eyeButton.isSelected
}
@objc private func handleCodeTap() {
guard !isCountingDown else { return }
delegate?.inputViewDidRequestCode(self)
}
// MARK: - Countdown
///
func startCountdown() {
guard !isCountingDown else { return }
isCountingDown = true
countdownSeconds = 60
codeButton.isEnabled = false
codeButton.backgroundColor = EPLoginConfig.Colors.iconDisabled
let queue = DispatchQueue.main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: 1.0)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.countdownSeconds -= 1
if self.countdownSeconds <= 0 {
self.stopCountdown()
self.codeButton.setTitle(YMLocalizedString("XPLoginInputView1"), for: .normal)
} else {
self.codeButton.setTitle("\(self.countdownSeconds)s", for: .normal)
}
}
timer.resume()
self.timer = timer
}
///
func stopCountdown() {
guard let timer = timer else { return }
timer.cancel()
self.timer = nil
isCountingDown = false
codeButton.isEnabled = true
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
}
}

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,661 @@
//
// EPEditSettingViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
import Photos
import SnapKit
///
/// 退
class EPEditSettingViewController: BaseViewController {
// MARK: - UI Components
private lazy var profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 60 // 120/2 = 60
imageView.layer.masksToBounds = true
imageView.backgroundColor = .systemGray5
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var cameraIconView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "icon_setting_camear")
imageView.backgroundColor = UIColor(hex: "#0C0527")
imageView.layer.cornerRadius = 15 // 30/2 = 15
imageView.layer.masksToBounds = true
return imageView
}()
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = UIColor(hex: "#0C0527")
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingCell")
tableView.isScrollEnabled = true //
return tableView
}()
// MARK: - Data
private var settingItems: [SettingItem] = []
private var userInfo: UserInfoModel?
private var apiHelper: EPMineAPIHelper = EPMineAPIHelper()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupUI()
setupData()
loadUserInfo()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//
restoreParentNavigationBarStyle()
}
// MARK: - Setup
private func setupNavigationBar() {
title = YMLocalizedString("EPEditSetting.Title")
// iOS 13+
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(hex: "#0C0527")
appearance.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
]
appearance.shadowColor = .clear // 线
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
navigationController?.navigationBar.compactAppearance = appearance
navigationController?.navigationBar.tintColor = .white //
//
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
// push backButtonTitle
navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(
title: "",
style: .plain,
target: nil,
action: nil
)
}
private func restoreParentNavigationBarStyle() {
// EPMineViewController 使
let transparentAppearance = UINavigationBarAppearance()
transparentAppearance.configureWithTransparentBackground()
transparentAppearance.backgroundColor = .clear
transparentAppearance.shadowColor = .clear
navigationController?.navigationBar.standardAppearance = transparentAppearance
navigationController?.navigationBar.scrollEdgeAppearance = transparentAppearance
navigationController?.navigationBar.compactAppearance = transparentAppearance
}
private func setupUI() {
view.backgroundColor = UIColor(hex: "#0C0527")
//
view.addSubview(profileImageView)
profileImageView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(40)
make.centerX.equalTo(view)
make.size.equalTo(120)
}
//
view.addSubview(cameraIconView)
cameraIconView.snp.makeConstraints { make in
make.bottom.equalTo(profileImageView.snp.bottom)
make.trailing.equalTo(profileImageView.snp.trailing)
make.size.equalTo(30)
}
// TableView
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(profileImageView.snp.bottom).offset(40)
make.leading.trailing.bottom.equalTo(view)
}
//
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
profileImageView.addGestureRecognizer(tapGesture)
//
let cameraTapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
cameraIconView.addGestureRecognizer(cameraTapGesture)
}
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") }
)
]
NSLog("[EPEditSetting] setupData 完成,设置项数量: \(settingItems.count)")
}
private func loadUserInfo() {
// EPMineViewController
if userInfo != nil {
updateProfileImage()
tableView.reloadData()
return
}
//
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) {
//
showLoading()
// API
apiHelper.updateNickname(withNick: newNickname,
completion: { [weak self] in
self?.hideHUD()
//
self?.userInfo?.nick = newNickname
self?.tableView.reloadData()
//
self?.showSuccessToast(YMLocalizedString("XPMineUserInfoEditViewController13"))
print("[EPEditSetting] 昵称更新成功: \(newNickname)")
},
failure: { [weak self] (code: Int, msg: String?) in
self?.hideHUD()
//
let errorMsg = msg ?? "昵称更新失败,请稍后重试"
self?.showErrorToast(errorMsg)
print("[EPEditSetting] 昵称更新失败: \(code) - \(errorMsg)")
}
)
}
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: - Public Methods
/// EPMineViewController
@objc func updateWithUserInfo(_ userInfo: UserInfoModel) {
self.userInfo = userInfo
updateProfileImage()
tableView.reloadData()
NSLog("[EPEditSetting] 已更新用户信息: \(userInfo.nick ?? "未知")")
}
}
// MARK: - UITableViewDataSource & UITableViewDelegate
extension EPEditSettingViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1 // section
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let count = settingItems.count + 1 // +1 for nickname row
NSLog("[EPEditSetting] TableView rows count: \(count), settingItems: \(settingItems.count)")
return 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
//
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
if indexPath.row == 0 {
//
cell.textLabel?.text = YMLocalizedString("EPEditSetting.Nickname")
//
let arrowImageView = UIImageView()
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
arrowImageView.contentMode = .scaleAspectFit
cell.contentView.addSubview(arrowImageView)
arrowImageView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
//
let nicknameLabel = UILabel()
nicknameLabel.text = userInfo?.nick ?? "未设置"
nicknameLabel.textColor = .lightGray
nicknameLabel.font = UIFont.systemFont(ofSize: 16)
cell.contentView.addSubview(nicknameLabel)
nicknameLabel.snp.makeConstraints { make in
make.trailing.equalTo(arrowImageView.snp.leading).offset(-12)
make.centerY.equalToSuperview()
}
} else {
//
let item = settingItems[indexPath.row - 1]
cell.textLabel?.text = item.title
//
let arrowImageView = UIImageView()
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
arrowImageView.contentMode = .scaleAspectFit
cell.contentView.addSubview(arrowImageView)
arrowImageView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
if item.style == .default {
cell.textLabel?.textColor = .systemRed
} else {
cell.textLabel?.textColor = .white
}
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60 // 60pt
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.row == 0 {
//
showNicknameEditAlert()
} else {
//
let item = settingItems[indexPath.row - 1]
item.action()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}
}
// 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) {
//
EPProgressHUD.showProgress(0, total: 1)
// 使 EPSDKManager OCR
EPSDKManager.shared.uploadImages([image],
progress: { uploaded, total in
EPProgressHUD.showProgress(uploaded, total: total)
},
success: { [weak self] resList in
EPProgressHUD.dismiss()
guard !resList.isEmpty,
let firstRes = resList.first,
let avatarUrl = firstRes["resUrl"] as? String else {
print("[EPEditSetting] 头像上传成功但无法获取URL")
return
}
print("[EPEditSetting] 头像上传成功: \(avatarUrl)")
// API
self?.updateAvatarAPI(avatarUrl: avatarUrl)
},
failure: { [weak self] errorMsg in
EPProgressHUD.dismiss()
print("[EPEditSetting] 头像上传失败: \(errorMsg)")
//
DispatchQueue.main.async {
let alert = UIAlertController(title: "上传失败", message: errorMsg, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self?.present(alert, animated: true)
}
}
)
}
private func updateAvatarAPI(avatarUrl: String) {
// 使 API Helper
apiHelper.updateAvatar(withUrl: avatarUrl, completion: { [weak self] in
print("[EPEditSetting] 头像更新成功")
//
self?.userInfo?.avatar = avatarUrl
//
self?.notifyParentAvatarUpdated(avatarUrl)
}, failure: { [weak self] (code: Int, msg: String?) in
print("[EPEditSetting] 头像更新失败: \(code) - \(msg ?? "未知错误")")
//
DispatchQueue.main.async {
let alert = UIAlertController(
title: "更新失败",
message: msg ?? "头像更新失败,请稍后重试",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self?.present(alert, animated: true)
}
})
}
private func notifyParentAvatarUpdated(_ avatarUrl: String) {
// EPMineViewController
let userInfo = ["avatarUrl": avatarUrl]
NotificationCenter.default.post(name: NSNotification.Name("EPEditSettingAvatarUpdated"), object: nil, userInfo: userInfo)
}
}
// 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 ()
@@ -39,27 +41,21 @@
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
[self setupUI]; [self setupUI];
NSLog(@"[EPMineViewController] 个人主页加载完成"); NSLog(@"[EPMineViewController] viewDidLoad 完成");
} }
- (void)viewWillAppear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated]; [super viewWillAppear:animated];
//
[self.navigationController setNavigationBarHidden:YES animated:animated]; [self.navigationController setNavigationBarHidden:YES animated:animated];
// //
[self loadUserDetailInfo]; [self loadUserDetailInfo];
} }
// MARK: - Setup // MARK: - Setup
- (void)setupUI {
//
self.view.backgroundColor = [UIColor clearColor];
- (void)setupUI {
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")]; UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
bgImageView.contentMode = UIViewContentModeScaleAspectFill; bgImageView.contentMode = UIViewContentModeScaleAspectFill;
bgImageView.clipsToBounds = YES; bgImageView.clipsToBounds = YES;
@@ -75,23 +71,40 @@
} }
- (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];
// 使 Masonry
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) { [self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(20); make.top.mas_equalTo(self.view);
make.leading.trailing.equalTo(self.view); make.leading.mas_equalTo(self.view);
make.height.equalTo(@300); make.trailing.mas_equalTo(self.view);
make.height.mas_equalTo(kGetScaleWidth(260));
}]; }];
//
__weak typeof(self) weakSelf = self;
self.headerView.onSettingsButtonTapped = ^{
__strong typeof(weakSelf) self = weakSelf;
[self openSettings];
};
//
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAvatarUpdated:)
name:@"EPEditSettingAvatarUpdated"
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) { [self.momentListView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.headerView.mas_bottom).offset(10); make.top.mas_equalTo(self.headerView.mas_bottom);
make.leading.trailing.bottom.equalTo(self.view); make.bottom.mas_equalTo(self.view);
make.leading.mas_equalTo(self.view);
make.trailing.mas_equalTo(self.view);
}]; }];
} }
@@ -99,52 +112,51 @@
- (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; @kWeakify(self);
[self.apiHelper getUserDetailInfoWithUid:uid completion:^(UserInfoModel * _Nullable userInfo) { [self.apiHelper getUserDetailInfoWithUid:uid
__strong typeof(weakSelf) self = weakSelf; completion:^(UserInfoModel * _Nullable userInfo) {
self.userInfo = userInfo; @kStrongify(self);
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 +174,40 @@
return _apiHelper; return _apiHelper;
} }
// MARK: - Actions
- (void)openSettings {
//
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@""
style:UIBarButtonItemStylePlain
target:nil
action:nil];
EPEditSettingViewController *settingsVC = [[EPEditSettingViewController alloc] init];
//
if (self.userInfo) {
[settingsVC updateWithUserInfo:self.userInfo];
}
[self.navigationController pushViewController:settingsVC animated:YES];
NSLog(@"[EPMineViewController] 打开设置页面,已传递用户信息");
}
- (void)onAvatarUpdated:(NSNotification *)notification {
NSString *avatarUrl = notification.userInfo[@"avatarUrl"];
if (avatarUrl && self.userInfo) {
//
self.userInfo.avatar = avatarUrl;
// UI
[self updateHeaderWithUserInfo:self.userInfo];
NSLog(@"[EPMineViewController] 头像已更新: %@", avatarUrl);
}
}
- (void)dealloc {
// 使 block
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"EPEditSettingAvatarUpdated" object:nil];
}
@end @end

View File

@@ -24,6 +24,16 @@ NS_ASSUME_NONNULL_BEGIN
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure; failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
/// 更新用户头像
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
/// 更新用户昵称
- (void)updateNicknameWithNick:(NSString *)nickname
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -9,6 +9,7 @@
#import "Api+Mine.h" #import "Api+Mine.h"
#import "UserInfoModel.h" #import "UserInfoModel.h"
#import "BaseModel.h" #import "BaseModel.h"
#import "AccountInfoStorage.h"
@implementation EPMineAPIHelper @implementation EPMineAPIHelper
@@ -38,5 +39,39 @@
} uid:uid page:@"1" pageSize:@"20"]; } uid:uid page:@"1" pageSize:@"20"];
} }
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
[Api userV2UploadAvatar:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
if (completion) completion();
} else {
if (failure) failure(code, msg);
}
} avatarUrl:avatarUrl needPay:@NO];
}
- (void)updateNicknameWithNick:(NSString *)nickname
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
NSString *uid = [[AccountInfoStorage instance] getUid];
NSString *ticket = [[AccountInfoStorage instance] getTicket];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (nickname.length > 0) {
[params setValue:nickname forKey:@"nick"];
}
[params setObject:uid forKey:@"uid"];
[params setObject:ticket forKey:@"ticket"];
[Api completeUserInfo:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
if (completion) completion();
} else {
if (failure) failure(code, msg);
}
} userInfo:params];
}
@end @end

View File

@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 大圆形头像 + 渐变背景 + 用户信息展示 /// 大圆形头像 + 渐变背景 + 用户信息展示
@interface EPMineHeaderView : UIView @interface EPMineHeaderView : UIView
/// 设置按钮点击回调
@property (nonatomic, copy, nullable) void(^onSettingsButtonTapped)(void);
/// 更新用户信息 /// 更新用户信息
/// @param userInfoDict 用户信息字典 /// @param userInfoDict 用户信息字典
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict; - (void)updateWithUserInfo:(NSDictionary *)userInfoDict;

View File

@@ -24,12 +24,6 @@
/// ///
@property (nonatomic, strong) UIButton *settingsButton; @property (nonatomic, strong) UIButton *settingsButton;
///
@property (nonatomic, strong) UIButton *followButton;
///
@property (nonatomic, strong) UIButton *fansButton;
@end @end
@implementation EPMineHeaderView @implementation EPMineHeaderView
@@ -97,36 +91,6 @@
make.trailing.equalTo(self).offset(-20); make.trailing.equalTo(self).offset(-20);
make.size.mas_equalTo(CGSizeMake(40, 40)); make.size.mas_equalTo(CGSizeMake(40, 40));
}]; }];
//
self.followButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.followButton setTitle:@"关注" forState:UIControlStateNormal];
[self.followButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.followButton.titleLabel.font = [UIFont systemFontOfSize:16];
self.followButton.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.2];
self.followButton.layer.cornerRadius = 20;
[self addSubview:self.followButton];
[self.followButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.idLabel.mas_bottom).offset(20);
make.centerX.equalTo(self).offset(-50);
make.size.mas_equalTo(CGSizeMake(80, 40));
}];
//
self.fansButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.fansButton setTitle:@"粉丝" forState:UIControlStateNormal];
[self.fansButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.fansButton.titleLabel.font = [UIFont systemFontOfSize:16];
self.fansButton.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.2];
self.fansButton.layer.cornerRadius = 20;
[self addSubview:self.fansButton];
[self.fansButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.idLabel.mas_bottom).offset(20);
make.centerX.equalTo(self).offset(50);
make.size.mas_equalTo(CGSizeMake(80, 40));
}];
} }
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict { - (void)updateWithUserInfo:(NSDictionary *)userInfoDict {
@@ -138,14 +102,6 @@
NSString *uid = userInfoDict[@"uid"] ?: @""; NSString *uid = userInfoDict[@"uid"] ?: @"";
self.idLabel.text = [NSString stringWithFormat:@"ID:%@", uid]; self.idLabel.text = [NSString stringWithFormat:@"ID:%@", uid];
//
NSNumber *following = userInfoDict[@"following"] ?: @0;
[self.followButton setTitle:[NSString stringWithFormat:@"关注 %@", following] forState:UIControlStateNormal];
//
NSNumber *followers = userInfoDict[@"followers"] ?: @0;
[self.fansButton setTitle:[NSString stringWithFormat:@"粉丝 %@", followers] forState:UIControlStateNormal];
// //
NSString *avatarURL = userInfoDict[@"avatar"]; NSString *avatarURL = userInfoDict[@"avatar"];
if (avatarURL && avatarURL.length > 0) { if (avatarURL && avatarURL.length > 0) {
@@ -159,7 +115,10 @@
- (void)settingsButtonTapped { - (void)settingsButtonTapped {
NSLog(@"[EPMineHeaderView] 设置按钮点击"); NSLog(@"[EPMineHeaderView] 设置按钮点击");
// TODO: // 使 block
if (self.onSettingsButtonTapped) {
self.onSettingsButtonTapped();
}
} }
@end @end

View File

@@ -90,10 +90,16 @@ NSString *const EPMomentPublishSuccessNotification = @"EPMomentPublishSuccessNot
make.leading.trailing.equalTo(self.textView); make.leading.trailing.equalTo(self.textView);
make.height.mas_equalTo(1); make.height.mas_equalTo(1);
}]; }];
// 3
// itemW = ( - 30 - 20) / 3
// = 3itemW + 2(10*2)
CGFloat itemW = (KScreenWidth - 15*2 - 10*2)/3.0;
CGFloat collectionHeight = itemW * 3 + 10 * 2;
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.contentView).inset(15); make.leading.trailing.equalTo(self.contentView).inset(15);
make.top.equalTo(self.lineView.mas_bottom).offset(10); make.top.equalTo(self.lineView.mas_bottom).offset(10);
make.height.mas_equalTo(110); make.height.mas_equalTo(collectionHeight);
}]; }];
// //

View File

@@ -54,9 +54,6 @@
// MARK: - Setup UI // MARK: - Setup UI
- (void)setupUI { - (void)setupUI {
//
self.view.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.97 alpha:1.0];
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")]; UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
bgImageView.contentMode = UIViewContentModeScaleAspectFill; bgImageView.contentMode = UIViewContentModeScaleAspectFill;
bgImageView.clipsToBounds = YES; bgImageView.clipsToBounds = YES;

View File

@@ -1,35 +0,0 @@
//
// EPMomentAPIHelper.h
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <Foundation/Foundation.h>
#import "BaseMvpPresenter.h"
#import "MomentsInfoModel.h"
#import "MomentsListInfoModel.h"
NS_ASSUME_NONNULL_BEGIN
/// 推荐/我的动态列表数据源类型
typedef NS_ENUM(NSInteger, EPMomentListSourceType) {
EPMomentListSourceTypeRecommend = 0,
EPMomentListSourceTypeMine = 1
};
/// 统一封装 Moments 列表 API
@interface EPMomentAPIHelper : BaseMvpPresenter
/// 拉取最新动态列表(默认 types:"0,2" 图片+文字)
- (void)fetchLatestMomentsWithNextID:(NSString *)nextID
completion:(void (^)(NSArray <MomentsInfoModel *>* _Nullable list, NSString *nextMomentID))completion
failure:(void(^)(NSInteger code, NSString * _Nullable msg))failure;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,42 +0,0 @@
//
// EPMomentAPIHelper.m
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <UIKit/UIKit.h>
#import "EPMomentAPIHelper.h"
#import "Api+Moments.h"
#import "AccountInfoStorage.h"
#import "BaseModel.h"
@implementation EPMomentAPIHelper
// [Api momentsRecommendList:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
// if (code == 200 && data.data) {
// NSArray *array = [MomentsInfoModel modelsWithArray:data.data];
// if (completion) completion(array ?: @[], 200, @"success");
// } else {
// if (completion) completion(@[], code, msg);
// }
// } page:pageStr pageSize:pageSizeStr types:types];
- (void)fetchLatestMomentsWithNextID:(NSString *)nextID
completion:(void (^)(NSArray <MomentsInfoModel *>* _Nullable list, NSString *nextMomentID))completion
failure:(void(^)(NSInteger code, NSString * _Nullable msg))failure {
NSString *pageSizeStr = @"20";
NSString *types = @"0,2"; // +
[Api momentsLatestList:[self createHttpCompletion:^(BaseModel * _Nonnull data) {
MomentsListInfoModel *listInfo = [MomentsListInfoModel modelWithDictionary:data.data];
if (completion) completion(listInfo.dynamicList ?: @[],
listInfo.nextDynamicId);
} fail:^(NSInteger code, NSString * _Nullable msg) {
if (failure) failure(code, msg);
}] dynamicId:nextID pageSize:pageSizeStr types:types];
}
@end

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

@@ -120,7 +120,8 @@
make.leading.trailing.equalTo(self.cardView); make.leading.trailing.equalTo(self.cardView);
make.top.equalTo(self.imagesContainer.mas_bottom).offset(12); make.top.equalTo(self.imagesContainer.mas_bottom).offset(12);
make.height.mas_equalTo(50); make.height.mas_equalTo(50);
make.bottom.equalTo(self.cardView).offset(-8); //
make.bottom.equalTo(self.cardView).offset(-8).priority(UILayoutPriorityDefaultHigh);
}]; }];
// //

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

@@ -361,8 +361,51 @@ import SnapKit
normalImage: normalImage, normalImage: normalImage,
selectedImage: selectedImage selectedImage: selectedImage
) )
// delegate
nav.delegate = self
return nav return nav
} }
// MARK: - TabBar Visibility Control
/// TabBar
private func showCustomTabBar(animated: Bool = true) {
guard customTabBarView.isHidden else { return }
if animated {
customTabBarView.isHidden = false
customTabBarView.alpha = 0
customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
self.customTabBarView.alpha = 1
self.customTabBarView.transform = .identity
}
} else {
customTabBarView.isHidden = false
customTabBarView.alpha = 1
}
}
/// TabBar
private func hideCustomTabBar(animated: Bool = true) {
guard !customTabBarView.isHidden else { return }
if animated {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.customTabBarView.alpha = 0
self.customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
}) { _ in
self.customTabBarView.isHidden = true
self.customTabBarView.transform = .identity
}
} else {
customTabBarView.isHidden = true
customTabBarView.alpha = 0
}
}
} }
// MARK: - UITabBarControllerDelegate // MARK: - UITabBarControllerDelegate
@@ -389,6 +432,29 @@ extension EPTabBarController: UITabBarControllerDelegate {
} }
} }
// MARK: - UINavigationControllerDelegate
extension EPTabBarController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool) {
//
let isRootViewController = navigationController.viewControllers.count == 1
if isRootViewController {
// TabBar
showCustomTabBar(animated: animated)
NSLog("[EPTabBarController] 显示 TabBar - 根页面")
} else {
// TabBar
hideCustomTabBar(animated: animated)
NSLog("[EPTabBarController] 隐藏 TabBar - 子页面 (层级: \(navigationController.viewControllers.count))")
}
}
}
// MARK: - OC Compatibility // MARK: - OC Compatibility
extension EPTabBarController { extension EPTabBarController {

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,52 @@
// 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: - API Helpers
#import "EPMineAPIHelper.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