- 替换 QuickCaptureFlow 和 ArchiveFlow 为 UnifiedCaptureFlow 统一流程 - 新增 VLSession 封装 Qwen2.5-VL 模型进行图像文本推理 - 实现 AIRuntime 中 VL 模型的准备和分析功能 - 添加 VLPrompts 定义体检化验单识别的 JSON 输出模板 - 创建 CaptureReviewForm 提供 VL 解析结果的可编辑表单界面 - 集成 VisionKit 文档扫描器支持真机多页文档扫描 - 为模拟器实现 PhotosPicker 回退方案选择已有照片 - 在 RootView 中统一使用 UnifiedCaptureFlow 处理快速和归档流程 - 添加 CustomMetricEditor 支持自定义监测指标的创建编辑删除 - 扩展 KangkangApp 模型配置以支持新数据类型 - 实现档案列表中症状结束功能通过时间线行点击触发
289 lines
7.8 KiB
Swift
289 lines
7.8 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
enum IndicatorStatus: String, Codable, CaseIterable {
|
|
case high, low, normal
|
|
}
|
|
|
|
enum ReportType: String, Codable, CaseIterable {
|
|
case checkup, lab, imaging, prescription, other
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .checkup: return "体检报告"
|
|
case .lab: return "化验单"
|
|
case .imaging: return "影像报告"
|
|
case .prescription: return "处方"
|
|
case .other: return "其他"
|
|
}
|
|
}
|
|
}
|
|
|
|
@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?
|
|
|
|
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) {
|
|
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
|
|
}
|
|
|
|
var status: IndicatorStatus {
|
|
IndicatorStatus(rawValue: statusRaw) ?? .normal
|
|
}
|
|
}
|
|
|
|
@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]
|
|
|
|
init(content: String, createdAt: Date = .now, tags: [String] = []) {
|
|
self.content = content
|
|
self.createdAt = createdAt
|
|
self.tags = tags
|
|
}
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|
|
|
|
@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 "已关闭" }
|
|
if isEveryDay { return "每天" }
|
|
if weekdays.isEmpty { return "未选日" }
|
|
let names = ["日", "一", "二", "三", "四", "五", "六"]
|
|
let sorted = weekdays.sorted()
|
|
return "每周 " + sorted.map { names[$0 - 1] }.joined()
|
|
}
|
|
|
|
var timeLabel: String {
|
|
String(format: "%02d:%02d", hour, minute)
|
|
}
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|