fix: add missing animations to progress rings, bars, and charts in learning views

- StudyHomeView: ring trim animation + bar width animation + week bar spring animations with staggered delay + numeric text transitions
- LearningSessionView: animatedProgress state with onChange driving smooth ring animation + background track ring
- ReviewCardView: progress bar width animation
- AnalysisHomeView: chart line trim animation + gradient fill fade-in

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-18 15:59:31 +08:00
parent 8c486c73ae
commit 9a4b4afaf4
4 changed files with 53 additions and 7 deletions

View File

@ -60,17 +60,41 @@ struct ZXStatBadge: View { let icon: String; let label: String; let value: Strin
struct ZXChartView: View {
let data: [(String, CGFloat)] = [("", 0.62), ("", 0.65), ("", 0.71), ("", 0.68), ("", 0.75), ("", 0.79), ("", 0.78)]
@State private var showChart = false
var body: some View {
VStack(spacing: 0) {
GeometryReader { g in
ZStack(alignment: .topLeading) {
// Gradient fill under the line
Path { path in let w = g.size.width / 7
for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } }
}.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
path.addLine(to: CGPoint(x: g.size.width, y: g.size.height))
path.addLine(to: CGPoint(x: w / 2, y: g.size.height))
path.closeSubpath()
}
.fill(
LinearGradient(
colors: [Color.zxPurple.opacity(0.2), Color.zxPurple.opacity(0.0)],
startPoint: .top, endPoint: .bottom
)
)
.opacity(showChart ? 1 : 0)
.animation(.easeOut(duration: 0.8).delay(0.3), value: showChart)
// Animated line
Path { path in let w = g.size.width / 7
for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height
if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } }
}
.trim(from: 0, to: showChart ? 1 : 0)
.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.animation(.easeOut(duration: 1.0), value: showChart)
}
}.frame(height: 100)
HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } }
}
.onAppear { showChart = true }
}
}

View File

@ -7,6 +7,7 @@ struct LearningSessionView: View {
let taskColor: Color
@State private var elapsed: TimeInterval = 0
@State private var animatedProgress: CGFloat = 0
@State private var isRunning = true
@State private var isPaused = false
@State private var showEndConfirm = false
@ -55,21 +56,41 @@ struct LearningSessionView: View {
private var timerCard: some View {
VStack(spacing: 16) {
ZStack {
// Background track
Circle()
.trim(from: 0, to: min(elapsed / 1800, 1))
.stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round))
.trim(from: 0, to: 1)
.stroke(Color.zxFill008, style: StrokeStyle(lineWidth: 8, lineCap: .round))
.rotationEffect(.degrees(-90))
.frame(width: 180, height: 180)
// Animated progress
Circle()
.trim(from: 0, to: animatedProgress)
.stroke(
AngularGradient(
colors: [Color.zxPurple, Color.zxAccent, Color.zxPurple],
center: .center
),
style: StrokeStyle(lineWidth: 8, lineCap: .round)
)
.rotationEffect(.degrees(-90))
.frame(width: 180, height: 180)
.animation(.easeInOut(duration: 0.5), value: animatedProgress)
VStack(spacing: 4) {
Text(formatTime(elapsed))
.font(.system(size: 36, weight: .black))
.foregroundColor(Color.zxF0)
.tracking(-1)
.contentTransition(.numericText())
Text("已学习")
.font(.system(size: 13, weight: .medium))
.foregroundColor(Color.zxF04)
}
}
.onChange(of: elapsed) { newElapsed in
withAnimation(.easeInOut(duration: 0.5)) {
animatedProgress = min(CGFloat(newElapsed) / 1800, 1)
}
}
HStack(spacing: 12) {
Button {
if isRunning { isPaused = true; isRunning = false }

View File

@ -67,6 +67,7 @@ struct ReviewCardView: View {
RoundedRectangle(cornerRadius: 2)
.fill(ZXGradient.progressBar)
.frame(width: max(3, CGFloat(current.count) / CGFloat(current.total) * (UIScreen.main.bounds.width - 40)), height: 3)
.animation(.easeInOut(duration: 0.5), value: current.count)
}
}
.padding(.horizontal, 20)

View File

@ -29,7 +29,7 @@ struct StudyHomeView: View {
}
}
VStack(alignment: .leading, spacing: 14) { Text("本周学习活跃").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
HStack(alignment: .bottom, spacing: 8) { ForEach(0..<7, id: \.self) { i in VStack(spacing: 8) { RoundedRectangle(cornerRadius: 6).fill(i == 6 ? Color.zxFill01 : Color(hex: "#7C6EFA", opacity: studyHomeVM.weekActivity[i] * 0.9 + 0.1)).frame(height: studyHomeVM.weekActivity[i] * 60); Text(studyHomeVM.dayLabels[i]).font(.system(size: 10, weight: i == 2 ? .bold : .regular)).foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) }.frame(maxWidth: .infinity) } }
HStack(alignment: .bottom, spacing: 8) { ForEach(0..<7, id: \.self) { i in VStack(spacing: 8) { RoundedRectangle(cornerRadius: 6).fill(i == 6 ? Color.zxFill01 : Color(hex: "#7C6EFA", opacity: studyHomeVM.weekActivity[i] * 0.9 + 0.1)).frame(height: studyHomeVM.weekActivity[i] * 60).animation(.spring(response: 0.6, dampingFraction: 0.7).delay(Double(i) * 0.05), value: studyHomeVM.weekActivity[i]); Text(studyHomeVM.dayLabels[i]).font(.system(size: 10, weight: i == 2 ? .bold : .regular)).foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) }.frame(maxWidth: .infinity) } }
HStack { Text("总计 3.5 小时").font(.system(size: 11)).foregroundColor(Color.zxF03); Spacer(); Text("日均 30 分钟").font(.system(size: 11)).foregroundColor(Color.zxF03) } }
.padding(.bottom, 120) }
.padding(.horizontal, 20) }
@ -39,9 +39,9 @@ struct StudyHomeView: View {
.task { await studyVM.loadSessions() }
}
private var pc: some View { let dn = studyHomeVM.doneCount; let pct = CGFloat(dn) / 5
return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer()
ZStack { Circle().trim(from: 0, to: pct).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 64, height: 64); Text("\(Int(pct * 100))%").font(.system(size: 14, weight: .heavy)).foregroundColor(Color.zxPurple) } }
ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.zxFill008).frame(height: 6); RoundedRectangle(cornerRadius: 3).fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)).frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6) }
return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0).contentTransition(.numericText()); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer()
ZStack { Circle().trim(from: 0, to: pct).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 64, height: 64).animation(.easeInOut(duration: 0.8), value: pct); Text("\(Int(pct * 100))%").font(.system(size: 14, weight: .heavy)).foregroundColor(Color.zxPurple).contentTransition(.numericText()) } }
ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.zxFill008).frame(height: 6); RoundedRectangle(cornerRadius: 3).fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)).frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6).animation(.easeInOut(duration: 0.6), value: pct) }
HStack { VStack(alignment: .leading, spacing: 2) { Text("\(dn * 12) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("已学").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(spacing: 2) { Text("\((5 - dn) * 11) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("剩余").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(alignment: .trailing, spacing: 2) { Text("+5 点").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("掌握").font(.system(size: 10)).foregroundColor(Color.zxF04) } } }
.padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }
}