// // MainTabsView.swift // suixinkan // // Created by Codex on 2026/6/18. // import SwiftUI /// 主 Tab 容器视图,使用系统 TabView 承载登录后的一级入口。 struct MainTabsView: View { private static let leadingTabs: [AppTab] = [.home, .orders] private static let trailingTabs: [AppTab] = [.statistics, .profile] @EnvironmentObject private var accountContext: AccountContext @EnvironmentObject private var appRouter: AppRouter @Environment(\.ordersAPI) private var ordersAPI @EnvironmentObject private var toastCenter: ToastCenter @StateObject private var badgeViewModel = MainTabBadgeViewModel() @State private var showScanner = false @State private var tabSelection: MainTabSelection = .tab(.home) @State private var selectedBusinessTab: AppTab = .home var body: some View { TabView(selection: $tabSelection) { ForEach(Self.leadingTabs) { tab in TabNavigationStackHost(tab: tab) .tabItem { tabItem(for: tab) } .tag(MainTabSelection.tab(tab)) .badge(tab == .orders ? badgeViewModel.pendingWriteOffCount ?? 0 : 0) } ScannerTabPlaceholder() .accessibilityIdentifier("main.scan") .tabItem { Image("icon_scan") .accessibilityIdentifier("main.scan") .accessibilityLabel("扫码核销") } .tag(MainTabSelection.scanner) ForEach(Self.trailingTabs) { tab in TabNavigationStackHost(tab: tab) .tabItem { tabItem(for: tab) } .tag(MainTabSelection.tab(tab)) .badge(tab == .orders ? badgeViewModel.pendingWriteOffCount ?? 0 : 0) } } .fullScreenCover(isPresented: $showScanner) { OrderScannerPage( onClose: { showScanner = false }, onSuccess: { rawCode in showScanner = false appRouter.routeToOrderVerification(scannedCode: rawCode) }, onFailure: { error in showScanner = false toastCenter.show(error.localizedDescription) } ) } .onAppear { syncSelection(with: appRouter.selectedTab) } .onChange(of: tabSelection) { selection in handleTabSelection(selection) } .task { await refreshOrderBadge() } .onChange(of: appRouter.selectedTab) { selectedTab in syncSelection(with: selectedTab) guard selectedTab == .orders else { return } Task { await refreshOrderBadge() } } .onChange(of: accountContext.currentScenic?.id) { _ in Task { await refreshOrderBadge() } } .onChange(of: accountContext.currentStore?.id) { _ in Task { await refreshOrderBadge() } } } /// 刷新订单 Tab 待核销角标数量。 private func refreshOrderBadge() async { await badgeViewModel.refreshPendingWriteOffCount( api: ordersAPI, scenicId: accountContext.currentScenic?.id, storeId: accountContext.currentStore?.id ) } /// 处理系统 TabBar 选择变化,扫码入口只弹出页面并立即恢复之前的业务 Tab 选中态。 private func handleTabSelection(_ selection: MainTabSelection) { switch selection { case .tab(let tab): selectedBusinessTab = tab guard appRouter.selectedTab != tab else { return } appRouter.select(tab) case .scanner: showScanner = true restoreBusinessTabSelection() } } /// 同步外部路由切换到系统 TabView 的可见选中态。 private func syncSelection(with tab: AppTab) { selectedBusinessTab = tab guard tabSelection != .tab(tab) else { return } tabSelection = .tab(tab) } /// 在下一轮主线程恢复选中态,避免系统 TabBar 在扫码页关闭后仍高亮扫码入口。 private func restoreBusinessTabSelection() { let tab = selectedBusinessTab DispatchQueue.main.async { tabSelection = .tab(tab) } } /// 构建普通业务 Tab 的图标与标题。 @ViewBuilder private func tabItem(for tab: AppTab) -> some View { Image(appRouter.selectedTab == tab ? tab.selectedImageName : tab.unselectedImageName) .renderingMode(.original) Text(tab.title) } } /// 主 TabView 内部选择项,扫码只用于拦截点击并不进入 AppTab 业务导航。 private enum MainTabSelection: Hashable { case tab(AppTab) case scanner } /// 单个业务 Tab 对应的系统导航栈。 private struct TabNavigationStackHost: View { @EnvironmentObject private var appRouter: AppRouter let tab: AppTab var body: some View { TabNavigationStackContent(tab: tab, router: appRouter.router(for: tab)) } } /// 直接观察单个 Tab 的路由路径,确保子页面修改 RouterPath 后导航栈能立即响应。 private struct TabNavigationStackContent: View { let tab: AppTab @ObservedObject var router: RouterPath var body: some View { NavigationStack(path: $router.path) { tab.rootView .navigationTitle(tab.title) .navigationDestination(for: AppRoute.self) { route in route.destinationView .toolbar(route.hidesTabBarWhenPushed ? .hidden : .visible, for: .tabBar) } } .environmentObject(router) } } /// 扫码 Tab 占位内容,实际点击会被 TabView selection 绑定拦截并展示扫码页。 private struct ScannerTabPlaceholder: View { var body: some View { Color.clear } } #Preview { MainTabsView() .environmentObject(AppRouter()) .environmentObject(AccountContext()) .environment(\.ordersAPI, OrdersAPI(client: APIClient())) .environmentObject(ToastCenter()) }