替换 Xcode 默认模板: - 删除 ContentView/Item/__App - 新增 App/TijiApp(SwiftData ModelContainer)、RootView(3 Tab + RecordSheet) - DesignSystem:Tokens(色板/字体/圆角)+ Components(卡片/按钮/Chip) - Models:Indicator / Report / DiaryEntry @Model 初版 - Features:Home / Quick(A1-A3)/ Archive(B1-B5)/ Record / Trends / Me 静态 UI W2 AI 基座工作将在此基线上叠加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
6.9 KiB
Swift
199 lines
6.9 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)] {
|
||
[
|
||
("总胆固醇", "5.42", "3.10–5.18"),
|
||
("甘油三酯", "1.78", "0.45–1.70"),
|
||
("低密度脂蛋白", "3.84↑", "<3.40"),
|
||
("高密度脂蛋白", "1.21", ">1.04"),
|
||
("载脂蛋白 A1", "1.42", "1.00–1.60"),
|
||
("载脂蛋白 B", "1.04", "0.55–1.05"),
|
||
("谷丙转氨酶", "28", "9–50"),
|
||
("谷草转氨酶", "24", "15–40"),
|
||
("空腹血糖", "5.4", "3.9–6.1"),
|
||
("糖化血红蛋白", "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)
|
||
}
|
||
}
|