import SwiftUI import SwiftData /// 计算属性形式:每次取值按当前语言解析,语言切换后即时更新(不可用 static/let 缓存)。 private func symptomPresets() -> [String] { [String(appLoc: "头痛"), String(appLoc: "咳嗽"), String(appLoc: "腹痛"), String(appLoc: "发烧"), String(appLoc: "恶心"), String(appLoc: "失眠"), String(appLoc: "疲劳"), String(appLoc: "关节痛")] } struct SymptomStartSheet: View { @Environment(\.modelContext) private var ctx @Environment(\.dismiss) private var dismiss @State private var name: String = "" @State private var customName: String = "" @State private var startedAt: Date = .now @State private var severity: Double = 3 @State private var note: String = "" private var resolvedName: String { let trimmed = customName.trimmingCharacters(in: .whitespaces) return trimmed.isEmpty ? name : trimmed } private var canSubmit: Bool { !resolvedName.isEmpty } var body: some View { VStack(spacing: 0) { handle header ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 18) { presetSection customSection timeSection severitySection noteSection } .padding(.horizontal, 20) .padding(.bottom, 20) } footer } .background( Tj.Palette.sand .clipShape(RoundedRectangle(cornerRadius: Tj.Radius.xl, style: .continuous)) .ignoresSafeArea(edges: .bottom) ) .presentationDetents([.large]) .presentationDragIndicator(.hidden) .presentationBackground(Tj.Palette.sand) .presentationCornerRadius(Tj.Radius.xl) } private var handle: some View { Capsule() .fill(Tj.Palette.line) .frame(width: 40, height: 4) .padding(.top, 10) .padding(.bottom, 14) } private var header: some View { HStack { Text("症状开始") .font(.tjH2()) .foregroundStyle(Tj.Palette.text) Spacer() Text("结束时再来点结束") .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) } .padding(.horizontal, 20) .padding(.bottom, 16) } private var presetSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel(String(appLoc: "常见症状")) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(symptomPresets(), id: \.self) { item in chip(item, selected: name == item) { name = item customName = "" } } } } } } private var customSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel(String(appLoc: "或者自己写")) TextField("例如:眼皮跳", text: $customName) .textInputAutocapitalization(.never) .padding(.horizontal, 14) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous) .fill(Tj.Palette.paper) ) .overlay( RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous) .strokeBorder(Tj.Palette.line, lineWidth: 1) ) .onChange(of: customName) { _, newValue in if !newValue.trimmingCharacters(in: .whitespaces).isEmpty { name = "" } } } } private var timeSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel(String(appLoc: "开始时间")) DatePicker("", selection: $startedAt, in: ...Date.now) .datePickerStyle(.compact) .labelsHidden() } } private var severitySection: some View { VStack(alignment: .leading, spacing: 8) { HStack { sectionLabel(String(appLoc: "强度")) Spacer() Text("\(Int(severity)) / 5") .font(.system(size: 13, weight: .semibold, design: .monospaced)) .foregroundStyle(severityColor) } Slider(value: $severity, in: 1...5, step: 1) .tint(severityColor) HStack { Text("轻微").font(.system(size: 11)).foregroundStyle(Tj.Palette.text3) Spacer() Text("剧烈").font(.system(size: 11)).foregroundStyle(Tj.Palette.text3) } } } private var noteSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel(String(appLoc: "备注(可选)")) TextField("位置、可能诱因…", text: $note, axis: .vertical) .lineLimit(2...4) .padding(.horizontal, 14) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous) .fill(Tj.Palette.paper) ) .overlay( RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous) .strokeBorder(Tj.Palette.line, lineWidth: 1) ) } } private var footer: some View { HStack(spacing: 12) { Button("取消") { dismiss() } .buttonStyle(TjGhostButton(height: 44, fontSize: 15, horizontalPadding: 18)) Button("开始记录") { submit() } .buttonStyle(TjPrimaryButton(height: 44, fontSize: 15, horizontalPadding: 18)) .disabled(!canSubmit) .opacity(canSubmit ? 1 : 0.4) } .padding(.horizontal, 20) .padding(.vertical, 14) .background( Tj.Palette.sand .overlay(alignment: .top) { Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1) } ) } private var severityColor: Color { switch Int(severity) { case 1, 2: return Tj.Palette.leaf case 3: return Tj.Palette.amber default: return Tj.Palette.brick } } private func sectionLabel(_ text: String) -> some View { Text(text) .font(.system(size: 12, weight: .semibold)) .tracking(0.3) .foregroundStyle(Tj.Palette.text2) } private func chip(_ label: String, selected: Bool, action: @escaping () -> Void) -> some View { Button(action: action) { Text(label) .font(.system(size: 13, weight: selected ? .semibold : .regular)) .foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text) .padding(.horizontal, 14) .padding(.vertical, 8) .background( Capsule().fill(selected ? Tj.Palette.ink : Tj.Palette.paper) ) .overlay( Capsule().strokeBorder(Tj.Palette.line, lineWidth: selected ? 0 : 1) ) } .buttonStyle(.plain) } private func submit() { guard canSubmit else { return } let symptom = Symptom( name: resolvedName, startedAt: startedAt, note: note.trimmingCharacters(in: .whitespaces).isEmpty ? nil : note, severity: Int(severity) ) ctx.insert(symptom) try? ctx.save() dismiss() } } #Preview { SymptomStartSheet() .modelContainer(for: Symptom.self, inMemory: true) }