fix: 增加登录失效确认弹窗

This commit is contained in:
2026-07-31 15:25:12 +08:00
parent 0cf0a41a60
commit 212607fc82
3 changed files with 201 additions and 3 deletions

View File

@ -8,6 +8,7 @@ import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private var sessionExpiredDialog: SessionExpiredDialogViewController?
func scene(
_ scene: UIScene,
@ -86,18 +87,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
@objc private func handleSessionDidExpire() {
guard AppStore.shared.session.isLoggedIn else { return }
guard sessionExpiredDialog == nil else { return }
GlobalLoadingManager.shared.hideAll()
let dialog = SessionExpiredDialogViewController { [weak self] in
self?.transitionToLogin()
}
sessionExpiredDialog = dialog
guard let presenter = topViewController(from: window?.rootViewController) else {
sessionExpiredDialog = nil
return
}
presenter.present(dialog, animated: true)
}
private func transitionToLogin() {
sessionExpiredDialog = nil
PushNotificationManager.shared.handleLogout()
AppStore.shared.logout()
AppRouter.setRoot(.login, on: window)
}
@objc private func handleUserDidLogout() {
PushNotificationManager.shared.handleLogout()
AppStore.shared.logout()
AppRouter.setRoot(.login, on: window)
transitionToLogin()
}
@objc private func handleUserDidLogin() {
sessionExpiredDialog?.dismiss(animated: false)
sessionExpiredDialog = nil
AppRouter.setRoot(.mainTab, on: window)
DispatchQueue.main.async {
PushNotificationManager.shared.handleLoginCompleted()
@ -108,4 +127,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@objc private func handleAccountDidSwitch() {
PushNotificationManager.shared.handleAccountSwitched()
}
private func topViewController(from viewController: UIViewController?) -> UIViewController? {
guard let viewController else { return nil }
if let presented = viewController.presentedViewController {
return topViewController(from: presented)
}
if let navigationController = viewController as? UINavigationController {
return topViewController(from: navigationController.visibleViewController)
}
if let tabBarController = viewController as? UITabBarController {
return topViewController(from: tabBarController.selectedViewController)
}
return viewController
}
}

View File

@ -0,0 +1,103 @@
//
// SessionExpiredDialogViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// 退
@MainActor
final class SessionExpiredDialogViewController: UIViewController {
private let onConfirm: () -> Void
private let confirmButton = AppButton(title: "确认并退出登录")
private var didConfirm = false
///
init(onConfirm: @escaping () -> Void) {
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
isModalInPresentation = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = UIColor.black.withAlphaComponent(0.32)
view.accessibilityViewIsModal = true
view.accessibilityIdentifier = "sessionExpired.dialog"
let cardView = UIView()
cardView.backgroundColor = AppColor.cardBackground
cardView.layer.cornerRadius = AppRadius.lg
cardView.clipsToBounds = true
let warningImageView = UIImageView(
image: UIImage(systemName: "exclamationmark.circle.fill")
)
warningImageView.tintColor = UIColor(hex: 0xF05A5A)
warningImageView.contentMode = .scaleAspectFit
warningImageView.isAccessibilityElement = true
warningImageView.accessibilityLabel = "登录状态异常"
let titleLabel = UILabel()
titleLabel.text = "登录提醒"
titleLabel.textColor = AppColor.textPrimary
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
titleLabel.textAlignment = .center
let messageLabel = UILabel()
messageLabel.text = "当前用户已在其他设备登录,\n请重新登录"
messageLabel.textColor = UIColor(hex: 0x4B5563)
messageLabel.font = .systemFont(ofSize: 14)
messageLabel.textAlignment = .center
messageLabel.numberOfLines = 0
confirmButton.accessibilityIdentifier = "sessionExpired.confirmButton"
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
let contentStack = UIStackView(arrangedSubviews: [
warningImageView,
titleLabel,
messageLabel,
confirmButton,
])
contentStack.axis = .vertical
contentStack.alignment = .fill
contentStack.spacing = 16
contentStack.setCustomSpacing(4, after: titleLabel)
contentStack.setCustomSpacing(20, after: messageLabel)
view.addSubview(cardView)
cardView.addSubview(contentStack)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(32)
}
contentStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(24)
}
warningImageView.snp.makeConstraints { make in
make.height.equalTo(64)
}
}
@objc private func confirmTapped() {
guard !didConfirm else { return }
didConfirm = true
confirmButton.isEnabled = false
onConfirm()
}
}

View File

@ -0,0 +1,62 @@
//
// SessionExpiredDialogViewControllerTests.swift
// suixinkanTests
//
import UIKit
import XCTest
@testable import suixinkan
/// Android
@MainActor
final class SessionExpiredDialogViewControllerTests: XCTestCase {
func testDialogUsesForcedPresentationAndAndroidCopy() throws {
let dialog = SessionExpiredDialogViewController(onConfirm: {})
dialog.loadViewIfNeeded()
XCTAssertEqual(dialog.modalPresentationStyle, .overFullScreen)
XCTAssertEqual(dialog.modalTransitionStyle, .crossDissolve)
XCTAssertTrue(dialog.isModalInPresentation)
XCTAssertTrue(dialog.view.accessibilityViewIsModal)
let labels = dialog.view.allSubviews(of: UILabel.self)
XCTAssertTrue(labels.contains { $0.text == "登录提醒" })
XCTAssertTrue(labels.contains { $0.text == "当前用户已在其他设备登录,\n请重新登录" })
let button = try XCTUnwrap(
dialog.view.allSubviews(of: UIButton.self).first {
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
}
)
XCTAssertEqual(button.title(for: .normal), "确认并退出登录")
}
func testConfirmCallbackOnlyRunsOnce() throws {
var confirmationCount = 0
let dialog = SessionExpiredDialogViewController {
confirmationCount += 1
}
dialog.loadViewIfNeeded()
let button = try XCTUnwrap(
dialog.view.allSubviews(of: UIButton.self).first {
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
}
)
button.sendActions(for: .touchUpInside)
button.sendActions(for: .touchUpInside)
XCTAssertEqual(confirmationCount, 1)
XCTAssertFalse(button.isEnabled)
}
}
private extension UIView {
func allSubviews<View: UIView>(of type: View.Type) -> [View] {
subviews.flatMap { subview -> [View] in
let current = (subview as? View).map { [$0] } ?? []
return current + subview.allSubviews(of: type)
}
}
}