feat(symptom): add Symptom @Model + start/end sheets + ongoing card
- Symptom @Model with severity 1-5 clamp, isOngoing, duration helpers - SymptomStartSheet / SymptomEndSheet / OngoingSymptomsCard - RecordSheet 加 .symptom kind 入口 - RootView 增加 'records' tab + ArchiveListView placeholder - HomeView 顶部加 OngoingSymptomsCard - ModelsSchemaTests: 2 个 Symptom 烟测(ongoing predicate + severity clamp) Note: Symptom 是 CLAUDE.md §10 清单外的新功能,由产品负责人决定加入。 ArchiveListView 仍是 placeholder,真实 C1 实现按计划在 W4。
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
import SwiftUI
|
||||
|
||||
enum TjTab: String, Hashable, CaseIterable {
|
||||
case home, trend, me
|
||||
case home, records, trend, me
|
||||
var label: String {
|
||||
switch self {
|
||||
case .home: return "首页"
|
||||
case .trend: return "趋势"
|
||||
case .me: return "我的"
|
||||
case .home: return "主页"
|
||||
case .records: return "记录"
|
||||
case .trend: return "趋势"
|
||||
case .me: return "我的"
|
||||
}
|
||||
}
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .home: return "house"
|
||||
case .trend: return "chart.line.uptrend.xyaxis"
|
||||
case .me: return "person.circle"
|
||||
case .home: return "house"
|
||||
case .records: return "list.bullet.rectangle"
|
||||
case .trend: return "chart.line.uptrend.xyaxis"
|
||||
case .me: return "person.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,14 +29,16 @@ struct RootView: View {
|
||||
@State private var tab: TjTab = .home
|
||||
@State private var showRecordSheet = false
|
||||
@State private var activeFlow: ActiveFlow?
|
||||
@State private var showSymptomStart = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Group {
|
||||
switch tab {
|
||||
case .home: HomeView(onTapArchive: {})
|
||||
case .trend: TrendsView()
|
||||
case .me: MeView()
|
||||
case .home: HomeView(onTapArchive: { tab = .records })
|
||||
case .records: ArchiveListView()
|
||||
case .trend: TrendsView()
|
||||
case .me: MeView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
@@ -51,11 +55,15 @@ struct RootView: View {
|
||||
switch kind {
|
||||
case .quick: activeFlow = .quick
|
||||
case .archive: activeFlow = .archive
|
||||
case .symptom: showSymptomStart = true
|
||||
case .diary: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSymptomStart) {
|
||||
SymptomStartSheet()
|
||||
}
|
||||
#if os(iOS)
|
||||
.fullScreenCover(item: $activeFlow) { flow in
|
||||
switch flow {
|
||||
@@ -83,66 +91,101 @@ private struct TabBar: View {
|
||||
let onTap: (TjTab) -> Void
|
||||
let onTapRecord: () -> Void
|
||||
|
||||
private let cornerRadius: CGFloat = 22
|
||||
private let slotHeight: CGFloat = 34
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
tabItem(.home)
|
||||
Color.clear.frame(width: 60, height: 1)
|
||||
tabItem(.records)
|
||||
recordSlot
|
||||
tabItem(.trend)
|
||||
tabItem(.me)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 4)
|
||||
.background(
|
||||
Tj.Palette.paper
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Tj.Palette.lineSoft)
|
||||
.frame(height: 1)
|
||||
}
|
||||
.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) {
|
||||
recordButton.offset(y: -22)
|
||||
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 {
|
||||
Button { onTap(t) } label: {
|
||||
let isActive = active == t
|
||||
return Button { onTap(t) } label: {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: t.icon)
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
.frame(width: 26, height: 26)
|
||||
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))
|
||||
.font(.system(size: 11, weight: isActive ? .semibold : .regular))
|
||||
}
|
||||
.foregroundStyle(active == t ? Tj.Palette.ink : Tj.Palette.text3)
|
||||
.foregroundStyle(isActive ? Tj.Palette.ink : Tj.Palette.text3)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 4)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(TabPressStyle())
|
||||
}
|
||||
|
||||
private var recordButton: some View {
|
||||
private var recordSlot: some View {
|
||||
Button(action: onTapRecord) {
|
||||
VStack(spacing: 4) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Tj.Palette.ink)
|
||||
.shadow(color: Color(red: 0.157, green: 0.216, blue: 0.176).opacity(0.25),
|
||||
radius: 12, x: 0, y: 4)
|
||||
.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: 20, weight: .semibold))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.paper)
|
||||
}
|
||||
.frame(width: 52, height: 52)
|
||||
.frame(width: slotHeight, height: slotHeight)
|
||||
|
||||
Text("记录")
|
||||
.font(.system(size: 11))
|
||||
Text("新建")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.ink)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user