feat(CaptureService): 改进报告解析逻辑并添加多语言键支持

- 修改应用描述从"个人健康影像档案"到"个人健康随记"
- 添加对多种JSON键名的支持,包括中文键名(如"指标"、"项目"、"结果"等)
- 实现指标状态智能推断功能,可根据数值和参考范围自动判断高低状态
- 支持多种状态标识符,包括箭头符号(↑↓)和中英文状态词
- 增加对不同参考范围格式的解析支持(如"< 3.40"、"208 - 428"等)
- 添加相关单元测试验证中文键名和状态推断功能
```
This commit is contained in:
link2026
2026-06-06 12:53:52 +08:00
parent 77697e1600
commit 675c33bea1
6 changed files with 540 additions and 20 deletions

View File

@@ -22,7 +22,7 @@ struct AboutView: View {
section(icon: "sparkles", title: String(appLoc: "这是什么")) {
paragraph(
String(appLoc: "康康是一款以本地优先为设计原则的个人健康影像档案工具。") +
String(appLoc: "康康是一款以本地优先为设计原则的个人健康随记工具。") +
String(appLoc: "你可以拍下体检报告、化验单和影像资料,图片与数据默认保存在本机;") +
String(appLoc: "设备上的 AI 模型会尝试把专业指标转述为通俗说明,帮你记录并回顾自己的健康变化。")
)
@@ -107,7 +107,7 @@ struct AboutView: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("本地优先的个人健康影像档案")
Text("本地优先的个人健康随记")
.font(.system(size: 13))
.foregroundStyle(Tj.Palette.text2)

View File

@@ -5609,24 +5609,24 @@
}
}
},
"康康是一款以本地优先为设计原则的个人健康影像档案工具。" : {
"康康是一款以本地优先为设计原则的个人健康随记工具。" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kangkang is a personal health imaging archive tool designed with a local-first principle."
"value" : "Kangkang is a personal health journal tool designed with a local-first principle."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kangkangは、ローカルファーストを設計原則とする個人向け健康画像アーカイブツールです。"
"value" : "Kangkangは、ローカルファーストを設計原則とする個人向け健康ジャーナルツールです。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kangkang은 로컬 우선을 설계 원칙으로 하는 개인 건강 영상 아카이브 도구입니다."
"value" : "Kangkang은 로컬 우선을 설계 원칙으로 하는 개인 건강 저널 도구입니다."
}
}
}
@@ -8138,24 +8138,24 @@
}
}
},
"本地优先的个人健康影像档案" : {
"本地优先的个人健康随记" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Local-first personal health imaging archive"
"value" : "Local-first personal health journal"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ローカル優先の個人健康画像アーカイブ"
"value" : "ローカル優先の個人健康ジャーナル"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "로컬 우선 개인 건강 영상 아카이브"
"value" : "로컬 우선 개인 건강 저널"
}
}
}
@@ -12207,4 +12207,4 @@
}
},
"version" : "1.0"
}
}

View File

@@ -179,7 +179,7 @@ actor CaptureService {
let summary = (dict["summary"] as? String) ?? ""
let pages = (dict["page_count"] as? Int) ?? pageCount
let indicatorsRaw = (dict["indicators"] as? [[String: Any]]) ?? []
let indicatorsRaw = arrayValue(dict, keys: ["indicators", "indicator", "items", "指标", "指标列表", "项目"])
let indicators: [ParsedReport.ParsedIndicator] = indicatorsRaw.compactMap {
parseIndicator($0)
}
@@ -212,7 +212,7 @@ actor CaptureService {
// :{"indicators":[...]} [...]( key)
let indicatorsRaw: [[String: Any]]
if let dict = obj as? [String: Any] {
indicatorsRaw = (dict["indicators"] as? [[String: Any]]) ?? []
indicatorsRaw = arrayValue(dict, keys: ["indicators", "indicator", "items", "指标", "指标列表", "项目"])
} else if let arr = obj as? [[String: Any]] {
indicatorsRaw = arr
} else {
@@ -335,18 +335,100 @@ actor CaptureService {
}
private static func parseIndicator(_ d: [String: Any]) -> ParsedReport.ParsedIndicator? {
guard let name = (d["name"] as? String)?.trimmingCharacters(in: .whitespaces),
guard let name = stringValue(d, keys: ["name", "item", "indicator", "test", "项目", "指标", "指标名", "指标名称", "检查项目", "检验项目"])?.trimmingCharacters(in: .whitespaces),
!name.isEmpty else { return nil }
let value: String
if let v = d["value"] as? String { value = v }
else if let v = d["value"] as? NSNumber { value = v.stringValue }
if let v = stringValue(d, keys: ["value", "result", "reading", "结果", "数值", "检测值", "测定值"]) { value = v }
else { value = "" }
let unit = (d["unit"] as? String) ?? ""
let range = (d["range"] as? String) ?? ""
let statusRaw = (d["status"] as? String)?.lowercased() ?? "normal"
let status = IndicatorStatus(rawValue: statusRaw) ?? .normal
let unit = stringValue(d, keys: ["unit", "单位"]) ?? ""
let range = stringValue(d, keys: ["range", "reference", "reference_range", "ref", "参考", "参考值", "参考范围", "正常范围"]) ?? ""
let statusRaw = stringValue(d, keys: ["status", "flag", "abnormal", "异常", "提示", "标记"])
let status = parseIndicatorStatus(raw: statusRaw, value: value, range: range)
return .init(name: name, value: value, unit: unit, range: range, status: status)
}
private static func stringValue(_ d: [String: Any], keys: [String]) -> String? {
for key in keys {
if let s = d[key] as? String {
return s
}
if let n = d[key] as? NSNumber {
return n.stringValue
}
}
return nil
}
private static func arrayValue(_ d: [String: Any], keys: [String]) -> [[String: Any]] {
for key in keys {
if let arr = d[key] as? [[String: Any]] {
return arr
}
}
return []
}
private static func parseIndicatorStatus(raw: String?, value: String, range: String) -> IndicatorStatus {
let normalized = raw?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() ?? ""
if ["high", "h", "hi", "above", "up", "", "", "+", "偏高", "", "增高", "升高", "偏高↑", "h↑"].contains(normalized) {
return .high
}
if ["low", "l", "lo", "below", "down", "", "", "-", "偏低", "", "降低", "偏低↓", "l↓"].contains(normalized) {
return .low
}
if ["normal", "n", "ok", "正常", "阴性", "无异常"].contains(normalized) {
return .normal
}
return inferStatus(value: value, range: range) ?? .normal
}
private static func inferStatus(value: String, range: String) -> IndicatorStatus? {
guard let v = firstNumber(in: value) else { return nil }
let compact = range
.replacingOccurrences(of: "", with: "-")
.replacingOccurrences(of: "", with: "-")
.replacingOccurrences(of: "", with: "-")
.replacingOccurrences(of: "~", with: "-")
.replacingOccurrences(of: "", with: "-")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !compact.isEmpty else { return nil }
let numbers = numbers(in: compact)
if compact.contains("<") || compact.contains("") || compact.contains("") {
guard let upper = numbers.first else { return nil }
return v > upper ? .high : .normal
}
if compact.contains(">") || compact.contains("") || compact.contains("") {
guard let lower = numbers.first else { return nil }
return v < lower ? .low : .normal
}
if numbers.count >= 2 {
let lower = numbers[0]
let upper = numbers[1]
if v < lower { return .low }
if v > upper { return .high }
return .normal
}
return nil
}
private static func firstNumber(in text: String) -> Double? {
numbers(in: text).first
}
private static func numbers(in text: String) -> [Double] {
let pattern = #"-?\d+(?:\.\d+)?"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
let ns = text as NSString
let range = NSRange(location: 0, length: ns.length)
return regex.matches(in: text, range: range).compactMap {
Double(ns.substring(with: $0.range))
}
}
}
// MARK: - Report CaptureService (MainActor )