feat(ios): IOS-M0-05 Learning Activity 学习统计详情
- ActivityService 新增 trend()/streak()/recommendations() - ActivityViewModel 加载连续学习/趋势/推荐数据 - AnalysisHomeView 新增连续学习卡片(火焰+天数+最长记录) - AnalysisHomeView 新增学习推荐列表(type图标+标题+优先级标签) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6664612212
commit
0a8d873157
@ -493,6 +493,28 @@ struct AddSourceRequest: Codable {
|
||||
let title: String?
|
||||
}
|
||||
|
||||
// MARK: - Learning Activity
|
||||
|
||||
struct ActivityTrend: Codable {
|
||||
let date: String?
|
||||
let value: Double?
|
||||
let label: String?
|
||||
}
|
||||
|
||||
struct ActivityStreak: Codable {
|
||||
let currentStreak: Int?
|
||||
let longestStreak: Int?
|
||||
let lastActiveDate: String?
|
||||
}
|
||||
|
||||
struct ActivityRecommendation: Codable, Identifiable {
|
||||
var id: String { title ?? UUID().uuidString }
|
||||
let title: String?
|
||||
let description: String?
|
||||
let type: String?
|
||||
let priority: String?
|
||||
}
|
||||
|
||||
// MARK: - File Upload (COS presigned URL flow)
|
||||
|
||||
struct FileUploadUrlRequest: Codable {
|
||||
|
||||
@ -258,6 +258,18 @@ class ActivityService {
|
||||
func heatmap() async throws -> [String: Int] {
|
||||
return try await client.request("/activity/heatmap")
|
||||
}
|
||||
|
||||
func trend(days: Int = 7) async throws -> [ActivityTrend] {
|
||||
return try await client.request("/activity/trend?days=\(days)")
|
||||
}
|
||||
|
||||
func streak() async throws -> ActivityStreak {
|
||||
return try await client.request("/activity/streak")
|
||||
}
|
||||
|
||||
func recommendations() async throws -> [ActivityRecommendation] {
|
||||
return try await client.request("/activity/recommendations")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Feedback
|
||||
|
||||
@ -6,6 +6,9 @@ class ActivityViewModel: ObservableObject {
|
||||
@Published var summary: ActivitySummary?
|
||||
@Published var focusItems: [FocusItem] = []
|
||||
@Published var heatmap: [String: Int] = [:]
|
||||
@Published var streak: ActivityStreak?
|
||||
@Published var trends: [ActivityTrend] = []
|
||||
@Published var recommendations: [ActivityRecommendation] = []
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String?
|
||||
|
||||
@ -16,10 +19,12 @@ class ActivityViewModel: ObservableObject {
|
||||
async let s = ActivityService.shared.summary()
|
||||
async let f = FocusItemService.shared.list()
|
||||
async let h = ActivityService.shared.heatmap()
|
||||
let (summaryResult, focusResult, heatmapResult) = try await (s, f, h)
|
||||
summary = summaryResult
|
||||
focusItems = focusResult
|
||||
heatmap = heatmapResult
|
||||
async let st = ActivityService.shared.streak()
|
||||
async let t = ActivityService.shared.trend()
|
||||
async let r = ActivityService.shared.recommendations()
|
||||
let (summaryResult, focusResult, heatmapResult, streakResult, trendResult, recResult) = try await (s, f, h, st, t, r)
|
||||
summary = summaryResult; focusItems = focusResult; heatmap = heatmapResult
|
||||
streak = streakResult; trends = trendResult; recommendations = recResult
|
||||
} catch {
|
||||
if summary == nil { errorMessage = "加载分析数据失败" }
|
||||
}
|
||||
@ -31,10 +36,12 @@ class ActivityViewModel: ObservableObject {
|
||||
async let s = ActivityService.shared.summary()
|
||||
async let f = FocusItemService.shared.list()
|
||||
async let h = ActivityService.shared.heatmap()
|
||||
let (summaryResult, focusResult, heatmapResult) = try await (s, f, h)
|
||||
summary = summaryResult
|
||||
focusItems = focusResult
|
||||
heatmap = heatmapResult
|
||||
async let st = ActivityService.shared.streak()
|
||||
async let t = ActivityService.shared.trend()
|
||||
async let r = ActivityService.shared.recommendations()
|
||||
let (summaryResult, focusResult, heatmapResult, streakResult, trendResult, recResult) = try await (s, f, h, st, t, r)
|
||||
summary = summaryResult; focusItems = focusResult; heatmap = heatmapResult
|
||||
streak = streakResult; trends = trendResult; recommendations = recResult
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,31 @@ struct AnalysisHomeView: View {
|
||||
}
|
||||
}.padding(16).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
|
||||
if let streak = viewModel.streak {
|
||||
HStack(spacing: 12) {
|
||||
ZXStatBadge(icon: "flame.fill", label: "连续学习", value: "\(streak.currentStreak ?? 0) 天", trend: "", color: Color.zxOrange)
|
||||
ZXStatBadge(icon: "trophy.fill", label: "最长连续", value: "\(streak.longestStreak ?? 0) 天", trend: "", color: Color.zxAmber)
|
||||
ZXStatBadge(icon: "calendar", label: "最后活跃", value: streak.lastActiveDate.flatMap { String($0.prefix(10)) } ?? "-", trend: "", color: Color.zxPrimary)
|
||||
}
|
||||
}
|
||||
if !viewModel.recommendations.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("学习推荐").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
ForEach(viewModel.recommendations.prefix(3)) { rec in
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: rec.type == "review" ? "arrow.triangle.2.circlepath" : "lightbulb.fill")
|
||||
.font(.system(size: 14)).foregroundColor(Color.zxAccent)
|
||||
.frame(width: 32, height: 32).background(Color.zxAccent.opacity(0.1)).clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(rec.title ?? "").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF0)
|
||||
if let desc = rec.description { Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF04).lineLimit(1) }
|
||||
}
|
||||
Spacer()
|
||||
if let p = rec.priority { Text(p).font(.system(size: 10, weight: .bold)).foregroundColor(p == "high" ? Color.zxCoral : Color.zxAmber).padding(.horizontal, 6).padding(.vertical, 2).background((p == "high" ? Color.zxCoral : Color.zxAmber).opacity(0.1)).clipShape(Capsule()) }
|
||||
}.padding(10).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}.padding(16).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill").font(.system(size: 14)).foregroundColor(Color.zxYellow); Text("薄弱知识点").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) }; Spacer(); NavigationLink(value: Route.weakPoints) { Text("全部 \(viewModel.focusItems.count) 个").font(.system(size: 12)).foregroundColor(Color.zxPurple) }.accessibilityLabel("查看全部薄弱知识点") }
|
||||
ForEach(viewModel.focusItems.prefix(5)) { item in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user