```
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:
@@ -31,6 +31,18 @@ struct IndicatorQuickSheet: View {
|
||||
/// nil 时(如 Preview)不显示拍照按钮。
|
||||
var onRequestCamera: (() -> Void)? = nil
|
||||
|
||||
/// 从已有指标「再记一条」时的预选目标。nil = 正常空白新建。
|
||||
/// seriesKey 命中 MonitorMetric / CustomMonitorMetric → 预选对应预设(保留进趋势 + 自动判异常);
|
||||
/// 否则按 name/unit/range 走自由输入预填。数值一律留空,由用户填新读数。
|
||||
var prefill: Prefill? = nil
|
||||
|
||||
struct Prefill: Equatable {
|
||||
var seriesKey: String?
|
||||
var name: String = ""
|
||||
var unit: String = ""
|
||||
var range: String = ""
|
||||
}
|
||||
|
||||
@Environment(\.modelContext) private var ctx
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Query private var profiles: [UserProfile]
|
||||
@@ -69,6 +81,32 @@ struct IndicatorQuickSheet: View {
|
||||
// 隐藏管理 sheet 触发态
|
||||
@State private var showHiddenSheet: Bool = false
|
||||
|
||||
// 「再记一条」预选只应用一次
|
||||
@State private var didApplyPrefill = false
|
||||
|
||||
// 顶部搜索:点放大镜展开搜索框,按名实时过滤长期监测预设 / 自定义指标 / 化验项快捷。
|
||||
@State private var searchingMetrics = false
|
||||
@State private var metricQuery = ""
|
||||
|
||||
private var isSearchingMetrics: Bool {
|
||||
!metricQuery.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
}
|
||||
private var filteredMonitorMetrics: [MonitorMetric] {
|
||||
let q = metricQuery.trimmingCharacters(in: .whitespaces)
|
||||
guard !q.isEmpty else { return visibleMonitorMetrics }
|
||||
return visibleMonitorMetrics.filter { $0.displayName.localizedCaseInsensitiveContains(q) }
|
||||
}
|
||||
private var filteredCustomMetrics: [CustomMonitorMetric] {
|
||||
let q = metricQuery.trimmingCharacters(in: .whitespaces)
|
||||
guard !q.isEmpty else { return customMetrics }
|
||||
return customMetrics.filter { $0.name.localizedCaseInsensitiveContains(q) }
|
||||
}
|
||||
private var filteredLabPresets: [IndicatorPreset] {
|
||||
let q = metricQuery.trimmingCharacters(in: .whitespaces)
|
||||
guard !q.isEmpty else { return labPresets }
|
||||
return labPresets.filter { $0.name.localizedCaseInsensitiveContains(q) }
|
||||
}
|
||||
|
||||
private static var defaultReminderTime: Date {
|
||||
Calendar.current.date(bySettingHour: 8, minute: 0, second: 0, of: .now) ?? .now
|
||||
}
|
||||
@@ -137,6 +175,7 @@ struct IndicatorQuickSheet: View {
|
||||
|
||||
footer
|
||||
}
|
||||
.onAppear { applyPrefillIfNeeded() }
|
||||
.task(id: longTermKey) { hydrateReminder() }
|
||||
.background(
|
||||
Tj.Palette.sand
|
||||
@@ -161,19 +200,64 @@ struct IndicatorQuickSheet: View {
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Text("记录指标")
|
||||
.font(.tjH2())
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Spacer()
|
||||
Text("本地处理 · 永不上传")
|
||||
.font(.tjScaled( 12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
VStack(spacing: 12) {
|
||||
HStack(spacing: 10) {
|
||||
Text("记录指标")
|
||||
.font(.tjH2())
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Spacer()
|
||||
Text("本地处理 · 永不上传")
|
||||
.font(.tjScaled( 12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
searchToggle
|
||||
}
|
||||
if searchingMetrics { searchField }
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
|
||||
private var searchToggle: some View {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.18)) {
|
||||
searchingMetrics.toggle()
|
||||
if !searchingMetrics { metricQuery = "" }
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: searchingMetrics ? "xmark" : "magnifyingglass")
|
||||
.font(.tjScaled( 14, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(Circle().fill(Tj.Palette.sand2))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(searchingMetrics ? String(appLoc: "关闭搜索") : String(appLoc: "搜索指标"))
|
||||
}
|
||||
|
||||
private var searchField: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.tjScaled( 13))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
TextField(String(appLoc: "搜索指标名"), text: $metricQuery)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.tint(Tj.Palette.ink)
|
||||
if !metricQuery.isEmpty {
|
||||
Button { metricQuery = "" } label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.background(fieldBg)
|
||||
.overlay(fieldBorder)
|
||||
}
|
||||
|
||||
/// 顶部「拍照识别」入口:并入原「指标速记」。点后由 RootView 切到相机 VL 流程。
|
||||
@ViewBuilder
|
||||
private var cameraEntrySection: some View {
|
||||
@@ -241,13 +325,19 @@ struct IndicatorQuickSheet: View {
|
||||
}
|
||||
let columns = [GridItem(.flexible()), GridItem(.flexible())]
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(visibleMonitorMetrics) { m in
|
||||
ForEach(filteredMonitorMetrics) { m in
|
||||
monitorTile(m)
|
||||
}
|
||||
ForEach(customMetrics) { cm in
|
||||
ForEach(filteredCustomMetrics) { cm in
|
||||
customTile(cm)
|
||||
}
|
||||
addCustomTile
|
||||
// 搜索态下不显示「自定义(新建)」格,聚焦过滤结果。
|
||||
if !isSearchingMetrics { addCustomTile }
|
||||
}
|
||||
if isSearchingMetrics, filteredMonitorMetrics.isEmpty, filteredCustomMetrics.isEmpty {
|
||||
Text("没有匹配的长期监测指标")
|
||||
.font(.tjScaled( 12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHiddenSheet) {
|
||||
@@ -386,14 +476,18 @@ struct IndicatorQuickSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var labPresetSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sectionLabel(String(appLoc: "化验项快捷(不进趋势)"))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(labPresets) { p in
|
||||
chip(p.name, selected: selectedLabPreset == p) {
|
||||
applyLab(p)
|
||||
// 搜索且化验项无匹配:整段隐藏(避免只剩一个空标题)。
|
||||
if !(isSearchingMetrics && filteredLabPresets.isEmpty) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sectionLabel(String(appLoc: "化验项快捷(不进趋势)"))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(filteredLabPresets) { p in
|
||||
chip(p.name, selected: selectedLabPreset == p) {
|
||||
applyLab(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -941,6 +1035,29 @@ struct IndicatorQuickSheet: View {
|
||||
|
||||
// MARK: - apply preset
|
||||
|
||||
/// 「再记一条」预选:seriesKey 命中长期监测预设 / 自定义指标则选中对应卡片(进趋势 + 自动判异常),
|
||||
/// 否则按 name/unit/range 走自由输入。数值不预填——让用户填新读数。只应用一次。
|
||||
private func applyPrefillIfNeeded() {
|
||||
guard !didApplyPrefill, let p = prefill else { return }
|
||||
didApplyPrefill = true
|
||||
if let key = p.seriesKey {
|
||||
if let m = MonitorMetric.allCases.first(where: { metric in
|
||||
metric.fields.contains { $0.seriesKey == key }
|
||||
}) {
|
||||
applyMonitor(m)
|
||||
return
|
||||
}
|
||||
if let cm = customMetrics.first(where: { $0.seriesKey == key }) {
|
||||
applyCustom(cm)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 无 seriesKey 或未匹配预设(化验项 / 报告 / 自由指标):自由输入预填,不带 seriesKey,不进趋势。
|
||||
name = p.name
|
||||
unit = p.unit
|
||||
range = p.range
|
||||
}
|
||||
|
||||
private func applyMonitor(_ m: MonitorMetric) {
|
||||
if selectedMonitor == m {
|
||||
// 取消选择
|
||||
|
||||
39
康康/Features/Indicator/RecordAnotherButton.swift
Normal file
39
康康/Features/Indicator/RecordAnotherButton.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
extension IndicatorQuickSheet.Prefill {
|
||||
/// 从一条已有指标推断「再记一条」的预选目标:
|
||||
/// seriesKey 命中长期监测 / 自定义指标则预选对应预设(进趋势 + 自动判异常),否则按 name/unit/range 自由预填。
|
||||
init(indicator i: Indicator) {
|
||||
self.init(seriesKey: i.seriesKey, name: i.name, unit: i.unit, range: i.range)
|
||||
}
|
||||
}
|
||||
|
||||
/// 指标详情 / 同类聚合详情底部「再记一条」按钮:打开预选同款指标的录入表单(数值留空,由用户填新读数)。
|
||||
/// 自带弹窗状态,`TimelineEntryDetailView` 与 `IndicatorSeriesDetailView` 共用同一组件。
|
||||
struct RecordAnotherButton: View {
|
||||
/// 按钮文案里显示的指标名(如「空腹血糖」「血压」)。
|
||||
let name: String
|
||||
/// 打开录入表单时的预选目标。
|
||||
let prefill: IndicatorQuickSheet.Prefill
|
||||
|
||||
@State private var showSheet = false
|
||||
|
||||
var body: some View {
|
||||
Button { showSheet = true } label: {
|
||||
Label(String(appLoc: "再记一条「\(name)」"), systemImage: "plus.circle.fill")
|
||||
.font(.tjScaled( 13, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.ink)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
|
||||
.fill(Tj.Palette.leaf.opacity(0.16))
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.sheet(isPresented: $showSheet) {
|
||||
IndicatorQuickSheet(prefill: prefill)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user