Files
kangkang/康康/Features/Archive/B4ProgressView.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

294 lines
10 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 B4ProgressView: View {
var onComplete: () -> Void
@State private var step: Int = 1
@State private var pulse = false
@State private var glow = false
@State private var rotate: Double = 0
@State private var elapsed: Double = 0.2
private let lineLabels = [
String(appLoc: "正在本地识别第 1 / 3 页…"),
String(appLoc: "正在本地识别第 2 / 3 页…"),
String(appLoc: "正在本地识别第 3 / 3 页…"),
String(appLoc: "提取指标 · 共 28 项"),
String(appLoc: "生成整体摘要…"),
]
var body: some View {
ZStack {
backgroundGradient.ignoresSafeArea()
VStack(spacing: 0) {
Spacer()
chip.padding(.bottom, 36)
Text("本地 AI · 正在解读")
.font(.system(size: 22, weight: .semibold))
.tracking(1)
.foregroundStyle(Color.white.opacity(0.95))
.padding(.bottom, 6)
Text("QWEN2.5-VL · ON-DEVICE · SME2")
.font(.system(size: 11, design: .monospaced))
.tracking(0.5)
.foregroundStyle(Color.white.opacity(0.55))
.padding(.bottom, 30)
lineList
.padding(.horizontal, 28)
speedBadge.padding(.top, 32)
Spacer()
Text("本地处理中 · 不会上传任何内容")
.font(.system(size: 10, design: .monospaced))
.tracking(0.5)
.foregroundStyle(Color.white.opacity(0.45))
.padding(.bottom, 30)
}
.padding(.horizontal, 28)
}
.preferredColorScheme(.dark)
.onAppear { startAnimations() }
}
private var backgroundGradient: some View {
RadialGradient(
colors: [
Color(red: 0.22, green: 0.21, blue: 0.18),
Color(red: 0.13, green: 0.12, blue: 0.10),
Color(red: 0.08, green: 0.075, blue: 0.06),
],
center: .init(x: 0.5, y: 0.3),
startRadius: 60,
endRadius: 700
)
}
private var chip: some View {
ZStack {
Circle()
.fill(Color(red: 0.93, green: 0.75, blue: 0.40).opacity(glow ? 0.18 : 0.0))
.frame(width: 176, height: 176)
.blur(radius: 30)
Circle()
.strokeBorder(Color.white.opacity(0.18),
style: StrokeStyle(lineWidth: 1, dash: [4, 4]))
.frame(width: 140, height: 140)
.rotationEffect(.degrees(rotate))
RoundedRectangle(cornerRadius: 22, style: .continuous)
.fill(
LinearGradient(
colors: [Color(red: 0.36, green: 0.34, blue: 0.30),
Color(red: 0.22, green: 0.21, blue: 0.18)],
startPoint: .topLeading, endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 22, style: .continuous)
.strokeBorder(Color.white.opacity(0.10), lineWidth: 1)
)
.frame(width: 96, height: 96)
.shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 12)
.overlay(ChipGlyph())
.overlay(alignment: .topTrailing) {
Circle()
.fill(Color(red: 0.95, green: 0.78, blue: 0.40))
.frame(width: 6, height: 6)
.opacity(pulse ? 1 : 0.35)
.shadow(color: Color(red: 0.95, green: 0.78, blue: 0.40), radius: 6)
.padding(10)
}
.scaleEffect(pulse ? 1.06 : 1.0)
.opacity(pulse ? 0.92 : 1.0)
}
}
private var lineList: some View {
VStack(alignment: .leading, spacing: 10) {
ForEach(Array(lineLabels.enumerated()), id: \.offset) { idx, label in
LineRow(
text: label,
done: step > idx + 1,
active: step == idx + 1,
isLast: idx == lineLabels.count - 1
)
.opacity(step >= idx + 1 ? 1 : 0)
.offset(y: step >= idx + 1 ? 0 : 6)
.animation(.easeOut(duration: 0.4).delay(Double(idx) * 0.05), value: step)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var speedBadge: some View {
Text(String(format: String(appLoc: "已处理 %.1fs · 比云端快 4.2×"), elapsed))
.font(.system(size: 10, design: .monospaced))
.tracking(0.6)
.foregroundStyle(Color.white.opacity(0.75))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Capsule().fill(Color.white.opacity(0.08)))
}
private func startAnimations() {
withAnimation(.easeInOut(duration: 2.0).repeatForever(autoreverses: true)) {
pulse.toggle()
}
withAnimation(.easeInOut(duration: 2.4).repeatForever(autoreverses: true)) {
glow.toggle()
}
withAnimation(.linear(duration: 14).repeatForever(autoreverses: false)) {
rotate = 360
}
Task {
for _ in 0..<lineLabels.count {
try? await Task.sleep(nanoseconds: 900_000_000)
await MainActor.run {
withAnimation { step += 1 }
elapsed += 0.9
}
}
try? await Task.sleep(nanoseconds: 600_000_000)
await MainActor.run { onComplete() }
}
}
}
private struct LineRow: View {
let text: String
let done: Bool
let active: Bool
let isLast: Bool
@State private var dotPulse = false
var body: some View {
HStack(spacing: 10) {
ZStack {
Circle()
.fill(done
? Color(red: 0.95, green: 0.78, blue: 0.40)
: Color.white.opacity(0.12))
if done {
Image(systemName: "checkmark")
.font(.system(size: 8, weight: .bold))
.foregroundStyle(Color(red: 0.10, green: 0.115, blue: 0.094))
}
}
.frame(width: 14, height: 14)
Text(text)
.font(.system(size: 13))
.foregroundStyle(done ? Color.white.opacity(0.95) : Color.white.opacity(0.45))
if active {
Spacer()
Text("···")
.font(.system(size: 10, design: .monospaced))
.foregroundStyle(Color.white.opacity(dotPulse ? 0.9 : 0.4))
.onAppear {
withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
dotPulse.toggle()
}
}
}
}
}
}
private struct ChipGlyph: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5, style: .continuous)
.strokeBorder(Color.white.opacity(0.8), lineWidth: 1.4)
.frame(width: 28, height: 28)
RoundedRectangle(cornerRadius: 2, style: .continuous)
.fill(Color(red: 0.95, green: 0.78, blue: 0.40).opacity(0.35))
.overlay(
RoundedRectangle(cornerRadius: 2, style: .continuous)
.strokeBorder(Color(red: 0.95, green: 0.78, blue: 0.40), lineWidth: 1)
)
.frame(width: 16, height: 16)
innerCross
outerPins
}
.frame(width: 56, height: 56)
}
private var innerCross: some View {
Canvas { ctx, size in
let amber = Color(red: 0.95, green: 0.78, blue: 0.40)
let stroke = GraphicsContext.Shading.color(amber)
let cx = size.width / 2
let cy = size.height / 2
let pairs: [(CGPoint, CGPoint)] = [
(CGPoint(x: cx, y: cy - 8), CGPoint(x: cx, y: cy - 4)),
(CGPoint(x: cx, y: cy + 4), CGPoint(x: cx, y: cy + 8)),
(CGPoint(x: cx - 8, y: cy), CGPoint(x: cx - 4, y: cy)),
(CGPoint(x: cx + 4, y: cy), CGPoint(x: cx + 8, y: cy)),
]
for (s, e) in pairs {
var p = Path()
p.move(to: s)
p.addLine(to: e)
ctx.stroke(p, with: stroke, style: StrokeStyle(lineWidth: 1, lineCap: .round))
}
}
.frame(width: 56, height: 56)
}
private var outerPins: some View {
Canvas { ctx, size in
let pinColor = GraphicsContext.Shading.color(Color.white.opacity(0.45))
let cx = size.width / 2
let cy = size.height / 2
let halfChip: CGFloat = 14
let outsideStart: CGFloat = 20
let outsideEnd: CGFloat = 26
let positions: [CGFloat] = [-8, 0, 8]
for offset in positions {
// top
var p = Path()
p.move(to: CGPoint(x: cx + offset, y: cy - outsideEnd))
p.addLine(to: CGPoint(x: cx + offset, y: cy - halfChip))
ctx.stroke(p, with: pinColor, style: StrokeStyle(lineWidth: 1, lineCap: .round))
// bottom
p = Path()
p.move(to: CGPoint(x: cx + offset, y: cy + halfChip))
p.addLine(to: CGPoint(x: cx + offset, y: cy + outsideEnd))
ctx.stroke(p, with: pinColor, style: StrokeStyle(lineWidth: 1, lineCap: .round))
// left
p = Path()
p.move(to: CGPoint(x: cx - outsideEnd, y: cy + offset))
p.addLine(to: CGPoint(x: cx - halfChip, y: cy + offset))
ctx.stroke(p, with: pinColor, style: StrokeStyle(lineWidth: 1, lineCap: .round))
// right
p = Path()
p.move(to: CGPoint(x: cx + halfChip, y: cy + offset))
p.addLine(to: CGPoint(x: cx + outsideStart + 2, y: cy + offset))
ctx.stroke(p, with: pinColor, style: StrokeStyle(lineWidth: 1, lineCap: .round))
}
}
.frame(width: 56, height: 56)
}
}
#Preview {
B4ProgressView(onComplete: {})
}