diff --git a/康康/Features/Indicator/IndicatorQuickSheet.swift b/康康/Features/Indicator/IndicatorQuickSheet.swift index d4e6376..c297394 100644 --- a/康康/Features/Indicator/IndicatorQuickSheet.swift +++ b/康康/Features/Indicator/IndicatorQuickSheet.swift @@ -601,14 +601,6 @@ struct IndicatorQuickSheet: View { ) ctx.insert(indicator) try? ctx.save() - - // 身高顺手回写 Profile - if m == .height, let cm = Int(value.trimmingCharacters(in: .whitespaces)), - let p = profile { - p.heightCM = cm - p.updatedAt = .now - try? ctx.save() - } } private func saveFreeform() { diff --git a/康康/Features/Monitor/MonitorMetric.swift b/康康/Features/Monitor/MonitorMetric.swift index 62a0d1e..ca770af 100644 --- a/康康/Features/Monitor/MonitorMetric.swift +++ b/康康/Features/Monitor/MonitorMetric.swift @@ -9,11 +9,11 @@ enum MonitorMetric: String, CaseIterable, Identifiable { case bloodPressure // bp.systolic + bp.diastolic case fastingGlucose // glucose.fasting case postprandialGlucose // glucose.postprandial - case weight // weight case temperature // temperature case heartRate // heart_rate case spo2 // spo2 - case height // height(录入后回写 UserProfile.heightCM) + + // 注:身高 / 体重不在这里——它们是 UserProfile 的字段(单值,不存历史)。 var id: String { rawValue } @@ -22,11 +22,9 @@ enum MonitorMetric: String, CaseIterable, Identifiable { case .bloodPressure: return "血压" case .fastingGlucose: return "空腹血糖" case .postprandialGlucose: return "餐后血糖" - case .weight: return "体重" case .temperature: return "体温" case .heartRate: return "心率" case .spo2: return "血氧" - case .height: return "身高" } } @@ -36,11 +34,9 @@ enum MonitorMetric: String, CaseIterable, Identifiable { case .bloodPressure: return "heart.fill" case .fastingGlucose: return "drop.fill" case .postprandialGlucose: return "drop.circle.fill" - case .weight: return "scalemass.fill" case .temperature: return "thermometer.medium" case .heartRate: return "waveform.path.ecg" case .spo2: return "lungs.fill" - case .height: return "ruler.fill" } } @@ -71,12 +67,6 @@ enum MonitorMetric: String, CaseIterable, Identifiable { unit: "mmol/L", placeholder: "6.5", baseRange: 0...7.8)] - case .weight: - return [Field(seriesKey: "weight", - label: "体重", - unit: "kg", - placeholder: "68", - baseRange: nil)] case .temperature: return [Field(seriesKey: "temperature", label: "体温", @@ -95,12 +85,6 @@ enum MonitorMetric: String, CaseIterable, Identifiable { unit: "%", placeholder: "98", baseRange: 95...100)] - case .height: - return [Field(seriesKey: "height", - label: "身高", - unit: "cm", - placeholder: "175", - baseRange: nil)] } } } diff --git a/康康/Features/Profile/ProfileEditView.swift b/康康/Features/Profile/ProfileEditView.swift index ef3b070..e31a8d7 100644 --- a/康康/Features/Profile/ProfileEditView.swift +++ b/康康/Features/Profile/ProfileEditView.swift @@ -53,11 +53,28 @@ private struct ProfileEditForm: View { // MARK: - 基本 private var basicsSection: some View { - Section("基本") { + Section { birthYearPicker sexPicker heightRow + weightRow bloodTypePicker + } header: { + Text("基本") + } footer: { + if let bmi = profile.bmi { + Text("BMI: \(String(format: "%.1f", bmi)) \(bmiLabel(bmi))") + .font(.system(size: 11)) + } + } + } + + private func bmiLabel(_ bmi: Double) -> String { + switch bmi { + case ..<18.5: return "(偏瘦)" + case ..<24: return "(正常)" + case ..<28: return "(超重)" + default: return "(肥胖)" } } @@ -97,6 +114,18 @@ private struct ProfileEditForm: View { } } + private var weightRow: some View { + HStack { + Text("体重") + Spacer() + TextField("kg", value: $profile.weightKG, format: .number.precision(.fractionLength(0...1))) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(width: 80) + Text("kg").foregroundStyle(Tj.Palette.text3) + } + } + private var bloodTypePicker: some View { Picker("血型", selection: $profile.bloodTypeRaw) { Text("不知道").tag("") diff --git a/康康/Models/UserProfile.swift b/康康/Models/UserProfile.swift index 68bef4f..e0d855c 100644 --- a/康康/Models/UserProfile.swift +++ b/康康/Models/UserProfile.swift @@ -3,10 +3,11 @@ import SwiftData @Model final class UserProfile { - // —— 核心 4 项 —— + // —— 核心 5 项 —— var birthYear: Int? // 1990。隐私考虑只存年,不存月日 var biologicalSexRaw: String // "" / "male" / "female" var heightCM: Int? + var weightKG: Double? // 体重支持小数(68.5) var bloodTypeRaw: String // "" / "A" / "B" / "AB" / "O" // —— 健康背景 —— @@ -22,6 +23,7 @@ final class UserProfile { init(birthYear: Int? = nil, biologicalSexRaw: String = "", heightCM: Int? = nil, + weightKG: Double? = nil, bloodTypeRaw: String = "", allergies: [String] = [], chronicConditions: [String] = [], @@ -31,6 +33,7 @@ final class UserProfile { self.birthYear = birthYear self.biologicalSexRaw = biologicalSexRaw self.heightCM = heightCM + self.weightKG = weightKG self.bloodTypeRaw = bloodTypeRaw self.allergies = allergies self.chronicConditions = chronicConditions @@ -65,19 +68,36 @@ extension UserProfile { return Calendar.current.component(.year, from: .now) - y } - /// 给 ProfileCard 一行预览:"38岁 · 男 · 175cm · A型" + /// 给 ProfileCard 一行预览:"38岁 · 男 · 175cm · 68kg · A型" var summaryLine: String { var parts: [String] = [] if let age { parts.append("\(age)岁") } if sex != .undisclosed { parts.append(sex.label) } if let h = heightCM { parts.append("\(h)cm") } + if let w = weightKG { + let s = w.truncatingRemainder(dividingBy: 1) == 0 + ? String(format: "%.0fkg", w) + : String(format: "%.1fkg", w) + parts.append(s) + } if !bloodTypeRaw.isEmpty { parts.append("\(bloodTypeRaw)型") } return parts.joined(separator: " · ") } /// 资料是否完整到值得显示 summaryLine(否则提示"完善资料") var hasAnyBasics: Bool { - birthYear != nil || sex != .undisclosed || heightCM != nil || !bloodTypeRaw.isEmpty + birthYear != nil || + sex != .undisclosed || + heightCM != nil || + weightKG != nil || + !bloodTypeRaw.isEmpty + } + + /// BMI(kg/m²),需要同时有身高 + 体重才能算 + var bmi: Double? { + guard let h = heightCM, h > 0, let w = weightKG else { return nil } + let m = Double(h) / 100.0 + return w / (m * m) } } diff --git a/康康Tests/UserProfileTests.swift b/康康Tests/UserProfileTests.swift index e588509..b1526db 100644 --- a/康康Tests/UserProfileTests.swift +++ b/康康Tests/UserProfileTests.swift @@ -17,6 +17,7 @@ struct UserProfileTests { let p = UserProfile() #expect(p.birthYear == nil) #expect(p.heightCM == nil) + #expect(p.weightKG == nil) #expect(p.biologicalSexRaw == "") #expect(p.bloodTypeRaw == "") #expect(p.allergies.isEmpty) @@ -25,6 +26,7 @@ struct UserProfileTests { #expect(p.familyHistory.isEmpty) #expect(p.age == nil) #expect(p.sex == .undisclosed) + #expect(p.bmi == nil) #expect(p.hasAnyBasics == false) #expect(p.summaryLine == "") } @@ -50,7 +52,32 @@ struct UserProfileTests { #expect(line.contains("岁")) #expect(line.contains("女")) #expect(line.contains("162cm")) - #expect(!line.contains("型")) // 血型空,不出现 + #expect(!line.contains("kg")) // 体重空,不出现 + #expect(!line.contains("型")) // 血型空,不出现 + } + + @Test func summaryLineIncludesWeightWhenSet() { + let p = UserProfile(heightCM: 175, weightKG: 68.5) + #expect(p.summaryLine.contains("175cm")) + #expect(p.summaryLine.contains("68.5kg")) + } + + @Test func summaryLineFormatsIntegerWeightWithoutDecimal() { + let p = UserProfile(weightKG: 70) + #expect(p.summaryLine == "70kg") + } + + @Test func bmiNilWithoutBothHeightAndWeight() { + #expect(UserProfile(heightCM: 175).bmi == nil) + #expect(UserProfile(weightKG: 68).bmi == nil) + #expect(UserProfile().bmi == nil) + } + + @Test func bmiComputedFromHeightAndWeight() throws { + let p = UserProfile(heightCM: 175, weightKG: 70) + let bmi = try #require(p.bmi) + // 70 / (1.75 * 1.75) = 22.857 + #expect(abs(bmi - 22.857) < 0.01) } @Test func loadOrCreateReturnsExistingSingleton() throws {