```
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:
@@ -11,8 +11,10 @@ struct DiaryQuickSheet: View {
|
||||
|
||||
@State private var content: String = ""
|
||||
@State private var createdAt: Date = .now
|
||||
/// 「拍药盒」分支:全屏扫描流程,确认后存为带「用药」tag 的日记。
|
||||
/// 「拍药盒」分支:全屏扫描流程,识别后入药品库。
|
||||
@State private var showMedicationScan = false
|
||||
/// 「用药」分支:记一次服用(选药 + 剂量 + 时间),存为带「用药」tag 的日记。
|
||||
@State private var showMedicationLog = false
|
||||
/// 「记症状」分支:嵌套弹出 SymptomStartSheet(自带保存/取消,关闭后回到本页)。
|
||||
@State private var showSymptomStart = false
|
||||
|
||||
@@ -98,14 +100,20 @@ struct DiaryQuickSheet: View {
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
// 入口三选一:写日记(本页)/ 拍药盒(存「用药」日记)/ 记症状(SymptomStartSheet)
|
||||
HStack(spacing: 10) {
|
||||
// 入口四选(2×2):写日记(本页)/ 用药(MedicationLogSheet,记剂量+时间)/
|
||||
// 拍药盒(识别入药品库)/ 记症状(SymptomStartSheet)。
|
||||
LazyVGrid(columns: [GridItem(.flexible(), spacing: 10),
|
||||
GridItem(.flexible(), spacing: 10)], spacing: 10) {
|
||||
modeCard(icon: "pencil", title: String(appLoc: "写日记"),
|
||||
subtitle: String(appLoc: "文字或语音"), active: true) {
|
||||
contentFocused = true
|
||||
}
|
||||
modeCard(icon: "pills.fill", title: String(appLoc: "拍药盒"),
|
||||
subtitle: String(appLoc: "识别用药"), active: false) {
|
||||
modeCard(icon: "pills.fill", title: String(appLoc: "用药"),
|
||||
subtitle: String(appLoc: "记剂量与时间"), active: false) {
|
||||
showMedicationLog = true
|
||||
}
|
||||
modeCard(icon: "camera.viewfinder", title: String(appLoc: "拍药盒"),
|
||||
subtitle: String(appLoc: "识别入药品库"), active: false) {
|
||||
showMedicationScan = true
|
||||
}
|
||||
modeCard(icon: "waveform.path.ecg", title: String(appLoc: "记症状"),
|
||||
@@ -252,9 +260,9 @@ struct DiaryQuickSheet: View {
|
||||
.presentationCornerRadius(Tj.Radius.xl)
|
||||
.fullScreenCover(isPresented: $showMedicationScan) {
|
||||
MedicationScanFlow(
|
||||
onSave: { entries in
|
||||
// 落库:「用药」日记(进记录时间线)+ 同步个人资料·当前用药。
|
||||
MedicationArchiver.archive(entries: entries, in: ctx)
|
||||
onSave: { meds, images in
|
||||
// 识别后入药品库(含原图),不再写日记。服用流水走「写日记 · 用药」模式。
|
||||
MedicationArchiver.archive(medications: meds, images: images, in: ctx)
|
||||
dismiss()
|
||||
},
|
||||
onClose: { showMedicationScan = false }
|
||||
@@ -264,6 +272,10 @@ struct DiaryQuickSheet: View {
|
||||
// 嵌套 sheet:症状表单自带保存/取消;取消回到日记,不强行关闭。
|
||||
SymptomStartSheet()
|
||||
}
|
||||
.sheet(isPresented: $showMedicationLog) {
|
||||
// 嵌套 sheet:用药记录表单自带保存/取消;保存后回到日记(不强行关闭)。
|
||||
MedicationLogSheet()
|
||||
}
|
||||
.onDisappear {
|
||||
suggestTask?.cancel()
|
||||
voiceFlowTask?.cancel()
|
||||
|
||||
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