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?
|
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)
|
// MARK: - File Upload (COS presigned URL flow)
|
||||||
|
|
||||||
struct FileUploadUrlRequest: Codable {
|
struct FileUploadUrlRequest: Codable {
|
||||||
|
|||||||
@ -258,6 +258,18 @@ class ActivityService {
|
|||||||
func heatmap() async throws -> [String: Int] {
|
func heatmap() async throws -> [String: Int] {
|
||||||
return try await client.request("/activity/heatmap")
|
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
|
// MARK: - Feedback
|
||||||
|
|||||||
@ -6,6 +6,9 @@ class ActivityViewModel: ObservableObject {
|
|||||||
@Published var summary: ActivitySummary?
|
@Published var summary: ActivitySummary?
|
||||||
@Published var focusItems: [FocusItem] = []
|
@Published var focusItems: [FocusItem] = []
|
||||||
@Published var heatmap: [String: Int] = [:]
|
@Published var heatmap: [String: Int] = [:]
|
||||||
|
@Published var streak: ActivityStreak?
|
||||||
|
@Published var trends: [ActivityTrend] = []
|
||||||
|
@Published var recommendations: [ActivityRecommendation] = []
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
|
|
||||||
@ -16,10 +19,12 @@ class ActivityViewModel: ObservableObject {
|
|||||||
async let s = ActivityService.shared.summary()
|
async let s = ActivityService.shared.summary()
|
||||||
async let f = FocusItemService.shared.list()
|
async let f = FocusItemService.shared.list()
|
||||||
async let h = ActivityService.shared.heatmap()
|
async let h = ActivityService.shared.heatmap()
|
||||||
let (summaryResult, focusResult, heatmapResult) = try await (s, f, h)
|
async let st = ActivityService.shared.streak()
|
||||||
summary = summaryResult
|
async let t = ActivityService.shared.trend()
|
||||||
focusItems = focusResult
|
async let r = ActivityService.shared.recommendations()
|
||||||
heatmap = heatmapResult
|
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 {
|
} catch {
|
||||||
if summary == nil { errorMessage = "加载分析数据失败" }
|
if summary == nil { errorMessage = "加载分析数据失败" }
|
||||||
}
|
}
|
||||||
@ -31,10 +36,12 @@ class ActivityViewModel: ObservableObject {
|
|||||||
async let s = ActivityService.shared.summary()
|
async let s = ActivityService.shared.summary()
|
||||||
async let f = FocusItemService.shared.list()
|
async let f = FocusItemService.shared.list()
|
||||||
async let h = ActivityService.shared.heatmap()
|
async let h = ActivityService.shared.heatmap()
|
||||||
let (summaryResult, focusResult, heatmapResult) = try await (s, f, h)
|
async let st = ActivityService.shared.streak()
|
||||||
summary = summaryResult
|
async let t = ActivityService.shared.trend()
|
||||||
focusItems = focusResult
|
async let r = ActivityService.shared.recommendations()
|
||||||
heatmap = heatmapResult
|
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 {}
|
} 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))
|
}.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) {
|
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("查看全部薄弱知识点") }
|
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
|
ForEach(viewModel.focusItems.prefix(5)) { item in
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user