Add Lottie-based global loading overlay across key flows.

Introduce GlobalLoadingCenter with reference counting, wire it through RootView and major auth/order/profile screens, and add the loading animation asset plus unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 11:25:56 +08:00
parent 1d1eb46ed8
commit 7fd964fe19
17 changed files with 514 additions and 139 deletions

View File

@ -71,15 +71,9 @@ struct AccountSelectionView: View {
guard let selectedAccount else { return }
onConfirm(selectedAccount)
} label: {
HStack(spacing: AppMetrics.Spacing.xSmall) {
if isLoading {
ProgressView()
.tint(.white)
}
Text("进入系统")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
}
.foregroundStyle(.white)
Text("进入系统")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
.background(canConfirm ? AppDesign.primary : Color(hex: 0xC9CED6), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))

View File

@ -17,6 +17,7 @@ struct LoginView: View {
@Environment(ProfileAPI.self) private var profileAPI
@Environment(AccountContextAPI.self) private var accountContextAPI
@Environment(AuthSessionCoordinator.self) private var authSessionCoordinator
@Environment(\.globalLoading) private var globalLoading
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var viewModel = LoginViewModel()
@FocusState private var focusedField: LoginField?
@ -111,27 +112,13 @@ struct LoginView: View {
Spacer().frame(height: AppMetrics.Spacing.mediumLarge)
Button(action: loginAction) {
HStack {
if viewModel.isLoading {
ProgressView()
.tint(.white)
.frame(width: AppMetrics.ControlSize.progressWidth)
} else {
Spacer()
.frame(width: AppMetrics.ControlSize.progressWidth)
}
Text("登录")
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
.foregroundStyle(.white)
Spacer()
.frame(width: AppMetrics.ControlSize.progressWidth)
}
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
.background(viewModel.canSubmit ? AppDesign.primary : Color.gray)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
Text("登录")
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.ControlSize.primaryButtonHeight)
.background(viewModel.canSubmit ? AppDesign.primary : Color.gray)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
}
.disabled(!viewModel.canSubmit || viewModel.isLoading)
.accessibilityIdentifier("login.submit")
@ -167,12 +154,14 @@ struct LoginView: View {
Task {
do {
let resolution = try await viewModel.login(authAPI: authAPI)
switch resolution {
case let .completed(response):
await completeLogin(with: response)
case .needsAccountSelection:
break
try await globalLoading.withLoading(message: "登录中...") {
let resolution = try await viewModel.login(authAPI: authAPI)
switch resolution {
case let .completed(response):
await completeLogin(with: response)
case .needsAccountSelection:
break
}
}
} catch is CancellationError {
// Keep cancellation silent; the current task was abandoned by SwiftUI.
@ -186,8 +175,10 @@ struct LoginView: View {
private func selectAccount(_ account: AccountSwitchAccount) {
Task {
do {
let response = try await viewModel.selectAccount(account, authAPI: authAPI)
await completeLogin(with: response)
try await globalLoading.withLoading(message: "账号切换中...") {
let response = try await viewModel.selectAccount(account, authAPI: authAPI)
await completeLogin(with: response)
}
} catch is CancellationError {
// Keep cancellation silent; the current task was abandoned by SwiftUI.
} catch {