import SwiftUI enum TjTab: String, Hashable, CaseIterable { case home, records, trend, me var label: String { switch self { case .home: return "主页" case .records: return "记录" case .trend: return "趋势" case .me: return "我的" } } var icon: String { switch self { case .home: return "house" case .records: return "list.bullet.rectangle" case .trend: return "chart.line.uptrend.xyaxis" case .me: return "person.circle" } } } enum ActiveFlow: Identifiable { case quick, archive var id: String { String(describing: self) } } struct RootView: View { @State private var tab: TjTab = .home @State private var showRecordSheet = false @State private var activeFlow: ActiveFlow? @State private var showSymptomStart = false @State private var showDiary = false var body: some View { VStack(spacing: 0) { Group { switch tab { case .home: HomeView(onTapArchive: { tab = .records }) case .records: ArchiveListView() case .trend: TrendsView() case .me: MeView() } } .frame(maxWidth: .infinity, maxHeight: .infinity) TabBar(active: tab, onTap: { tab = $0 }, onTapRecord: { showRecordSheet = true }) } .background(Tj.Palette.sand.ignoresSafeArea()) .sheet(isPresented: $showRecordSheet) { RecordSheet { kind in showRecordSheet = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { switch kind { case .quick: activeFlow = .quick case .archive: activeFlow = .archive case .symptom: showSymptomStart = true case .diary: showDiary = true } } } } .sheet(isPresented: $showSymptomStart) { SymptomStartSheet() } .sheet(isPresented: $showDiary) { DiaryQuickSheet() } #if os(iOS) .fullScreenCover(item: $activeFlow) { flow in switch flow { case .quick: QuickCaptureFlow(onClose: { activeFlow = nil }) case .archive: ArchiveFlow(onClose: { activeFlow = nil }) } } #else .sheet(item: $activeFlow) { flow in switch flow { case .quick: QuickCaptureFlow(onClose: { activeFlow = nil }) case .archive: ArchiveFlow(onClose: { activeFlow = nil }) } } #endif } } private struct TabBar: View { let active: TjTab let onTap: (TjTab) -> Void let onTapRecord: () -> Void private let cornerRadius: CGFloat = 22 private let slotHeight: CGFloat = 34 var body: some View { HStack(alignment: .bottom, spacing: 0) { tabItem(.home) tabItem(.records) recordSlot tabItem(.trend) tabItem(.me) } .padding(.horizontal, 4) .padding(.top, 10) .padding(.bottom, 6) .background(barBackground) } private var barBackground: some View { UnevenRoundedRectangle( topLeadingRadius: cornerRadius, bottomLeadingRadius: 0, bottomTrailingRadius: 0, topTrailingRadius: cornerRadius, style: .continuous ) .fill(Tj.Palette.paper) .overlay(alignment: .top) { Rectangle() .fill(Tj.Palette.lineSoft) .frame(height: 1) } .shadow(color: Tj.Palette.ink.opacity(0.05), radius: 10, x: 0, y: -2) } private func tabItem(_ t: TjTab) -> some View { let isActive = active == t return Button { onTap(t) } label: { VStack(spacing: 4) { ZStack { if isActive { Capsule() .fill(Tj.Palette.sand2) .frame(width: 44, height: slotHeight - 6) } Image(systemName: t.icon) .font(.system(size: 18, weight: isActive ? .semibold : .regular)) } .frame(width: 50, height: slotHeight) Text(t.label) .font(.system(size: 11, weight: isActive ? .semibold : .regular)) } .foregroundStyle(isActive ? Tj.Palette.ink : Tj.Palette.text3) .frame(maxWidth: .infinity) .contentShape(Rectangle()) } .buttonStyle(TabPressStyle()) } private var recordSlot: some View { Button(action: onTapRecord) { VStack(spacing: 4) { ZStack { Circle() .fill(Tj.Palette.ink) .overlay( Circle() .strokeBorder(Tj.Palette.paper, lineWidth: 2) ) .shadow(color: Tj.Palette.ink.opacity(0.18), radius: 4, x: 0, y: 2) Image(systemName: "plus") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(Tj.Palette.paper) } .frame(width: slotHeight, height: slotHeight) Text("新建") .font(.system(size: 11, weight: .semibold)) .foregroundStyle(Tj.Palette.ink) } .frame(maxWidth: .infinity) .contentShape(Rectangle()) } .buttonStyle(TabPressStyle()) } } private struct TabPressStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.92 : 1.0) .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed) } } #Preview { RootView() }