从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
261
suixinkan/App/AppServiceEnvironment.swift
Normal file
261
suixinkan/App/AppServiceEnvironment.swift
Normal file
@ -0,0 +1,261 @@
|
||||
//
|
||||
// AppServiceEnvironment.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private enum EnvironmentServiceDefaults {
|
||||
static func apiClient() -> APIClient {
|
||||
APIClient()
|
||||
}
|
||||
}
|
||||
|
||||
private struct APIClientEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: APIClient { EnvironmentServiceDefaults.apiClient() }
|
||||
}
|
||||
|
||||
private struct UploadAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: UploadAPI { UploadAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct PushAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: PushAPI { PushAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct AccountSnapshotStoreEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: AccountSnapshotStore { AccountSnapshotStore() }
|
||||
}
|
||||
|
||||
private struct AuthSessionCoordinatorEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: AuthSessionCoordinator { AuthSessionCoordinator() }
|
||||
}
|
||||
|
||||
private struct AccountContextAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: AccountContextAPI { AccountContextAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct AssetsAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: AssetsAPI { AssetsAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct AuthAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: AuthAPI { AuthAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct InviteAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: InviteAPI { InviteAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct LiveAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: LiveAPI { LiveAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct LocationReportAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: LocationReportAPI { LocationReportAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct MessageCenterAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: MessageCenterAPI { MessageCenterAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct OperatingAreaAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: OperatingAreaAPI { OperatingAreaAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct OrdersAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: OrdersAPI { OrdersAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct OSSUploadServiceEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: OSSUploadService {
|
||||
OSSUploadService(configService: UploadAPI(client: EnvironmentServiceDefaults.apiClient()))
|
||||
}
|
||||
}
|
||||
|
||||
private struct PaymentAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: PaymentAPI { PaymentAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct PilotCertificationAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: PilotCertificationAPI { PilotCertificationAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ProfileAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ProfileAPI { ProfileAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ProjectAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ProjectAPI { ProjectAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct PunchPointAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: PunchPointAPI { PunchPointAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ScenicPermissionAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ScenicPermissionAPI { ScenicPermissionAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ScenicQueueAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ScenicQueueAPI { ScenicQueueAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ScenicSettlementAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ScenicSettlementAPI { ScenicSettlementAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct ScheduleAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: ScheduleAPI { ScheduleAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct StatisticsAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: StatisticsAPI { StatisticsAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct TaskAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: TaskAPI { TaskAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
private struct WalletAPIEnvironmentKey: EnvironmentKey {
|
||||
static var defaultValue: WalletAPI { WalletAPI(client: EnvironmentServiceDefaults.apiClient()) }
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var apiClient: APIClient {
|
||||
get { self[APIClientEnvironmentKey.self] }
|
||||
set { self[APIClientEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var uploadAPI: UploadAPI {
|
||||
get { self[UploadAPIEnvironmentKey.self] }
|
||||
set { self[UploadAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var pushAPI: PushAPI {
|
||||
get { self[PushAPIEnvironmentKey.self] }
|
||||
set { self[PushAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var accountSnapshotStore: AccountSnapshotStore {
|
||||
get { self[AccountSnapshotStoreEnvironmentKey.self] }
|
||||
set { self[AccountSnapshotStoreEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var authSessionCoordinator: AuthSessionCoordinator {
|
||||
get { self[AuthSessionCoordinatorEnvironmentKey.self] }
|
||||
set { self[AuthSessionCoordinatorEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var accountContextAPI: AccountContextAPI {
|
||||
get { self[AccountContextAPIEnvironmentKey.self] }
|
||||
set { self[AccountContextAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var assetsAPI: AssetsAPI {
|
||||
get { self[AssetsAPIEnvironmentKey.self] }
|
||||
set { self[AssetsAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var authAPI: AuthAPI {
|
||||
get { self[AuthAPIEnvironmentKey.self] }
|
||||
set { self[AuthAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var inviteAPI: InviteAPI {
|
||||
get { self[InviteAPIEnvironmentKey.self] }
|
||||
set { self[InviteAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var liveAPI: LiveAPI {
|
||||
get { self[LiveAPIEnvironmentKey.self] }
|
||||
set { self[LiveAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var locationReportAPI: LocationReportAPI {
|
||||
get { self[LocationReportAPIEnvironmentKey.self] }
|
||||
set { self[LocationReportAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var messageCenterAPI: MessageCenterAPI {
|
||||
get { self[MessageCenterAPIEnvironmentKey.self] }
|
||||
set { self[MessageCenterAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var operatingAreaAPI: OperatingAreaAPI {
|
||||
get { self[OperatingAreaAPIEnvironmentKey.self] }
|
||||
set { self[OperatingAreaAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var ordersAPI: OrdersAPI {
|
||||
get { self[OrdersAPIEnvironmentKey.self] }
|
||||
set { self[OrdersAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var ossUploadService: OSSUploadService {
|
||||
get { self[OSSUploadServiceEnvironmentKey.self] }
|
||||
set { self[OSSUploadServiceEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var paymentAPI: PaymentAPI {
|
||||
get { self[PaymentAPIEnvironmentKey.self] }
|
||||
set { self[PaymentAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var pilotCertificationAPI: PilotCertificationAPI {
|
||||
get { self[PilotCertificationAPIEnvironmentKey.self] }
|
||||
set { self[PilotCertificationAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var profileAPI: ProfileAPI {
|
||||
get { self[ProfileAPIEnvironmentKey.self] }
|
||||
set { self[ProfileAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var projectAPI: ProjectAPI {
|
||||
get { self[ProjectAPIEnvironmentKey.self] }
|
||||
set { self[ProjectAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var punchPointAPI: PunchPointAPI {
|
||||
get { self[PunchPointAPIEnvironmentKey.self] }
|
||||
set { self[PunchPointAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var scenicPermissionAPI: ScenicPermissionAPI {
|
||||
get { self[ScenicPermissionAPIEnvironmentKey.self] }
|
||||
set { self[ScenicPermissionAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var scenicQueueAPI: ScenicQueueAPI {
|
||||
get { self[ScenicQueueAPIEnvironmentKey.self] }
|
||||
set { self[ScenicQueueAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var scenicSettlementAPI: ScenicSettlementAPI {
|
||||
get { self[ScenicSettlementAPIEnvironmentKey.self] }
|
||||
set { self[ScenicSettlementAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var scheduleAPI: ScheduleAPI {
|
||||
get { self[ScheduleAPIEnvironmentKey.self] }
|
||||
set { self[ScheduleAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var statisticsAPI: StatisticsAPI {
|
||||
get { self[StatisticsAPIEnvironmentKey.self] }
|
||||
set { self[StatisticsAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var taskAPI: TaskAPI {
|
||||
get { self[TaskAPIEnvironmentKey.self] }
|
||||
set { self[TaskAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
|
||||
var walletAPI: WalletAPI {
|
||||
get { self[WalletAPIEnvironmentKey.self] }
|
||||
set { self[WalletAPIEnvironmentKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
22
suixinkan/App/AppUITestLaunchState.swift
Normal file
22
suixinkan/App/AppUITestLaunchState.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// AppUITestLaunchState.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// UI 测试启动状态清理,仅在测试进程显式传入启动参数时执行。
|
||||
enum AppUITestLaunchState {
|
||||
static let resetArgument = "-suixinkan-ui-tests-reset-state"
|
||||
|
||||
static func resetIfNeeded(arguments: [String] = ProcessInfo.processInfo.arguments) {
|
||||
guard arguments.contains(resetArgument) else { return }
|
||||
|
||||
try? SessionTokenStore().clear()
|
||||
AccountSnapshotStore().clear()
|
||||
let preferences = AppPreferencesStore()
|
||||
preferences.clear()
|
||||
}
|
||||
}
|
||||
31
suixinkan/App/Navigation/NavigationCompatibility.swift
Normal file
31
suixinkan/App/Navigation/NavigationCompatibility.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// NavigationCompatibility.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/// iOS 16 compatible replacement for iOS 17's navigationDestination(item:).
|
||||
func appNavigationDestination<Item, Destination: View>(
|
||||
item: Binding<Item?>,
|
||||
@ViewBuilder destination: @escaping (Item) -> Destination
|
||||
) -> some View {
|
||||
navigationDestination(
|
||||
isPresented: Binding(
|
||||
get: { item.wrappedValue != nil },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
item.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
if let value = item.wrappedValue {
|
||||
destination(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
// Created by Codex on 2026/6/18.
|
||||
//
|
||||
|
||||
import Observation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// 应用内可导航目标,承载各 Tab 内部的路由目的地。
|
||||
@ -60,10 +60,9 @@ extension OrdersRoute {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 单个 NavigationStack 的路径容器,用于保存某个 Tab 的导航历史。
|
||||
final class RouterPath {
|
||||
var path: [AppRoute] = []
|
||||
final class RouterPath: ObservableObject {
|
||||
@Published var path: [AppRoute] = []
|
||||
|
||||
/// 将指定路由压入当前 Tab 的导航栈。
|
||||
func navigate(to route: AppRoute) {
|
||||
@ -77,13 +76,12 @@ final class RouterPath {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 主导航状态中心,管理当前 Tab 和每个 Tab 独立的 NavigationStack 路径。
|
||||
final class AppRouter {
|
||||
var selectedTab: AppTab = .home
|
||||
var selectedOrdersEntry: OrdersEntry = .storeOrders
|
||||
private(set) var pendingOrderScanCode: String?
|
||||
private var routers: [AppTab: RouterPath] = [:]
|
||||
final class AppRouter: ObservableObject {
|
||||
@Published var selectedTab: AppTab = .home
|
||||
@Published var selectedOrdersEntry: OrdersEntry = .storeOrders
|
||||
@Published private(set) var pendingOrderScanCode: String?
|
||||
@Published private var routers: [AppTab: RouterPath] = [:]
|
||||
|
||||
/// 获取指定 Tab 对应的路由路径容器,不存在时自动创建。
|
||||
func router(for tab: AppTab) -> RouterPath {
|
||||
|
||||
@ -10,12 +10,12 @@ import SwiftUI
|
||||
/// App 根视图,负责创建全局依赖并根据登录状态切换登录页和主界面。
|
||||
struct RootView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var appSession = AppSession()
|
||||
@State private var accountContext = AccountContext()
|
||||
@State private var permissionContext = PermissionContext()
|
||||
@State private var scenicSpotContext = ScenicSpotContext()
|
||||
@State private var appRouter = AppRouter()
|
||||
@State private var toastCenter = ToastCenter()
|
||||
@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
|
||||
@ -33,7 +33,7 @@ struct RootView: View {
|
||||
@State private var scenicSettlementAPI: ScenicSettlementAPI
|
||||
@State private var messageCenterAPI: MessageCenterAPI
|
||||
@State private var scenicQueueAPI: ScenicQueueAPI
|
||||
@State private var scenicQueueRuntime = ScenicQueueRuntime()
|
||||
@StateObject private var scenicQueueRuntime = ScenicQueueRuntime()
|
||||
@State private var liveAPI: LiveAPI
|
||||
@State private var operatingAreaAPI: OperatingAreaAPI
|
||||
@State private var pilotCertificationAPI: PilotCertificationAPI
|
||||
@ -98,41 +98,41 @@ struct RootView: View {
|
||||
rootContent
|
||||
.globalToastOverlay()
|
||||
.globalLoadingOverlay(loadingCenter: globalLoading)
|
||||
.environment(appSession)
|
||||
.environment(accountContext)
|
||||
.environment(permissionContext)
|
||||
.environment(scenicSpotContext)
|
||||
.environment(appRouter)
|
||||
.environment(toastCenter)
|
||||
.environmentObject(appSession)
|
||||
.environmentObject(accountContext)
|
||||
.environmentObject(permissionContext)
|
||||
.environmentObject(scenicSpotContext)
|
||||
.environmentObject(appRouter)
|
||||
.environmentObject(toastCenter)
|
||||
.environment(\.globalLoading, globalLoading)
|
||||
.environment(snapshotStore)
|
||||
.environment(apiClient)
|
||||
.environment(authAPI)
|
||||
.environment(profileAPI)
|
||||
.environment(uploadAPI)
|
||||
.environment(ossUploadService)
|
||||
.environment(accountContextAPI)
|
||||
.environment(ordersAPI)
|
||||
.environment(statisticsAPI)
|
||||
.environment(paymentAPI)
|
||||
.environment(walletAPI)
|
||||
.environment(pushAPI)
|
||||
.environment(scenicPermissionAPI)
|
||||
.environment(scenicSettlementAPI)
|
||||
.environment(messageCenterAPI)
|
||||
.environment(scenicQueueAPI)
|
||||
.environment(scenicQueueRuntime)
|
||||
.environment(liveAPI)
|
||||
.environment(operatingAreaAPI)
|
||||
.environment(pilotCertificationAPI)
|
||||
.environment(taskAPI)
|
||||
.environment(projectAPI)
|
||||
.environment(scheduleAPI)
|
||||
.environment(inviteAPI)
|
||||
.environment(assetsAPI)
|
||||
.environment(punchPointAPI)
|
||||
.environment(locationReportAPI)
|
||||
.environment(authSessionCoordinator)
|
||||
.environment(\.accountSnapshotStore, snapshotStore)
|
||||
.environment(\.apiClient, apiClient)
|
||||
.environment(\.authAPI, authAPI)
|
||||
.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)
|
||||
.environment(\.authSessionCoordinator, authSessionCoordinator)
|
||||
.task {
|
||||
apiClient.bindAuthTokenProvider { appSession.token }
|
||||
PushNotificationManager.shared.configure(api: pushAPI, session: appSession, router: appRouter)
|
||||
@ -160,7 +160,7 @@ struct RootView: View {
|
||||
api: accountContextAPI
|
||||
)
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newPhase in
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if appSession.isLoggedIn {
|
||||
scenicQueueRuntime.update(
|
||||
api: scenicQueueAPI,
|
||||
@ -172,7 +172,7 @@ struct RootView: View {
|
||||
scenicQueueRuntime.stop()
|
||||
}
|
||||
}
|
||||
.onChange(of: accountContext.currentScenic?.id) { _, _ in
|
||||
.onChange(of: accountContext.currentScenic?.id) { _ in
|
||||
if appSession.isLoggedIn {
|
||||
scenicQueueRuntime.update(
|
||||
api: scenicQueueAPI,
|
||||
@ -182,7 +182,7 @@ struct RootView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.onChange(of: appSession.phase) { _, phase in
|
||||
.onChange(of: appSession.phase) { phase in
|
||||
switch phase {
|
||||
case .loggedIn:
|
||||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
// Created by Codex on 2026/6/18.
|
||||
//
|
||||
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 当前账号的基础资料实体,用于跨页面展示昵称、手机号和头像。
|
||||
struct AccountProfile: Codable, Equatable {
|
||||
@ -58,14 +58,13 @@ struct BusinessScope: Codable, Equatable, Identifiable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 账号上下文状态中心,保存当前账号资料和景区/门店作用域。
|
||||
final class AccountContext {
|
||||
private(set) var profile: AccountProfile?
|
||||
private(set) var scenicScopes: [BusinessScope] = []
|
||||
private(set) var storeScopes: [BusinessScope] = []
|
||||
var currentScenic: BusinessScope?
|
||||
var currentStore: BusinessScope?
|
||||
final class AccountContext: ObservableObject {
|
||||
@Published private(set) var profile: AccountProfile?
|
||||
@Published private(set) var scenicScopes: [BusinessScope] = []
|
||||
@Published private(set) var storeScopes: [BusinessScope] = []
|
||||
@Published var currentScenic: BusinessScope?
|
||||
@Published var currentStore: BusinessScope?
|
||||
|
||||
/// 应用登录成功后写入账号资料。
|
||||
func applyLogin(profile: AccountProfile? = nil) {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 登录阶段实体,表示根视图当前应该展示的认证状态。
|
||||
enum AuthPhase: Equatable {
|
||||
@ -16,11 +16,10 @@ enum AuthPhase: Equatable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 登录会话状态中心,保存 token 和当前认证阶段。
|
||||
final class AppSession {
|
||||
private(set) var phase: AuthPhase = .loggedOut
|
||||
private(set) var token: String?
|
||||
final class AppSession: ObservableObject {
|
||||
@Published private(set) var phase: AuthPhase = .loggedOut
|
||||
@Published private(set) var token: String?
|
||||
|
||||
var isLoggedIn: Bool {
|
||||
phase == .loggedIn
|
||||
|
||||
@ -6,15 +6,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 登录会话协调器,统一处理登录完成、退出登录和缓存同步。
|
||||
final class AuthSessionCoordinator {
|
||||
@ObservationIgnored private let tokenStore: SessionTokenStore
|
||||
@ObservationIgnored private let snapshotStore: AccountSnapshotStore
|
||||
@ObservationIgnored private let preferencesStore: AppPreferencesStore
|
||||
private let tokenStore: SessionTokenStore
|
||||
private let snapshotStore: AccountSnapshotStore
|
||||
private let preferencesStore: AppPreferencesStore
|
||||
|
||||
/// 初始化登录会话协调器,并注入 token、账号快照和偏好存储。
|
||||
init(
|
||||
|
||||
@ -6,15 +6,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 权限上下文状态中心,保存角色权限、当前角色和扁平化权限 URI。
|
||||
final class PermissionContext {
|
||||
private(set) var rolePermissions: [RolePermissionResponse] = []
|
||||
private(set) var permissionURIs: Set<String> = []
|
||||
var currentRole: RoleInfo?
|
||||
final class PermissionContext: ObservableObject {
|
||||
@Published private(set) var rolePermissions: [RolePermissionResponse] = []
|
||||
@Published private(set) var permissionURIs: Set<String> = []
|
||||
@Published var currentRole: RoleInfo?
|
||||
|
||||
/// 替换角色权限列表,并尽量按缓存角色 ID 保持当前角色。
|
||||
func replaceRolePermissions(_ rolePermissions: [RolePermissionResponse], currentRoleId: Int? = nil) {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 景点加载阶段实体,表示当前景区景点列表的加载状态。
|
||||
enum ScenicSpotLoadState: Equatable {
|
||||
@ -17,12 +17,11 @@ enum ScenicSpotLoadState: Equatable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 景点上下文状态中心,保存当前景区下的景点或打卡点列表。
|
||||
final class ScenicSpotContext {
|
||||
private(set) var scenicId: Int?
|
||||
private(set) var spots: [ScenicSpotItem] = []
|
||||
private(set) var loadState: ScenicSpotLoadState = .idle
|
||||
final class ScenicSpotContext: ObservableObject {
|
||||
@Published private(set) var scenicId: Int?
|
||||
@Published private(set) var spots: [ScenicSpotItem] = []
|
||||
@Published private(set) var loadState: ScenicSpotLoadState = .idle
|
||||
|
||||
/// 按景区 ID 重新加载景点列表,失败只影响景点模块本身。
|
||||
func reload(scenicId: Int?, api: AccountContextServing) async {
|
||||
|
||||
@ -6,14 +6,13 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 登录态启动恢复器,负责冷启动时从缓存恢复并校验 token。
|
||||
final class SessionBootstrapper {
|
||||
@ObservationIgnored private let tokenStore: SessionTokenStore
|
||||
@ObservationIgnored private let snapshotStore: AccountSnapshotStore
|
||||
private let tokenStore: SessionTokenStore
|
||||
private let snapshotStore: AccountSnapshotStore
|
||||
private var didAttemptRestore = false
|
||||
|
||||
/// 初始化启动恢复器,并注入 token 与账号快照存储。
|
||||
|
||||
@ -5,17 +5,16 @@
|
||||
// Created by Codex on 2026/6/18.
|
||||
//
|
||||
|
||||
import Observation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 全局 Toast 状态中心,负责保存和清除当前提示文案。
|
||||
final class ToastCenter {
|
||||
fileprivate var message: String?
|
||||
@ObservationIgnored private let autoDismissNanoseconds: UInt64
|
||||
@ObservationIgnored private var dismissTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var displayToken: UInt64 = 0
|
||||
final class ToastCenter: ObservableObject {
|
||||
@Published fileprivate var message: String?
|
||||
private let autoDismissNanoseconds: UInt64
|
||||
@Published private var dismissTask: Task<Void, Never>?
|
||||
@Published private var displayToken: UInt64 = 0
|
||||
|
||||
/// 创建 Toast 状态中心,默认 2.2 秒后自动隐藏。
|
||||
init(autoDismissNanoseconds: UInt64 = 2_200_000_000) {
|
||||
@ -65,7 +64,7 @@ final class ToastCenter {
|
||||
|
||||
/// 全局 Toast 叠加层修饰器,负责把 Toast 展示到页面顶部。
|
||||
private struct GlobalToastOverlay: ViewModifier {
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
|
||||
@ -12,6 +12,10 @@ import SwiftUI
|
||||
struct suixinkanApp: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
init() {
|
||||
AppUITestLaunchState.resetIfNeeded()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
|
||||
Reference in New Issue
Block a user