Files
kangkang/康康/Features/Archive/B4ProgressView.swift
link2026 44ed01acf4 ```
refactor: 重命名项目名称从"体己"到"康康"

将整个项目的目录结构从"体己"重命名为"康康",包括所有源代码文件、
资源文件、测试文件以及Xcode项目配置文件。此更改涉及项目中所有的
文件路径和应用入口点(App/TijiApp.swift → App/KangkangApp.swift)。
```
2026-05-25 19:01:16 +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 = [
"正在本地识别第 1 / 3 页…",
"正在本地识别第 2 / 3 页…",
"正在本地识别第 3 / 3 页…",
"提取指标 · 共 28 项",
"生成整体摘要…",
]
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: "已处理 %.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: {})
}