```
feat(AI): 集成MNN推理引擎替换MLX作为主AI运行时 - 引入MNN(alibaba) + Arm SME2 + CPU作为主AI运行时,支持A19/iPhone17的 SME2和A17的NEON加速 - 添加MLX Swift作为兜底GPU推理方案,实现双后端切换机制 - 使用单一Qwen3.5-2B多模态模型(1.2GB),替代原有的LLM+VL分离架构 - 实现InferenceEngine.current引擎选择逻辑,真机默认MNN,模拟器回退MLX - 更新AIAgent架构,通过MNNLLMBridge(ObjC++) → MNNBackend进行推理 - 修改队列机制防止并发推理导致OOM,使用信号量闸门控制显存占用 - 更新文档中的技术栈说明、模块边界和周次交付计划 ```
This commit is contained in:
133
康康/Features/Diary/MedicationLogSheet.swift
Normal file
133
康康/Features/Diary/MedicationLogSheet.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
/// 「写日记 · 用药」:记一次服用流水 —— 选药(药品库或手输)+ 剂量 + 时间
|
||||
/// → 带 `DiaryEntry.medicationTag` 的日记,进「记录」时间线的「用药」分类。
|
||||
///
|
||||
/// 与药品库(`Medication`,master 清单)分层:这里是「某次吃了多少、什么时候吃的」。
|
||||
/// 嵌套 sheet,自带保存 / 取消(同 `SymptomStartSheet`),关闭后回到写日记页。
|
||||
struct MedicationLogSheet: View {
|
||||
@Environment(\.modelContext) private var ctx
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Query(sort: \Medication.updatedAt, order: .reverse)
|
||||
private var library: [Medication]
|
||||
|
||||
/// 选中的药品库药;手输模式为 nil。
|
||||
@State private var selectedMed: Medication?
|
||||
/// 手输药名(药品库为空,或想记不在库里的药)。与 selectedMed 互斥。
|
||||
@State private var manualName = ""
|
||||
@State private var dosage = ""
|
||||
@State private var takenAt: Date = .now
|
||||
|
||||
private var resolvedName: String {
|
||||
(selectedMed?.name ?? manualName).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
private var canSave: Bool { !resolvedName.isEmpty }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
if library.isEmpty {
|
||||
TextField(String(appLoc: "药名,如:缬沙坦胶囊"), text: $manualName)
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
} else {
|
||||
ForEach(library) { m in
|
||||
Button { select(m) } label: { medRow(m) }
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
TextField(String(appLoc: "或手动输入药名"), text: $manualName)
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.onChange(of: manualName) { _, v in
|
||||
if !v.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
selectedMed = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("吃了哪个药")
|
||||
} footer: {
|
||||
if library.isEmpty {
|
||||
Text("药品库还没有药,可在「记录 · 药品库」拍药盒或手动添加。这里直接手输也行。")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField(String(appLoc: "剂量,如:1 片 / 80mg"), text: $dosage)
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
} header: {
|
||||
Text("剂量")
|
||||
}
|
||||
|
||||
Section {
|
||||
DatePicker(String(appLoc: "时间"), selection: $takenAt, in: ...Date.now)
|
||||
} header: {
|
||||
Text("时间")
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||
.navigationTitle("记录用药")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button(String(appLoc: "取消")) { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(String(appLoc: "保存")) { save() }
|
||||
.fontWeight(.semibold)
|
||||
.disabled(!canSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func medRow(_ m: Medication) -> some View {
|
||||
let on = selectedMed === m
|
||||
return HStack(spacing: 10) {
|
||||
Image(systemName: on ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(on ? Tj.Palette.ink : Tj.Palette.text3)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(m.name)
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
if !m.detailLine.isEmpty {
|
||||
Text(m.detailLine)
|
||||
.font(.tjScaled( 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private func select(_ m: Medication) {
|
||||
selectedMed = m
|
||||
manualName = ""
|
||||
}
|
||||
|
||||
private func save() {
|
||||
guard canSave else { return }
|
||||
// content 单行:「药名 [规格] · 剂量」。剂量进正文,时间用 createdAt 承载。
|
||||
// 与 TimelineEntry.firstLine / TimelineEntryDetailView.medicationLines 单行解析兼容。
|
||||
var line = resolvedName
|
||||
if let s = selectedMed?.strength, !s.isEmpty { line += " \(s)" }
|
||||
let dose = dosage.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !dose.isEmpty { line += " · \(dose)" }
|
||||
|
||||
let entry = DiaryEntry(content: line, createdAt: takenAt, tags: [DiaryEntry.medicationTag])
|
||||
ctx.insert(entry)
|
||||
try? ctx.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MedicationLogSheet()
|
||||
.modelContainer(for: [Medication.self, DiaryEntry.self, Asset.self], inMemory: true)
|
||||
}
|
||||
Reference in New Issue
Block a user