新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
7.2 KiB
Swift
208 lines
7.2 KiB
Swift
//
|
||
// 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 bindAcquirerSaleUserId: Int?
|
||
@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
|
||
handleGlobalScanResult(rawCode)
|
||
},
|
||
onFailure: { error in
|
||
showScanner = false
|
||
toastCenter.show(error.localizedDescription)
|
||
}
|
||
)
|
||
}
|
||
.fullScreenCover(item: bindAcquirerBinding) { item in
|
||
NavigationStack {
|
||
BindAcquirerView(saleUserId: item.saleUserId, onSuccess: {
|
||
bindAcquirerSaleUserId = nil
|
||
})
|
||
}
|
||
}
|
||
.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
|
||
)
|
||
}
|
||
|
||
/// 处理主 Tab 扫码结果,获客员绑定二维码优先弹出绑定页。
|
||
private func handleGlobalScanResult(_ rawCode: String) {
|
||
if SaleUserQrParser.isSaleUserBindQR(rawCode) {
|
||
guard let saleUserId = SaleUserQrParser.parseSaleUserId(rawCode), saleUserId > 0 else {
|
||
toastCenter.show("请扫描正确的获客员二维码")
|
||
return
|
||
}
|
||
bindAcquirerSaleUserId = saleUserId
|
||
return
|
||
}
|
||
appRouter.routeToOrderVerification(scannedCode: rawCode)
|
||
}
|
||
|
||
private var bindAcquirerBinding: Binding<BindAcquirerSheetItem?> {
|
||
Binding(
|
||
get: { bindAcquirerSaleUserId.map(BindAcquirerSheetItem.init(saleUserId:)) },
|
||
set: { bindAcquirerSaleUserId = $0?.saleUserId }
|
||
)
|
||
}
|
||
|
||
/// 处理系统 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())
|
||
}
|