feat: 添加拍药盒功能和语音直达入口

- 实现拍药盒扫描流程,支持本地OCR识别药品信息
- 在日记页面添加拍药盒和记症状的三选一入口
- 优化按钮点击区域,确保符合苹果HIG最小命中区标准
- 添加用药记录到时间线的独立分类显示
- 实现长按+号语音直达功能,支持语音意图分类跳转
- 更新项目配置文件,启用代码分析和死代码剥离选项
- 增加多项本地化字符串支持新功能
```
This commit is contained in:
link2026
2026-06-13 09:16:25 +08:00
parent f58d6064ba
commit 6c6a950140
30 changed files with 1856 additions and 64 deletions

View File

@@ -0,0 +1,44 @@
import Foundation
/// App Widget ( App Group UserDefaults )
///
/// Widget SwiftData:store App ,
/// extension ;,
///
/// :`KangkangWidget` extension
/// (extension App , Xcode target membership )
/// :KangkangWidget/PinnedIndicatorsWidget.swift
struct WidgetSnapshot: Codable, Equatable {
struct Item: Codable, Equatable {
var name: String // ""
var value: String // "128"
var unit: String // "mmHg"
var statusRaw: String // IndicatorStatus.rawValue: high|low|normal
var capturedAt: Date
}
var updatedAt: Date
var items: [Item]
// MARK: - App Group
/// App Group ID target App Groups capability
static let appGroupID = "group.com.xuhuayong.kangkang"
static let storeKey = "kk.widget.snapshot.v1"
/// App Group (capability ) nil ,App
static var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
func save(to defaults: UserDefaults? = WidgetSnapshot.sharedDefaults) {
guard let defaults, let data = try? JSONEncoder().encode(self) else { return }
defaults.set(data, forKey: Self.storeKey)
}
static func load(from defaults: UserDefaults? = WidgetSnapshot.sharedDefaults) -> WidgetSnapshot? {
guard let defaults,
let data = defaults.data(forKey: storeKey) else { return nil }
return try? JSONDecoder().decode(WidgetSnapshot.self, from: data)
}
}

View File

@@ -0,0 +1,42 @@
import Foundation
import SwiftData
import WidgetKit
/// pinned App Group , WidgetKit
/// :App / (RootView)( pinned), AI
/// App Group capability no-op, App
enum WidgetSnapshotRefresher {
/// (seriesKey, name), 6
@MainActor
static func refresh(in ctx: ModelContext) {
let pinnedPredicate = #Predicate<Indicator> { $0.pinned == true }
var descriptor = FetchDescriptor<Indicator>(
predicate: pinnedPredicate,
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
)
descriptor.fetchLimit = 200 // pinned ,
guard let pinned = try? ctx.fetch(descriptor) else { return }
var seenSeries = Set<String>()
var items: [WidgetSnapshot.Item] = []
for ind in pinned { // capturedAt ,
let key = ind.seriesKey ?? ind.name
guard seenSeries.insert(key).inserted else { continue }
items.append(.init(
name: ind.name,
value: ind.value,
unit: ind.unit,
statusRaw: ind.statusRaw,
capturedAt: ind.capturedAt
))
if items.count >= 6 { break }
}
let snapshot = WidgetSnapshot(updatedAt: .now, items: items)
// , WidgetKit
if let old = WidgetSnapshot.load(), old.items == snapshot.items { return }
snapshot.save()
WidgetCenter.shared.reloadAllTimelines()
}
}