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,85 @@
import Testing
import Foundation
import SwiftData
@testable import
/// MedicationScanService.parseMedicationsJSON (JSON )
struct MedicationScanServiceTests {
@Test func parsesStandardObject() throws {
let raw = """
{"medications":[{"name":"","strength":"80mg×7","usage":""}]}
"""
let meds = try MedicationScanService.parseMedicationsJSON(raw)
#expect(meds.count == 1)
#expect(meds[0].name == "缬沙坦胶囊")
#expect(meds[0].strength == "80mg×7粒")
#expect(meds[0].entryText == "缬沙坦胶囊 80mg×7粒")
}
@Test func parsesBareArrayWithFence() throws {
let raw = """
```json
[{"name":"","strength":"0.5g×30","usage":",1,2"}]
```
"""
let meds = try MedicationScanService.parseMedicationsJSON(raw)
#expect(meds.count == 1)
#expect(meds[0].entryText == "二甲双胍缓释片 0.5g×30片 · 口服,一次1片,一日2次")
}
@Test func parsesChineseKeysAndDedupes() throws {
let raw = """
{"medications":[
{"":"","":"100mg","":""},
{"name":"","strength":"100mg","usage":""}
]}
"""
let meds = try MedicationScanService.parseMedicationsJSON(raw)
#expect(meds.count == 1)
}
@Test func emptyNameRowsAreDropped() throws {
let raw = #"{"medications":[{"name":"","strength":"10mg","usage":""}]}"#
let meds = try MedicationScanService.parseMedicationsJSON(raw)
#expect(meds.isEmpty)
}
@Test func trailingCommaIsRepaired() throws {
let raw = #"{"medications":[{"name":"","strength":"10mg×6","usage":"",},]}"#
let meds = try MedicationScanService.parseMedicationsJSON(raw)
#expect(meds.count == 1)
#expect(meds[0].name == "氯雷他定片")
}
@Test func invalidJSONThrows() {
#expect(throws: (any Error).self) {
try MedicationScanService.parseMedicationsJSON("识别不出来,抱歉")
}
}
}
/// 线(tab )
@MainActor
struct MedicationTimelineTests {
private func makeContext() throws -> ModelContext {
let schema = Schema([DiaryEntry.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
return ModelContext(try ModelContainer(for: schema, configurations: [config]))
}
@Test func medicationTaggedDiaryMapsToMedicationKind() throws {
let ctx = try makeContext()
let med = DiaryEntry(content: "缬沙坦胶囊 80mg×7粒", tags: [DiaryEntry.medicationTag])
let plain = DiaryEntry(content: "今天睡得不错")
ctx.insert(med); ctx.insert(plain)
try ctx.save()
let medEntry = TimelineEntry.from(diary: med)
#expect(medEntry.kind == .medication)
#expect(medEntry.title == "缬沙坦胶囊 80mg×7粒")
#expect(TimelineEntry.from(diary: plain).kind == .diary)
}
}

View File

@@ -0,0 +1,22 @@
import Testing
@testable import
struct SpeechDictationMergeTests {
@Test func emptyPrefixReturnsPartial() {
#expect(SpeechDictationService.merge(prefix: "", partial: "今天头晕") == "今天头晕")
}
@Test func plainPrefixJoinsWithSpace() {
#expect(SpeechDictationService.merge(prefix: "已有内容", partial: "新听写")
== "已有内容 新听写")
}
@Test func whitespaceTerminatedPrefixConcatsDirectly() {
#expect(SpeechDictationService.merge(prefix: "第一行\n", partial: "新听写")
== "第一行\n新听写")
}
@Test func emptyPartialKeepsPrefix() {
#expect(SpeechDictationService.merge(prefix: "已有内容", partial: "") == "已有内容")
}
}

View File

@@ -10,6 +10,10 @@ struct TimelineGroupingTests {
return Calendar(identifier: .gregorian).date(from: c)!
}()
@Test func timelineKindOrderMatchesRecordFilterChips() {
#expect(TimelineKind.allCases == [.diary, .symptom, .indicator, .medication, .report])
}
@Test func todaySection() {
#expect(TimelineGrouping.section(for: now, now: now) == .today)
}

View File

@@ -0,0 +1,53 @@
import Testing
import Foundation
@testable import
/// :LLM + 退
struct VoiceIntentServiceTests {
// MARK: - parseIntent(LLM )
@Test func parsesStandardJSON() {
#expect(VoiceIntentService.parseIntent(from: #"{"intent":"indicator"}"#) == .indicator)
}
@Test func parsesFencedAndThinkWrapped() {
let raw = """
<think>用户想记血压</think>
```json
{"intent": "Indicator"}
```
"""
#expect(VoiceIntentService.parseIntent(from: raw) == .indicator)
}
@Test func parsesBareWord() {
#expect(VoiceIntentService.parseIntent(from: "symptom") == .symptom)
#expect(VoiceIntentService.parseIntent(from: "\"diary\"") == .diary)
}
@Test func unknownReturnsNil() {
#expect(VoiceIntentService.parseIntent(from: #"{"intent":"unknown"}"#) == nil)
#expect(VoiceIntentService.parseIntent(from: "我不知道") == nil)
}
// MARK: - keywordMatch(退)
@Test func reminderBeatsMedication() {
// , reminder
#expect(VoiceIntentService.keywordMatch("每天八点提醒我吃药") == .reminder)
}
@Test func commonUtterances() {
#expect(VoiceIntentService.keywordMatch("记一下血压,高压128") == .indicator)
#expect(VoiceIntentService.keywordMatch("我有点头疼") == .symptom)
#expect(VoiceIntentService.keywordMatch("拍个药盒") == .medication)
#expect(VoiceIntentService.keywordMatch("把体检报告存进去") == .archive)
#expect(VoiceIntentService.keywordMatch("整理一份给医生看的") == .export)
#expect(VoiceIntentService.keywordMatch("写个日记") == .diary)
}
@Test func gibberishReturnsNil() {
#expect(VoiceIntentService.keywordMatch("啦啦啦啦") == nil)
}
}