Files
kangkang/康康/Features/Archive/B5ResultView.swift
link2026 d2c77d5c51 feat: 国际化(i18n) en/ja/ko + App 内语言切换
主体:多语言支持(简体中文源 + 英/日/韩)
- 基础设施:Localizable.xcstrings(String Catalog,sourceLanguage=zh-Hans)
  + pbxproj developmentRegion/knownRegions 注册 en/ja/ko
- 全部硬编码 Locale("zh_CN") → Locale.current;中文 dateFormat → Date.FormatStyle(跟随系统)
- UI 中文字面量统一为 String(appLoc:)(显式绑定所选语言 bundle+locale,即时切换)
  Text 字面量走环境 \.locale + Bundle 重定向
- 549 个 catalog key 全部 en/ja/ko 翻译完成(0 未翻译)
- App 内语言切换:我的 → 语言(LanguageManager + 即时生效,无需重启)
- 双用预设(症状/监测指标/慢病)本地化:static→computed 避免缓存

注:本提交为 WIP,一并打包了并行进行的功能模块
(HealthExport 健康导出、Security/Face ID 锁、DiaryAssist 日记 AI 辅助)
及 App 图标、CLAUDE.md、docs/scripts。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:28:24 +08:00

324 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct B5IndicatorData {
let name: String
let value: String
let unit: String
let range: String
let status: IndicatorStatus
let note: String?
}
struct B5ResultView: View {
var onSave: () -> Void
var onBack: () -> Void
@State private var expandedIndex: Int? = 0
@State private var normalsExpanded = false
let abnormal: [B5IndicatorData] = [
.init(name: String(appLoc: "低密度脂蛋白胆固醇"), value: "3.84", unit: "mmol/L", range: "< 3.40", status: .high,
note: String(appLoc: "超过参考上限 0.44。建议关注饮食结构3 个月内复查。")),
.init(name: String(appLoc: "甘油三酯 TG"), value: "1.78", unit: "mmol/L", range: "0.451.70", status: .high, note: nil),
.init(name: String(appLoc: "尿酸 UA"), value: "428", unit: "μmol/L", range: "150420", status: .high, note: nil),
.init(name: String(appLoc: "维生素 D"), value: "18", unit: "ng/mL", range: "30100", status: .low, note: nil),
]
let normalCount = 24
var body: some View {
VStack(spacing: 0) {
header
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
reportMeta.padding(.bottom, 16)
summaryCard.padding(.bottom, 18)
SectionLabel(String(appLoc: "异常项"), count: abnormal.count, accent: .brick)
.padding(.bottom, 10)
VStack(spacing: 8) {
ForEach(Array(abnormal.enumerated()), id: \.offset) { idx, it in
IndicatorRow(item: it, expanded: expandedIndex == idx) {
withAnimation { expandedIndex = (expandedIndex == idx) ? nil : idx }
}
}
}
.padding(.bottom, 18)
SectionLabel(String(appLoc: "正常项"), count: normalCount, accent: .leaf)
.padding(.bottom, 10)
normalCollapsed
}
.padding(.horizontal, 18)
.padding(.bottom, 16)
}
HStack(spacing: 10) {
Button(action: onSave) {
Text("保存归档").frame(maxWidth: .infinity)
}
.buttonStyle(TjPrimaryButton())
Button { } label: {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 16, weight: .semibold))
}
.buttonStyle(TjGhostButton(horizontalPadding: 16))
}
.padding(.horizontal, 18)
.padding(.bottom, 14)
.padding(.top, 10)
}
.background(Tj.Palette.sand.ignoresSafeArea())
}
private var header: some View {
HStack(spacing: 6) {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 36, height: 36)
}
Spacer()
Button { } label: {
HStack(spacing: 4) {
Image(systemName: "photo")
Text("查看原图")
}
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
.padding(8)
}
}
.padding(.horizontal, 12)
.padding(.top, 4)
}
private var reportMeta: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
TjBadge(text: String(appLoc: "体检报告"), style: .ink)
Text("3 页")
.font(.system(size: 11))
.foregroundStyle(Tj.Palette.text3)
Spacer()
TjLockChip()
}
Text("2026 春季年度体检")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(Tj.Palette.text)
Text("2026 / 05 / 25 · 协和医院体检中心")
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
}
}
private var summaryCard: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 10) {
Text("整体摘记")
.font(.system(size: 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.brick)
.fixedSize()
Rectangle().fill(Tj.Palette.line).frame(height: 1)
Text("本机摘要")
.font(.system(size: 11))
.foregroundStyle(Tj.Palette.text3)
.fixedSize()
}
.padding(.bottom, 12)
HStack(spacing: 14) {
Stat(n: "28", label: String(appLoc: "总项"))
Stat(n: "3", label: String(appLoc: "偏高"), tone: .brick)
Stat(n: "1", label: String(appLoc: "偏低"), tone: .amber)
Stat(n: "24", label: String(appLoc: "正常"), tone: .leaf)
}
.padding(.bottom, 14)
Text("本次共检测 28 项,\(Text("3 项偏高").fontWeight(.semibold).underline(color: Tj.Palette.brick))(血脂相关 2 项 + 尿酸)、\(Text("1 项偏低").fontWeight(.semibold).underline(color: Tj.Palette.amber))(维生素 D。整体趋势提示代谢风险有所抬升建议优化饮食并复查血脂。")
.font(.system(size: 14))
.foregroundStyle(Tj.Palette.text)
.lineSpacing(6)
.padding(.bottom, 12)
TjDashedDivider().padding(.bottom, 10)
Text("仅供参考,不构成医疗建议")
.font(.system(size: 11))
.italic()
.foregroundStyle(Tj.Palette.text3)
}
.padding(.leading, 20)
.padding(.trailing, 20)
.padding(.vertical, 20)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
Tj.Palette.paper
.overlay(alignment: .leading) {
Tj.Palette.brick.frame(width: 3)
}
)
.clipShape(RoundedRectangle(cornerRadius: 2, style: .continuous))
.shadow(color: Color(red: 0.196, green: 0.157, blue: 0.098).opacity(0.06), radius: 0, x: 0, y: 1)
}
private var normalCollapsed: some View {
Button { withAnimation { normalsExpanded.toggle() } } label: {
HStack(spacing: 10) {
TjBadge(text: "\(normalCount)", style: .leaf)
Text("谷丙转氨酶、空腹血糖、糖化血红蛋白…")
.font(.system(size: 13))
.foregroundStyle(Tj.Palette.text2)
.lineLimit(1)
Spacer()
Image(systemName: normalsExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.tjCard(bordered: true)
}
.buttonStyle(.plain)
}
}
private struct Stat: View {
let n: String
let label: String
var tone: Tone = .ink
enum Tone { case ink, brick, amber, leaf }
var color: Color {
switch tone {
case .ink: return Tj.Palette.text
case .brick: return Tj.Palette.brick
case .amber: return Color(red: 0.59, green: 0.45, blue: 0.27)
case .leaf: return Tj.Palette.leaf
}
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(n)
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(color)
Text(label)
.font(.system(size: 10))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct SectionLabel: View {
let title: String
let count: Int
let accent: AccentKind
enum AccentKind { case brick, leaf }
init(_ title: String, count: Int, accent: AccentKind) {
self.title = title
self.count = count
self.accent = accent
}
var body: some View {
HStack(spacing: 8) {
RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(accent == .brick ? Tj.Palette.brick : Tj.Palette.leaf)
.frame(width: 4, height: 14)
Text(title).font(.system(size: 13, weight: .semibold)).foregroundStyle(Tj.Palette.text)
Text("· \(count)").font(.system(size: 11)).foregroundStyle(Tj.Palette.text3)
}
}
}
private struct IndicatorRow: View {
let item: B5IndicatorData
let expanded: Bool
let onTap: () -> Void
var statusBadge: TjBadgeStyle {
switch item.status {
case .high: return .brick
case .low: return .amber
case .normal: return .leaf
}
}
var statusWord: String {
switch item.status {
case .high: return String(appLoc: "偏高")
case .low: return String(appLoc: "偏低")
case .normal: return String(appLoc: "正常")
}
}
var valueColor: Color {
switch item.status {
case .high: return Tj.Palette.brick
case .low: return Color(red: 0.55, green: 0.45, blue: 0.32)
case .normal: return Tj.Palette.text
}
}
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text(item.name)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
TjBadge(text: statusWord, style: statusBadge)
}
Text("范围 \(item.range) \(item.unit)")
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
Spacer(minLength: 8)
VStack(alignment: .trailing, spacing: 2) {
Text(item.value)
.font(.system(size: 22, weight: .semibold))
.foregroundStyle(valueColor)
Text(item.unit)
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
if expanded, let note = item.note {
TjDashedDivider()
Text(note)
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text2)
.lineSpacing(5)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(
item.status != .normal
? Color(red: 0.78, green: 0.68, blue: 0.48).opacity(0.5)
: Tj.Palette.lineSoft,
lineWidth: 1
)
)
}
.buttonStyle(.plain)
}
}