Files
kangkang/康康/Models/Models.swift
link2026 e179a369f6 根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
```
chore(config): 更新项目配置文件

- 调整开发环境配置参数
- 优化构建流程设置
- 更新依赖包版本管理
```
2026-07-01 08:03:35 +08:00

551 lines
20 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import CoreGraphics
import SwiftData
enum IndicatorStatus: String, Codable, CaseIterable {
case high, low, normal
}
/// manual = ;quickCapture = (VL);report =
/// manual()
enum IndicatorSource: String, Codable, CaseIterable {
case manual, quickCapture, report
var label: String {
switch self {
case .manual: return String(appLoc: "手动记录")
case .quickCapture: return String(appLoc: "指标速记")
case .report: return String(appLoc: "报告归档")
}
}
}
enum ReportType: String, Codable, CaseIterable {
case checkup, lab, imaging, prescription, other
var label: String {
switch self {
case .checkup: return String(appLoc: "体检报告")
case .lab: return String(appLoc: "化验单")
case .imaging: return String(appLoc: "影像报告")
case .prescription: return String(appLoc: "处方")
case .other: return String(appLoc: "其他")
}
}
}
@Model
final class Indicator {
var name: String
var value: String
var unit: String
var range: String
var statusRaw: String
var note: String?
var capturedAt: Date
var report: Report?
var asset: Asset?
var pinned: Bool = false
/// key, "bp.systolic" / "glucose.fasting" / "weight"
/// :IndicatorRecordSheet ;VL/Report/ nil
/// :Trends seriesKey ;Timeline ( bp.systolic + bp.diastolic )
var seriesKey: String?
/// (IndicatorSource.rawValue) SwiftData ,
var sourceRaw: String = IndicatorSource.manual.rawValue
/// VL 0-based;box (0...1)
///
var sourcePageIndex: Int?
var sourceBoxX: Double?
var sourceBoxY: Double?
var sourceBoxWidth: Double?
var sourceBoxHeight: Double?
init(name: String,
value: String,
unit: String,
range: String,
status: IndicatorStatus,
note: String? = nil,
capturedAt: Date = .now,
report: Report? = nil,
asset: Asset? = nil,
pinned: Bool = false,
seriesKey: String? = nil,
source: IndicatorSource = .manual,
sourcePageIndex: Int? = nil,
sourceBoxX: Double? = nil,
sourceBoxY: Double? = nil,
sourceBoxWidth: Double? = nil,
sourceBoxHeight: Double? = nil) {
self.name = name
self.value = value
self.unit = unit
self.range = range
self.statusRaw = status.rawValue
self.note = note
self.capturedAt = capturedAt
self.report = report
self.asset = asset
self.pinned = pinned
self.seriesKey = seriesKey
self.sourceRaw = source.rawValue
self.sourcePageIndex = sourcePageIndex
self.sourceBoxX = sourceBoxX
self.sourceBoxY = sourceBoxY
self.sourceBoxWidth = sourceBoxWidth
self.sourceBoxHeight = sourceBoxHeight
}
var status: IndicatorStatus {
IndicatorStatus(rawValue: statusRaw) ?? .normal
}
var source: IndicatorSource {
IndicatorSource(rawValue: sourceRaw) ?? .manual
}
var hasEvidenceBox: Bool {
evidenceRect != nil && sourcePageIndex != nil
}
var evidenceRect: CGRect? {
guard let x = sourceBoxX,
let y = sourceBoxY,
let width = sourceBoxWidth,
let height = sourceBoxHeight,
x >= 0, y >= 0, width > 0, height > 0,
x + width <= 1, y + height <= 1 else {
return nil
}
return CGRect(x: x, y: y, width: width, height: height)
}
}
@Model
final class Report {
var title: String
var typeRaw: String
var reportDate: Date
var institution: String?
var note: String?
var summary: String?
var pageCount: Int
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Indicator.report)
var indicators: [Indicator] = []
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(title: String,
type: ReportType,
reportDate: Date,
institution: String? = nil,
note: String? = nil,
summary: String? = nil,
pageCount: Int = 1,
createdAt: Date = .now) {
self.title = title
self.typeRaw = type.rawValue
self.reportDate = reportDate
self.institution = institution
self.note = note
self.summary = summary
self.pageCount = pageCount
self.createdAt = createdAt
}
var type: ReportType {
ReportType(rawValue: typeRaw) ?? .other
}
}
@Model
final class DiaryEntry {
var content: String
var createdAt: Date
var tags: [String]
/// ( 5 ://)
/// ( swiftdata-rebuild-data-loss)
/// cascade: Asset ;Vault JPEG unlink
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(content: String, createdAt: Date = .now, tags: [String] = []) {
self.content = content
self.createdAt = createdAt
self.tags = tags
}
}
extension DiaryEntry {
/// tag UI ,**** appLoc
/// ()线
static let medicationTag = "用药"
/// () tag: UI ,
/// 线; +
static let consultationTag = "问诊"
var isMedicationLog: Bool { tags.contains(Self.medicationTag) }
var isConsultation: Bool { tags.contains(Self.consultationTag) }
/// ( m4a) mimeType , Asset
var audioAsset: Asset? { assets.first { $0.mimeType.hasPrefix("audio") } }
}
@Model
final class Asset {
var relativePath: String
var mimeType: String
var bytes: Int
var createdAt: Date
init(relativePath: String,
mimeType: String = "image/jpeg",
bytes: Int = 0,
createdAt: Date = .now) {
self.relativePath = relativePath
self.mimeType = mimeType
self.bytes = bytes
self.createdAt = createdAt
}
}
/// : master ()
/// 使( `DiaryEntry.medicationTag` , + ):
/// / / / ,
/// @Model SwiftData ( KangkangApp )
@Model
final class Medication {
var name: String // (,), ParsedMedication.name
var strength: String // , "80mg×7"; ""
var usage: String // , ","; ""
var note: String? // ()
var createdAt: Date
var updatedAt: Date
/// ( / / , 5 )
/// cascade: Asset ;Vault JPEG unlink( DiaryEntry.assets )
@Relationship(deleteRule: .cascade)
var assets: [Asset] = []
init(name: String,
strength: String = "",
usage: String = "",
note: String? = nil,
createdAt: Date = .now) {
self.name = name
self.strength = strength
self.usage = usage
self.note = note
self.createdAt = createdAt
self.updatedAt = createdAt
}
/// / :"80mg×7 · "()
var detailLine: String {
[strength, usage]
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
.joined(separator: " · ")
}
}
@Model
final class Symptom {
var name: String
var startedAt: Date
var endedAt: Date?
var note: String?
var severity: Int
var tags: [String]
var createdAt: Date
init(name: String,
startedAt: Date = .now,
endedAt: Date? = nil,
note: String? = nil,
severity: Int = 3,
tags: [String] = [],
createdAt: Date = .now) {
self.name = name
self.startedAt = startedAt
self.endedAt = endedAt
self.note = note
self.severity = max(1, min(5, severity))
self.tags = tags
self.createdAt = createdAt
}
var isOngoing: Bool { endedAt == nil }
var duration: TimeInterval {
(endedAt ?? .now).timeIntervalSince(startedAt)
}
}
///
/// hardcoded `MonitorMetric` IndicatorQuickSheet grid ;
/// `seriesKey` `"custom.<uuid>"`, Indicator
@Model
final class CustomMonitorMetric {
@Attribute(.unique) var seriesKey: String
var name: String
var unit: String
var lowerBound: Double?
var upperBound: Double?
var icon: String
var createdAt: Date
init(name: String,
unit: String,
lowerBound: Double? = nil,
upperBound: Double? = nil,
icon: String = "circle.fill",
createdAt: Date = .now) {
self.seriesKey = "custom.\(UUID().uuidString)"
self.name = name
self.unit = unit
self.lowerBound = lowerBound
self.upperBound = upperBound
self.icon = icon
self.createdAt = createdAt
}
var referenceRange: ClosedRange<Double>? {
guard let lo = lowerBound, let hi = upperBound, lo <= hi else { return nil }
return lo...hi
}
var rangeText: String {
guard let r = referenceRange else { return "" }
return "\(Self.format(r.lowerBound)) - \(Self.format(r.upperBound))"
}
private static func format(_ v: Double) -> String {
v.truncatingRemainder(dividingBy: 1) == 0
? String(format: "%.0f", v)
: String(format: "%.1f", v)
}
}
///
/// metric (`metricId` = `MonitorMetric.rawValue`)
/// `enabled=false`(), `ctx.delete`
@Model
final class MetricReminder {
@Attribute(.unique) var metricId: String
var displayName: String
var enabled: Bool
var hour: Int // 0...23
var minute: Int // 0...59
var weekdays: [Int] // iOS Calendar :1=, 2=, ..., 7= 7 =
var createdAt: Date
var updatedAt: Date
init(metricId: String,
displayName: String,
hour: Int = 8,
minute: Int = 0,
weekdays: [Int] = [1, 2, 3, 4, 5, 6, 7],
enabled: Bool = true,
createdAt: Date = .now) {
self.metricId = metricId
self.displayName = displayName
self.enabled = enabled
self.hour = max(0, min(23, hour))
self.minute = max(0, min(59, minute))
self.weekdays = weekdays
self.createdAt = createdAt
self.updatedAt = createdAt
}
var isEveryDay: Bool { Set(weekdays) == Set(1...7) }
var frequencyLabel: String {
if !enabled { return String(appLoc: "已关闭") }
if isEveryDay { return String(appLoc: "每天") }
if weekdays.isEmpty { return String(appLoc: "未选日") }
let names = [String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: "")]
let sorted = weekdays.sorted()
return String(appLoc: "每周 ") + sorted.map { names[$0 - 1] }.joined()
}
var timeLabel: String {
String(format: "%02d:%02d", hour, minute)
}
/// (weekday , 7 = ); false
///
func occurs(on date: Date, calendar: Calendar = .current) -> Bool {
guard enabled else { return false }
return weekdays.contains(calendar.component(.weekday, from: date))
}
}
/// ( 20:00 5 12:30 2 )
/// `MetricReminder`():,
/// (5 / 2 ) `title`
/// 沿 weekday ( 7 = ); `ReminderService`
@Model
final class CustomReminder {
/// ; weekdays; dayOfMonth; month + dayOfMonth
enum Frequency: String, CaseIterable, Sendable {
case daily, weekly, monthly, yearly
}
@Attribute(.unique) var id: UUID
var title: String // , "5"
var note: String //
var hour: Int // 0...23
var minute: Int // 0...59
var weekdays: [Int] // iOS Calendar :1=, 2=, ..., 7= 7 =
var frequencyRaw: String = "daily" // :; frequenciesRaw
var dayOfMonth: Int = 1 // yearly + monthly ,1...31
var month: Int = 1 // yearly ,1...12
/// (["daily","weekly",...]) = ,退 frequency
var frequenciesRaw: [String] = []
/// (1...31) = ,退 dayOfMonth
var monthDays: [Int] = []
var enabled: Bool
var createdAt: Date
var updatedAt: Date
init(id: UUID = UUID(),
title: String,
note: String = "",
hour: Int = 8,
minute: Int = 0,
weekdays: [Int] = [1, 2, 3, 4, 5, 6, 7],
frequency: Frequency = .daily,
dayOfMonth: Int = 1,
month: Int = 1,
enabled: Bool = true,
createdAt: Date = .now) {
self.id = id
self.title = title
self.note = note
self.hour = max(0, min(23, hour))
self.minute = max(0, min(59, minute))
self.weekdays = weekdays
self.frequencyRaw = frequency.rawValue
self.dayOfMonth = max(1, min(31, dayOfMonth))
self.month = max(1, min(12, month))
self.enabled = enabled
self.createdAt = createdAt
self.updatedAt = createdAt
}
var isEveryDay: Bool { Set(weekdays) == Set(1...7) }
var frequency: Frequency {
get { Frequency(rawValue: frequencyRaw) ?? .daily }
set { frequencyRaw = newValue.rawValue }
}
/// ()frequenciesRaw 退 frequency( / init)
var frequencies: Set<Frequency> {
get {
let parsed = Set(frequenciesRaw.compactMap { Frequency(rawValue: $0) })
return parsed.isEmpty ? [frequency] : parsed
}
set {
frequenciesRaw = newValue.map(\.rawValue).sorted()
// , frequency
if let rep = newValue.map(\.rawValue).sorted().first { frequencyRaw = rep }
}
}
/// (,1...31)monthDays 退 dayOfMonth()
/// : dayOfMonth yearly ,+
var monthlyDays: [Int] {
get { monthDays.isEmpty ? [dayOfMonth] : monthDays.sorted() }
set { monthDays = Set(newValue.map { max(1, min(31, $0)) }).sorted() }
}
/// : · , · 1·15
/// ()
var frequencyLabel: String {
if !enabled { return String(appLoc: "已关闭") }
let active = frequencies
if active.contains(.daily) { return String(appLoc: "每天") }
// weekly 7
if active == [.weekly] && isEveryDay { return String(appLoc: "每天") }
let order: [Frequency] = [.weekly, .monthly, .yearly]
let parts = order.filter { active.contains($0) }.map { freqPartLabel($0) }
return parts.isEmpty ? String(appLoc: "未选日") : parts.joined(separator: " · ")
}
private func freqPartLabel(_ f: Frequency) -> String {
switch f {
case .daily:
return String(appLoc: "每天")
case .weekly:
if isEveryDay { return String(appLoc: "每天") }
if weekdays.isEmpty { return String(appLoc: "未选日") }
let names = [String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: ""), String(appLoc: "")]
return String(appLoc: "每周 ") + weekdays.sorted().map { names[$0 - 1] }.joined()
case .monthly:
let days = monthlyDays
if days.isEmpty { return String(appLoc: "未选日") }
return String(appLoc: "每月") + days.map { String($0) }.joined(separator: "·") + String(appLoc: "")
case .yearly:
return String(appLoc: "每年\(month)\(dayOfMonth)")
}
}
var timeLabel: String {
String(format: "%02d:%02d", hour, minute)
}
/// (,); false
/// monthly/yearly ( 31 ) false,
/// iOS
func occurs(on date: Date, calendar: Calendar = .current) -> Bool {
guard enabled else { return false }
let c = calendar.dateComponents([.weekday, .day, .month], from: date)
let wd = c.weekday ?? -1, day = c.day ?? -1, mo = c.month ?? -1
// :
for f in frequencies {
switch f {
case .daily: return true
case .weekly: if weekdays.contains(wd) { return true }
case .monthly: if monthlyDays.contains(day) { return true }
case .yearly: if month == mo && dayOfMonth == day { return true }
}
}
return false
}
}
@Model
final class ChatTurn {
var question: String
var answer: String
var referencedIndicatorIDs: [String]
var referencedReportIDs: [String]
var createdAt: Date
var decodeRate: Double
init(question: String,
answer: String,
referencedIndicatorIDs: [String] = [],
referencedReportIDs: [String] = [],
createdAt: Date = .now,
decodeRate: Double = 0) {
self.question = question
self.answer = answer
self.referencedIndicatorIDs = referencedIndicatorIDs
self.referencedReportIDs = referencedReportIDs
self.createdAt = createdAt
self.decodeRate = decodeRate
}
}