基于 Lottie 在关键流程中新增全局 Loading 遮罩

引入带引用计数的 GlobalLoadingCenter,通过 RootView 及主要登录/订单/个人中心页面接入,并添加 Loading 动画资源与单元测试。

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

View File

@ -18,6 +18,7 @@ struct AccountSwitchView: View {
@Environment(AccountContextAPI.self) private var accountContextAPI
@Environment(AuthSessionCoordinator.self) private var authSessionCoordinator
@Environment(ToastCenter.self) private var toastCenter
@Environment(\.globalLoading) private var globalLoading
@Environment(\.dismiss) private var dismiss
@State private var viewModel = AccountSwitchViewModel()
@ -71,15 +72,9 @@ struct AccountSwitchView: View {
Button {
Task { await confirmSelection() }
} label: {
HStack(spacing: 8) {
if viewModel.switching {
ProgressView()
.tint(.white)
}
Text("确认切换")
.font(.system(size: 16, weight: .semibold))
}
.foregroundStyle(.white)
Text("确认切换")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(canConfirm ? AppDesign.primary : Color(hex: 0xC9CED6), in: RoundedRectangle(cornerRadius: 12))
@ -187,7 +182,9 @@ struct AccountSwitchView: View {
///
private func loadAccounts(force: Bool = false) async {
do {
try await viewModel.load(api: profileAPI, force: force, currentAccountId: currentAccountId)
try await globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") {
try await viewModel.load(api: profileAPI, force: force, currentAccountId: currentAccountId)
}
} catch is CancellationError {
return
} catch {
@ -204,18 +201,20 @@ struct AccountSwitchView: View {
}
do {
let response = try await viewModel.switchAccount(account, api: authAPI)
let username = nonEmpty(accountContext.profile?.phone) ?? nonEmpty(account.phone) ?? ""
try await authSessionCoordinator.completeLogin(
with: response,
username: username,
privacyAgreementAccepted: authSessionCoordinator.loginPreferences().privacyAgreementAccepted,
appSession: appSession,
accountContext: accountContext,
permissionContext: permissionContext,
profileAPI: profileAPI,
accountContextAPI: accountContextAPI
)
try await globalLoading.withLoading(message: "账号切换中...") {
let response = try await viewModel.switchAccount(account, api: authAPI)
let username = nonEmpty(accountContext.profile?.phone) ?? nonEmpty(account.phone) ?? ""
try await authSessionCoordinator.completeLogin(
with: response,
username: username,
privacyAgreementAccepted: authSessionCoordinator.loginPreferences().privacyAgreementAccepted,
appSession: appSession,
accountContext: accountContext,
permissionContext: permissionContext,
profileAPI: profileAPI,
accountContextAPI: accountContextAPI
)
}
appRouter.reset()
toastCenter.show("账号已切换")
dismiss()

View File

@ -21,6 +21,7 @@ struct ProfileView: View {
@Environment(OSSUploadService.self) private var ossUploadService
@Environment(ToastCenter.self) private var toastCenter
@Environment(AuthSessionCoordinator.self) private var authSessionCoordinator
@Environment(\.globalLoading) private var globalLoading
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var viewModel = ProfileViewModel()
@ -73,14 +74,6 @@ struct ProfileView: View {
}
Button("取消", role: .cancel) {}
}
.overlay {
if viewModel.isLoading && viewModel.userInfo == nil {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.opacity(0.35))
}
}
.onChange(of: viewModel.isEditingProfile) { _, isEditing in
guard isEditing else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
@ -512,7 +505,9 @@ struct ProfileView: View {
///
private func reloadProfile(showToast: Bool) async {
do {
try await viewModel.reload(api: profileAPI)
try await globalLoading.withOptionalLoading(!showToast && viewModel.userInfo == nil, message: "加载中...") {
try await viewModel.reload(api: profileAPI)
}
if let userInfo = viewModel.userInfo {
authSessionCoordinator.refreshCachedProfile(from: userInfo, accountContext: accountContext)
} else {
@ -531,11 +526,13 @@ struct ProfileView: View {
///
private func saveProfileEdits() async {
do {
try await viewModel.saveProfile(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
try await globalLoading.withLoading(message: "保存中...") {
try await viewModel.saveProfile(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
}
if let userInfo = viewModel.userInfo {
authSessionCoordinator.refreshCachedProfile(from: userInfo, accountContext: accountContext)
} else {

View File

@ -15,6 +15,7 @@ struct RealNameAuthView: View {
@Environment(ProfileAPI.self) private var profileAPI
@Environment(OSSUploadService.self) private var ossUploadService
@Environment(ToastCenter.self) private var toastCenter
@Environment(\.globalLoading) private var globalLoading
@State private var viewModel = RealNameAuthViewModel()
@State private var pickedFrontItem: PhotosPickerItem?
@State private var pickedBackItem: PhotosPickerItem?
@ -37,14 +38,6 @@ struct RealNameAuthView: View {
.task {
await loadInfo()
}
.overlay {
if viewModel.loading && viewModel.info == nil {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.opacity(0.35))
}
}
.onChange(of: pickedFrontItem) { _, item in
Task { await prepareIdentityImage(from: item, side: .front) }
}
@ -380,7 +373,9 @@ struct RealNameAuthView: View {
///
private func loadInfo() async {
do {
try await viewModel.load(api: profileAPI)
try await globalLoading.withOptionalLoading(viewModel.info == nil, message: "加载中...") {
try await viewModel.load(api: profileAPI)
}
} catch is CancellationError {
return
} catch {
@ -400,11 +395,13 @@ struct RealNameAuthView: View {
///
private func submit() async {
do {
try await viewModel.submit(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
try await globalLoading.withLoading(message: "提交中...") {
try await viewModel.submit(
api: profileAPI,
uploader: ossUploadService,
scenicId: accountContext.currentScenic?.id ?? 0
)
}
} catch {
viewModel.statusMessage = error.localizedDescription
toastCenter.show(error.localizedDescription)