Files
suixinkan_ios_new/suixinkan/Features/Main/Views/MainTabsView.swift
汉秋 5541a0b106 新增自定义主 TabBar 与全局扫码核销流程
引入可配置的自定义 TabBar(含订单角标)、中央扫码入口跳转核销订单页,并更新迁移清单中最近完成的模块。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 10:52:45 +08:00

205 lines
6.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MainTabsView.swift
// suixinkan
//
// Created by Codex on 2026/6/18.
//
import SwiftUI
/// TabBar 使 TabView
enum MainTabBarStyle {
case system
case custom
}
/// TabBar TabView activeStyle
enum MainTabBarConfiguration {
static let activeStyle: MainTabBarStyle = .custom
}
/// Tab TabView TabBar
struct MainTabsView: View {
var body: some View {
switch MainTabBarConfiguration.activeStyle {
case .system:
SystemMainTabsView()
case .custom:
CustomMainTabsView()
}
}
}
/// TabView TabBar 便
private struct SystemMainTabsView: View {
@Environment(AppRouter.self) private var appRouter
var body: some View {
@Bindable var appRouter = appRouter
TabView(selection: $appRouter.selectedTab) {
ForEach(AppTab.allCases) { tab in
TabNavigationStackHost(tab: tab)
.tabItem { tab.label }
.tag(tab)
}
}
}
}
/// TabBar Tab NavigationStack 访
private struct CustomMainTabsView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(AppRouter.self) private var appRouter
@Environment(OrdersAPI.self) private var ordersAPI
@Environment(ToastCenter.self) private var toastCenter
@State private var badgeViewModel = MainTabBadgeViewModel()
@State private var showScanner = false
@State private var loadedTabs: Set<AppTab> = [.home]
private var orderBadgeText: String? {
guard let count = badgeViewModel.pendingWriteOffCount, count > 0 else {
return nil
}
return "\(count)"
}
var body: some View {
@Bindable var appRouter = appRouter
ZStack {
ForEach(AppTab.allCases) { tab in
PreservedTabPage(
tab: tab,
selectedTab: appRouter.selectedTab,
isLoaded: loadedTabs.contains(tab)
) {
CustomTabNavigationStackHost(
tab: tab,
selectedTab: $appRouter.selectedTab,
orderBadgeText: orderBadgeText,
onScanTap: { showScanner = true }
)
}
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.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)
}
)
}
.task {
loadedTabs.insert(appRouter.selectedTab)
await refreshOrderBadge()
}
.onChange(of: appRouter.selectedTab) { _, selectedTab in
loadedTabs.insert(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 TabBar
private struct CustomTabNavigationStackHost: View {
@Environment(AppRouter.self) private var appRouter
let tab: AppTab
@Binding var selectedTab: AppTab
let orderBadgeText: String?
let onScanTap: () -> Void
var body: some View {
NavigationStack(path: appRouter.binding(for: tab)) {
VStack(spacing: 0) {
tab.rootView
.frame(maxWidth: .infinity, maxHeight: .infinity)
CustomMainTabBar(
selectedTab: $selectedTab,
orderBadgeText: orderBadgeText,
onScanTap: onScanTap
)
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle(tab.title)
.navigationDestination(for: AppRoute.self) { route in
route.destinationView
.toolbar(route.hidesTabBarWhenPushed ? .hidden : .visible, for: .tabBar)
}
}
.environment(appRouter.router(for: tab))
}
}
/// Tab TabBar
private struct TabNavigationStackHost: View {
@Environment(AppRouter.self) private var appRouter
let tab: AppTab
var body: some View {
NavigationStack(path: appRouter.binding(for: tab)) {
tab.rootView
.navigationTitle(tab.title)
.navigationDestination(for: AppRoute.self) { route in
route.destinationView
.toolbar(route.hidesTabBarWhenPushed ? .hidden : .visible, for: .tabBar)
}
}
.environment(appRouter.router(for: tab))
}
}
/// TabBar Tab
private struct PreservedTabPage<Content: View>: View {
let tab: AppTab
let selectedTab: AppTab
let isLoaded: Bool
@ViewBuilder let content: () -> Content
var body: some View {
if isLoaded {
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.opacity(selectedTab == tab ? 1 : 0)
.zIndex(selectedTab == tab ? 1 : 0)
.allowsHitTesting(selectedTab == tab)
.accessibilityHidden(selectedTab != tab)
}
}
}
#Preview {
MainTabsView()
.environment(AppRouter())
.environment(AccountContext())
.environment(OrdersAPI(client: APIClient()))
.environment(ToastCenter())
}