主体:多语言支持(简体中文源 + 英/日/韩)
- 基础设施: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>
199 lines
7.0 KiB
Swift
199 lines
7.0 KiB
Swift
import SwiftUI
|
||
|
||
struct B2ScanView: View {
|
||
var onShoot: () -> Void
|
||
var onDone: () -> Void
|
||
var onClose: () -> Void
|
||
var page: Int = 2
|
||
var total: Int = 3
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
Color(red: 0.04, green: 0.047, blue: 0.04).ignoresSafeArea()
|
||
|
||
mockPaper
|
||
|
||
DetectedEdge()
|
||
.stroke(Color(red: 0.95, green: 0.78, blue: 0.45),
|
||
style: StrokeStyle(lineWidth: 2, dash: [6, 4]))
|
||
.opacity(0.95)
|
||
.padding(.horizontal, 30)
|
||
.padding(.top, 140)
|
||
.padding(.bottom, 200)
|
||
.allowsHitTesting(false)
|
||
|
||
VStack(spacing: 0) {
|
||
topBar
|
||
Spacer()
|
||
detectedBadge
|
||
Spacer()
|
||
thumbnails
|
||
bottomControls
|
||
}
|
||
}
|
||
.preferredColorScheme(.dark)
|
||
}
|
||
|
||
private var mockPaper: some View {
|
||
VStack(spacing: 2) {
|
||
Text("体 检 报 告 (第 \(page) 页)")
|
||
.font(.system(size: 12, weight: .bold))
|
||
.padding(.bottom, 4)
|
||
ForEach(reportRows, id: \.0) { row in
|
||
HStack {
|
||
Text(row.0).frame(maxWidth: .infinity, alignment: .leading)
|
||
Text(row.1)
|
||
Text(row.2).foregroundStyle(Tj.Palette.text3)
|
||
}
|
||
.font(.system(size: 9, design: .monospaced))
|
||
}
|
||
}
|
||
.padding(16)
|
||
.foregroundStyle(Tj.Palette.text)
|
||
.frame(maxWidth: .infinity)
|
||
.background(Color(red: 0.97, green: 0.95, blue: 0.89).opacity(0.95))
|
||
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||
.rotation3DEffect(.degrees(8), axis: (x: 1, y: 0, z: 0))
|
||
.rotationEffect(.degrees(-1))
|
||
.shadow(color: .black.opacity(0.6), radius: 20, x: 0, y: 12)
|
||
.padding(.horizontal, 40)
|
||
.padding(.top, 160)
|
||
.padding(.bottom, 220)
|
||
}
|
||
|
||
private var reportRows: [(String, String, String)] {
|
||
[
|
||
(String(appLoc: "总胆固醇"), "5.42", "3.10–5.18"),
|
||
(String(appLoc: "甘油三酯"), "1.78", "0.45–1.70"),
|
||
(String(appLoc: "低密度脂蛋白"), "3.84↑", "<3.40"),
|
||
(String(appLoc: "高密度脂蛋白"), "1.21", ">1.04"),
|
||
(String(appLoc: "载脂蛋白 A1"), "1.42", "1.00–1.60"),
|
||
(String(appLoc: "载脂蛋白 B"), "1.04", "0.55–1.05"),
|
||
(String(appLoc: "谷丙转氨酶"), "28", "9–50"),
|
||
(String(appLoc: "谷草转氨酶"), "24", "15–40"),
|
||
(String(appLoc: "空腹血糖"), "5.4", "3.9–6.1"),
|
||
(String(appLoc: "糖化血红蛋白"), "5.7", "4.0–6.0"),
|
||
]
|
||
}
|
||
|
||
private var topBar: some View {
|
||
HStack {
|
||
Button(action: onClose) {
|
||
Image(systemName: "xmark")
|
||
.font(.system(size: 18, weight: .semibold))
|
||
.foregroundStyle(Color.white)
|
||
.frame(width: 36, height: 36)
|
||
}
|
||
Spacer()
|
||
HStack(spacing: 4) {
|
||
Text("\(page)").font(.system(size: 12, design: .monospaced))
|
||
Text(" / \(total) · 像扫描文档一样对准")
|
||
.font(.system(size: 12))
|
||
}
|
||
.foregroundStyle(Color.white)
|
||
.padding(.horizontal, 14)
|
||
.padding(.vertical, 6)
|
||
.background(Capsule().fill(Color(red: 0.08, green: 0.11, blue: 0.094).opacity(0.7)))
|
||
Spacer()
|
||
Color.clear.frame(width: 36, height: 36)
|
||
}
|
||
.padding(.horizontal, 6)
|
||
.padding(.top, 50)
|
||
}
|
||
|
||
private var detectedBadge: some View {
|
||
Text("已识别边框 · 将自动透视校正")
|
||
.font(.system(size: 10, weight: .semibold))
|
||
.tracking(0.4)
|
||
.foregroundStyle(Color(red: 0.10, green: 0.115, blue: 0.094))
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 3)
|
||
.background(Capsule().fill(Color(red: 0.95, green: 0.78, blue: 0.45)))
|
||
.padding(.top, 140)
|
||
}
|
||
|
||
private var thumbnails: some View {
|
||
HStack {
|
||
PageThumbStack(index: 1)
|
||
Spacer()
|
||
Text("已拍 1 页")
|
||
.font(.system(size: 11, design: .monospaced))
|
||
.foregroundStyle(Color.white.opacity(0.7))
|
||
}
|
||
.padding(.horizontal, 18)
|
||
.padding(.bottom, 24)
|
||
}
|
||
|
||
private var bottomControls: some View {
|
||
HStack {
|
||
Color.clear.frame(width: 60, height: 60)
|
||
Spacer()
|
||
Button(action: onShoot) {
|
||
ZStack {
|
||
Circle().fill(Tj.Palette.paper)
|
||
Circle().strokeBorder(Color.white.opacity(0.4), lineWidth: 4)
|
||
}
|
||
.frame(width: 72, height: 72)
|
||
}
|
||
.buttonStyle(.plain)
|
||
Spacer()
|
||
Button(action: onDone) {
|
||
Text("完成")
|
||
.font(.system(size: 14, weight: .semibold))
|
||
.foregroundStyle(Tj.Palette.paper)
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 10)
|
||
.background(Capsule().fill(Color.white.opacity(0.1)))
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
.padding(.horizontal, 32)
|
||
.padding(.bottom, 40)
|
||
}
|
||
}
|
||
|
||
private struct DetectedEdge: Shape {
|
||
func path(in rect: CGRect) -> Path {
|
||
var p = Path()
|
||
let w = rect.width
|
||
let h = rect.height
|
||
p.move(to: CGPoint(x: w * 0.04, y: h * 0.05))
|
||
p.addLine(to: CGPoint(x: w * 0.92, y: h * 0.02))
|
||
p.addLine(to: CGPoint(x: w * 0.96, y: h * 0.96))
|
||
p.addLine(to: CGPoint(x: 0, y: h * 1.0))
|
||
p.closeSubpath()
|
||
return p
|
||
}
|
||
}
|
||
|
||
struct PageThumbStack: View {
|
||
let index: Int
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||
.fill(Color(red: 0.96, green: 0.93, blue: 0.87).opacity(0.7))
|
||
.frame(width: 56, height: 76)
|
||
.rotationEffect(.degrees(2))
|
||
.offset(x: 4, y: 4)
|
||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 2)
|
||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||
.fill(Color(red: 0.97, green: 0.95, blue: 0.89).opacity(0.85))
|
||
.frame(width: 56, height: 76)
|
||
.rotationEffect(.degrees(-1))
|
||
.offset(x: 2, y: 2)
|
||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 2)
|
||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||
.fill(Tj.Palette.paper)
|
||
.frame(width: 56, height: 76)
|
||
.overlay(
|
||
Text("p.\(index)")
|
||
.font(.system(size: 10, design: .monospaced))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
)
|
||
.shadow(color: .black.opacity(0.4), radius: 4, x: 0, y: 2)
|
||
}
|
||
.frame(width: 64, height: 84, alignment: .topLeading)
|
||
}
|
||
}
|