移除 SplashCoordinator,bootstrap 期间用 SplashView 覆盖避免空白闪屏,其余账号数据改为后台补充刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
261 lines
12 KiB
Swift
261 lines
12 KiB
Swift
//
|
||
// RootView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/18.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// App 根视图,负责创建全局依赖并根据登录状态切换登录页和主界面。
|
||
struct RootView: View {
|
||
@Environment(\.scenePhase) private var scenePhase
|
||
@StateObject private var appSession = AppSession()
|
||
@StateObject private var accountContext = AccountContext()
|
||
@StateObject private var permissionContext = PermissionContext()
|
||
@StateObject private var scenicSpotContext = ScenicSpotContext()
|
||
@StateObject private var appRouter = AppRouter()
|
||
@StateObject private var toastCenter = ToastCenter()
|
||
@State private var globalLoading = GlobalLoadingCenter()
|
||
@State private var snapshotStore: AccountSnapshotStore
|
||
@State private var apiClient: APIClient
|
||
@State private var authAPI: AuthAPI
|
||
@State private var profileAPI: ProfileAPI
|
||
@State private var uploadAPI: UploadAPI
|
||
@State private var ossUploadService: OSSUploadService
|
||
@State private var accountContextAPI: AccountContextAPI
|
||
@State private var ordersAPI: OrdersAPI
|
||
@State private var statisticsAPI: StatisticsAPI
|
||
@State private var paymentAPI: PaymentAPI
|
||
@State private var walletAPI: WalletAPI
|
||
@State private var pushAPI: PushAPI
|
||
@State private var scenicPermissionAPI: ScenicPermissionAPI
|
||
@State private var scenicSettlementAPI: ScenicSettlementAPI
|
||
@State private var messageCenterAPI: MessageCenterAPI
|
||
@State private var scenicQueueAPI: ScenicQueueAPI
|
||
@StateObject private var scenicQueueRuntime = ScenicQueueRuntime()
|
||
@State private var liveAPI: LiveAPI
|
||
@State private var operatingAreaAPI: OperatingAreaAPI
|
||
@State private var pilotCertificationAPI: PilotCertificationAPI
|
||
@State private var taskAPI: TaskAPI
|
||
@State private var projectAPI: ProjectAPI
|
||
@State private var scheduleAPI: ScheduleAPI
|
||
@State private var inviteAPI: InviteAPI
|
||
@State private var assetsAPI: AssetsAPI
|
||
@State private var punchPointAPI: PunchPointAPI
|
||
@State private var locationReportAPI: LocationReportAPI
|
||
@StateObject private var homeLocationViewModel: HomeLocationViewModel
|
||
@State private var cooperationOrderAPI: CooperationOrderAPI
|
||
@State private var authSessionCoordinator: AuthSessionCoordinator
|
||
@State private var sessionBootstrapper: SessionBootstrapper
|
||
@State private var isColdStartBootstrapPending = true
|
||
@State private var appConfigAPI: AppConfigAPI
|
||
|
||
/// 初始化网络客户端和业务 API,确保它们共享同一个 token provider。
|
||
init() {
|
||
let apiClient = APIClient()
|
||
let tokenStore = SessionTokenStore()
|
||
let snapshotStore = AccountSnapshotStore()
|
||
_snapshotStore = State(initialValue: snapshotStore)
|
||
_apiClient = State(initialValue: apiClient)
|
||
_authAPI = State(initialValue: AuthAPI(client: apiClient))
|
||
_appConfigAPI = State(initialValue: AppConfigAPI(client: apiClient))
|
||
_profileAPI = State(initialValue: ProfileAPI(client: apiClient))
|
||
let uploadAPI = UploadAPI(client: apiClient)
|
||
_uploadAPI = State(initialValue: uploadAPI)
|
||
_ossUploadService = State(initialValue: OSSUploadService(configService: uploadAPI))
|
||
_accountContextAPI = State(initialValue: AccountContextAPI(client: apiClient))
|
||
_ordersAPI = State(initialValue: OrdersAPI(client: apiClient))
|
||
_statisticsAPI = State(initialValue: StatisticsAPI(client: apiClient))
|
||
_paymentAPI = State(initialValue: PaymentAPI(client: apiClient))
|
||
_walletAPI = State(initialValue: WalletAPI(client: apiClient))
|
||
_pushAPI = State(initialValue: PushAPI(client: apiClient))
|
||
_scenicPermissionAPI = State(initialValue: ScenicPermissionAPI(client: apiClient))
|
||
_scenicSettlementAPI = State(initialValue: ScenicSettlementAPI(client: apiClient))
|
||
_messageCenterAPI = State(initialValue: MessageCenterAPI(client: apiClient))
|
||
_scenicQueueAPI = State(initialValue: ScenicQueueAPI(client: apiClient))
|
||
_liveAPI = State(initialValue: LiveAPI(client: apiClient))
|
||
_operatingAreaAPI = State(initialValue: OperatingAreaAPI(client: apiClient))
|
||
_pilotCertificationAPI = State(initialValue: PilotCertificationAPI(client: apiClient))
|
||
_taskAPI = State(initialValue: TaskAPI(client: apiClient))
|
||
_projectAPI = State(initialValue: ProjectAPI(client: apiClient))
|
||
_scheduleAPI = State(initialValue: ScheduleAPI(client: apiClient))
|
||
_inviteAPI = State(initialValue: InviteAPI(client: apiClient))
|
||
_assetsAPI = State(initialValue: AssetsAPI(client: apiClient))
|
||
_punchPointAPI = State(initialValue: PunchPointAPI(client: apiClient))
|
||
let locationReportAPI = LocationReportAPI(client: apiClient)
|
||
_locationReportAPI = State(initialValue: locationReportAPI)
|
||
_homeLocationViewModel = StateObject(wrappedValue: HomeLocationViewModel(api: locationReportAPI))
|
||
_cooperationOrderAPI = State(initialValue: CooperationOrderAPI(client: apiClient))
|
||
_authSessionCoordinator = State(
|
||
initialValue: AuthSessionCoordinator(
|
||
tokenStore: tokenStore,
|
||
snapshotStore: snapshotStore,
|
||
preferencesStore: AppPreferencesStore()
|
||
)
|
||
)
|
||
_sessionBootstrapper = State(
|
||
initialValue: SessionBootstrapper(
|
||
tokenStore: tokenStore,
|
||
snapshotStore: snapshotStore
|
||
)
|
||
)
|
||
}
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
rootContent
|
||
|
||
if isColdStartBootstrapPending {
|
||
SplashView()
|
||
.transition(.opacity)
|
||
.zIndex(1)
|
||
}
|
||
}
|
||
.globalToastOverlay()
|
||
.globalLoadingOverlay(loadingCenter: globalLoading)
|
||
.environmentObject(appSession)
|
||
.environmentObject(accountContext)
|
||
.environmentObject(permissionContext)
|
||
.environmentObject(scenicSpotContext)
|
||
.environmentObject(appRouter)
|
||
.environmentObject(toastCenter)
|
||
.environment(\.globalLoading, globalLoading)
|
||
.environment(\.accountSnapshotStore, snapshotStore)
|
||
.environment(\.apiClient, apiClient)
|
||
.environment(\.authAPI, authAPI)
|
||
.environment(\.appConfigAPI, appConfigAPI)
|
||
.environment(\.profileAPI, profileAPI)
|
||
.environment(\.uploadAPI, uploadAPI)
|
||
.environment(\.ossUploadService, ossUploadService)
|
||
.environment(\.accountContextAPI, accountContextAPI)
|
||
.environment(\.ordersAPI, ordersAPI)
|
||
.environment(\.statisticsAPI, statisticsAPI)
|
||
.environment(\.paymentAPI, paymentAPI)
|
||
.environment(\.walletAPI, walletAPI)
|
||
.environment(\.pushAPI, pushAPI)
|
||
.environment(\.scenicPermissionAPI, scenicPermissionAPI)
|
||
.environment(\.scenicSettlementAPI, scenicSettlementAPI)
|
||
.environment(\.messageCenterAPI, messageCenterAPI)
|
||
.environment(\.scenicQueueAPI, scenicQueueAPI)
|
||
.environmentObject(scenicQueueRuntime)
|
||
.environment(\.liveAPI, liveAPI)
|
||
.environment(\.operatingAreaAPI, operatingAreaAPI)
|
||
.environment(\.pilotCertificationAPI, pilotCertificationAPI)
|
||
.environment(\.taskAPI, taskAPI)
|
||
.environment(\.projectAPI, projectAPI)
|
||
.environment(\.scheduleAPI, scheduleAPI)
|
||
.environment(\.inviteAPI, inviteAPI)
|
||
.environment(\.assetsAPI, assetsAPI)
|
||
.environment(\.punchPointAPI, punchPointAPI)
|
||
.environment(\.locationReportAPI, locationReportAPI)
|
||
.environmentObject(homeLocationViewModel)
|
||
.environment(\.cooperationOrderAPI, cooperationOrderAPI)
|
||
.environment(\.authSessionCoordinator, authSessionCoordinator)
|
||
.task {
|
||
apiClient.bindAuthTokenProvider { appSession.token }
|
||
PushNotificationManager.shared.configure(api: pushAPI, session: appSession, router: appRouter)
|
||
await sessionBootstrapper.restore(
|
||
appSession: appSession,
|
||
accountContext: accountContext,
|
||
permissionContext: permissionContext,
|
||
profileAPI: profileAPI,
|
||
accountContextAPI: accountContextAPI,
|
||
toastCenter: toastCenter
|
||
)
|
||
withAnimation {
|
||
isColdStartBootstrapPending = false
|
||
}
|
||
if appSession.isLoggedIn, !AppUITestLaunchState.isRunningUITests {
|
||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||
}
|
||
}
|
||
.task(id: scenicSpotTaskID) {
|
||
guard appSession.isLoggedIn else {
|
||
scenicSpotContext.reset()
|
||
return
|
||
}
|
||
await scenicSpotContext.reload(
|
||
scenicId: accountContext.currentScenic?.id,
|
||
api: accountContextAPI
|
||
)
|
||
}
|
||
.onChange(of: scenePhase) { newPhase in
|
||
guard !AppUITestLaunchState.isRunningUITests else { return }
|
||
if appSession.isLoggedIn {
|
||
scenicQueueRuntime.update(
|
||
api: scenicQueueAPI,
|
||
userId: accountContext.profile?.userId,
|
||
scenicId: accountContext.currentScenic?.id,
|
||
scenePhase: newPhase
|
||
)
|
||
} else {
|
||
scenicQueueRuntime.stop()
|
||
}
|
||
}
|
||
.onChange(of: accountContext.currentScenic?.id) { _ in
|
||
guard !AppUITestLaunchState.isRunningUITests else { return }
|
||
if appSession.isLoggedIn {
|
||
scenicQueueRuntime.update(
|
||
api: scenicQueueAPI,
|
||
userId: accountContext.profile?.userId,
|
||
scenicId: accountContext.currentScenic?.id,
|
||
scenePhase: scenePhase
|
||
)
|
||
}
|
||
}
|
||
.onChange(of: appSession.phase) { phase in
|
||
switch phase {
|
||
case .loggedIn:
|
||
if !AppUITestLaunchState.isRunningUITests {
|
||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||
}
|
||
case .loggedOut:
|
||
accountContext.reset()
|
||
permissionContext.reset()
|
||
scenicSpotContext.reset()
|
||
appRouter.reset()
|
||
toastCenter.dismiss()
|
||
case .restoring:
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var rootContent: some View {
|
||
switch appSession.phase {
|
||
case .loggedOut:
|
||
LoginView()
|
||
case .restoring:
|
||
Color.clear
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
case .loggedIn:
|
||
MainTabsView()
|
||
.task {
|
||
#if DEBUG
|
||
await AppUITestRouteDriver.applyIfNeeded(appRouter: appRouter)
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 景点加载任务的稳定标识,确保登录状态和当前景区变化都会触发任务。
|
||
private var scenicSpotTaskID: String {
|
||
let phaseText: String
|
||
switch appSession.phase {
|
||
case .loggedOut:
|
||
phaseText = "loggedOut"
|
||
case .restoring:
|
||
phaseText = "restoring"
|
||
case .loggedIn:
|
||
phaseText = "loggedIn"
|
||
}
|
||
return "\(phaseText)-\(accountContext.currentScenic?.id ?? 0)"
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
RootView()
|
||
}
|