diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift index c76c688..b0ad0d4 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -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 { diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift index 87d032e..6d7dbaa 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -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 diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift b/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift index 97c1101..d91004e 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift @@ -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 {} } } diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index b23955e..6435957 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -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