Files
kangkang/康康/Features/Diary/MedicationLogSheet.swift
link2026 9d856fcfc4 ```
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,使用信号量闸门控制显存占用
- 更新文档中的技术栈说明、模块边界和周次交付计划
```
2026-06-15 09:24:59 +08:00

134 lines
5.3 KiB
Swift

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)
}