集成 MJRefresh 并重构主 TabBar 为系统 TabView 样式。
新增 SwiftUI 桥接层与单元测试,更新 Tab 图标资源命名和多倍图,同步调整 UI Test 的 Tab/扫码定位逻辑。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user