集成 MJRefresh 并重构主 TabBar 为系统 TabView 样式。

新增 SwiftUI 桥接层与单元测试,更新 Tab 图标资源命名和多倍图,同步调整 UI Test 的 Tab/扫码定位逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 10:22:53 +08:00
parent 79e735f628
commit 8997d1ba1c
140 changed files with 6574 additions and 877 deletions

View File

@ -7,46 +7,11 @@
import SwiftUI
/// TabBar 使 TabView
enum MainTabBarStyle {
case system
case custom
}
/// TabBar TabView activeStyle
enum MainTabBarConfiguration {
static let activeStyle: MainTabBarStyle = .custom
}
/// Tab TabView TabBar
/// Tab 使 TabView
struct MainTabsView: View {
var body: some View {
switch MainTabBarConfiguration.activeStyle {
case .system:
SystemMainTabsView()
case .custom:
CustomMainTabsView()
}
}
}
private static let leadingTabs: [AppTab] = [.home, .orders]
private static let trailingTabs: [AppTab] = [.statistics, .profile]
/// TabView TabBar 便
private struct SystemMainTabsView: View {
@EnvironmentObject private var appRouter: AppRouter
var body: some View {
TabView(selection: $appRouter.selectedTab) {
ForEach(AppTab.allCases) { tab in
TabNavigationStackHost(tab: tab)
.tabItem { tab.label }
.tag(tab)
}
}
}
}
/// TabBar Tab NavigationStack 访
private struct CustomMainTabsView: View {
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var appRouter: AppRouter
@Environment(\.ordersAPI) private var ordersAPI
@ -54,33 +19,34 @@ private struct CustomMainTabsView: View {
@StateObject 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)"
}
@State private var tabSelection: MainTabSelection = .tab(.home)
@State private var selectedBusinessTab: AppTab = .home
var body: some View {
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 }
)
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)
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.fullScreenCover(isPresented: $showScanner) {
OrderScannerPage(
onClose: { showScanner = false },
@ -94,12 +60,17 @@ private struct CustomMainTabsView: View {
}
)
}
.onAppear {
syncSelection(with: appRouter.selectedTab)
}
.onChange(of: tabSelection) { selection in
handleTabSelection(selection)
}
.task {
loadedTabs.insert(appRouter.selectedTab)
await refreshOrderBadge()
}
.onChange(of: appRouter.selectedTab) { selectedTab in
loadedTabs.insert(selectedTab)
syncSelection(with: selectedTab)
guard selectedTab == .orders else { return }
Task { await refreshOrderBadge() }
}
@ -119,41 +90,51 @@ private struct CustomMainTabsView: View {
storeId: accountContext.currentStore?.id
)
}
}
/// TabBar Tab TabBar
private struct CustomTabNavigationStackHost: View {
@EnvironmentObject private var appRouter: 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)
}
/// 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()
}
.environmentObject(appRouter.router(for: tab))
}
/// 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)
}
}
/// Tab TabBar
/// TabView AppTab
private enum MainTabSelection: Hashable {
case tab(AppTab)
case scanner
}
/// Tab
private struct TabNavigationStackHost: View {
@EnvironmentObject private var appRouter: AppRouter
@ -172,22 +153,10 @@ private struct TabNavigationStackHost: View {
}
}
/// TabBar Tab
private struct PreservedTabPage<Content: View>: View {
let tab: AppTab
let selectedTab: AppTab
let isLoaded: Bool
@ViewBuilder let content: () -> Content
/// Tab TabView selection
private struct ScannerTabPlaceholder: View {
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)
}
Color.clear
}
}