diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift index 58ef84d..007fdfe 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -1,9 +1,12 @@ import SwiftUI struct StudyHomeView: View { - @StateObject private var studyVM = StudyViewModel() @StateObject private var studyHomeVM = StudyHomeViewModel() @StateObject private var reviewVM = ReviewViewModel() + @State private var streakDays = 0 + @State private var weeklyMinutes = 0 + @State private var reviewCount = 0 + @State private var hasTodayReview = false var body: some View { ZStack { @@ -11,232 +14,183 @@ struct StudyHomeView: View { ScrollView { VStack(spacing: 16) { - // MARK: - Header + // Header + streak HStack { Spacer() - if studyVM.isLoading { - ZXLoadingView(size: 22, lineWidth: 2) - } HStack(spacing: 4) { Image(systemName: "flame.fill") - .font(.system(size: 14)) - .foregroundColor(Color.zxOrange) - Text("14 天连续") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.zxOrange) + .font(.system(size: 14)).foregroundColor(Color.zxOrange) + Text("\(streakDays) 天连续") + .font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxOrange) } - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.zxOrangeBG(0.1)) - .clipShape(Capsule()) + .padding(.horizontal, 12).padding(.vertical, 6) + .background(Color.zxOrangeBG(0.1)).clipShape(Capsule()) .overlay(Capsule().stroke(Color(hex: "#F97316", opacity: 0.2), lineWidth: 1)) } - .padding(.horizontal, 20) - .padding(.top, 8) - .padding(.bottom, 4) + .padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 4) - // MARK: - Progress - progressCard + // MARK: - Main Action Card + if hasTodayReview { + mainActionCard( + icon: "arrow.triangle.2.circlepath", + title: "今日复习", + subtitle: "\(reviewCount) 张卡片待复习", + color: Color.zxPurple, + route: Route.reviewCard + ) + } else { + mainActionCard( + icon: "sparkles", + title: "开始学习", + subtitle: "从知识库挑选内容开始今天的进步", + color: Color.zxPrimary, + route: Route.studyHome + ) + } - // MARK: - Today's Tasks + // MARK: - Weekly Summary + weeklySummaryCard + + // MARK: - Today Tasks VStack(alignment: .leading, spacing: 12) { HStack { - Text("今日任务") - .font(.system(size: 15, weight: .bold)) - .foregroundColor(Color.zxF0) + Text("今日任务").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) Spacer() HStack(spacing: 4) { - Image(systemName: "calendar") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - Text("AI 自动排期") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) + Image(systemName: "calendar").font(.system(size: 12)).foregroundColor(Color.zxF04) + Text("AI 自动排期").font(.system(size: 12)).foregroundColor(Color.zxF04) } } ForEach($studyHomeVM.tasks) { $t in - if t.tp == "回忆测试" { - NavigationLink(value: Route.activeRecall) { + routeForTask(t).map { route in + NavigationLink(value: route) { ZXSTaskRowView(task: t) { t.d.toggle() } - } - .foregroundColor(.primary) - } else if t.tp == "费曼练习" { - NavigationLink(value: Route.aiChat) { - ZXSTaskRowView(task: t) { t.d.toggle() } - } - .foregroundColor(.primary) - } else if t.tp == "薄弱点" { - NavigationLink(value: Route.weakPoints) { - ZXSTaskRowView(task: t) { t.d.toggle() } - } - .foregroundColor(.primary) - } else if t.tp == "间隔复习" { - NavigationLink(value: Route.reviewCard) { - ZXSTaskRowView(task: t) { t.d.toggle() } - } - .foregroundColor(.primary) - } else { - NavigationLink(value: Route.learningSession(taskTitle: t.t, taskType: t.tp, taskColorHex: t.ch)) { - ZXSTaskRowView(task: t) { t.d.toggle() } - } - .foregroundColor(.primary) + }.foregroundColor(.primary) } } - } - .padding(.horizontal, 20) + }.padding(.horizontal, 20) // MARK: - Daily Thinking - dailyThinkingCard - .padding(.horizontal, 20) + dailyThinkingCard.padding(.horizontal, 20) - // Bottom spacer Color.clear.frame(height: 100) } } .scrollIndicators(.hidden) - .zxPullToRefresh { await studyVM.loadSessions() } + .zxPullToRefresh { await refreshData() } } - .task { await studyVM.loadSessions() } + .task { await refreshData() } .navigationDestination(for: Route.self) { $0.destination } } - // MARK: - Progress Card + // MARK: - Main Action Card - private var progressCard: 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) - .contentTransition(.numericText()) - Text("/ 5") - Text("个任务") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxF04) + private func mainActionCard(icon: String, title: String, subtitle: String, color: Color, route: Route) -> some View { + NavigationLink(value: route) { + VStack(spacing: 16) { + HStack { + VStack(alignment: .leading, spacing: 6) { + Text("今日主行动").font(.system(size: 11, weight: .semibold)).foregroundColor(Color.zxInkTertiary).textCase(.uppercase).tracking(0.5) + Text(title).font(.system(size: 24, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5) + Text(subtitle).font(.system(size: 13)).foregroundColor(Color.zxF04) + } + Spacer() + ZStack { + Circle().fill(color.opacity(0.12)).frame(width: 64, height: 64) + Image(systemName: icon).font(.system(size: 26)).foregroundColor(color) } } - 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) + HStack(spacing: 8) { + Text("开始").font(.system(size: 14, weight: .semibold)) + Image(systemName: "arrow.right").font(.system(size: 12, weight: .bold)) } + .foregroundColor(Color.zxOnPrimary).frame(maxWidth: .infinity).frame(height: 44) + .background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 12)) } + .padding(20) + .background(Color.zxSurfaceElevated) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxHairline, lineWidth: 0.5)) + .clipShape(RoundedRectangle(cornerRadius: 20)) } - .padding(16) - .background(ZXGradient.progressCard) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 20)) + .foregroundColor(.primary) .padding(.horizontal, 20) } + // MARK: - Weekly Summary + + private var weeklySummaryCard: some View { + HStack(spacing: 0) { + weeklyStat("\(weeklyMinutes)", "本周分钟", Color.zxOrange) + weeklyStat("\(studyHomeVM.doneCount)", "完成任务", Color.zxPurple) + weeklyStat("\(reviewCount)", "复习卡片", Color.zxTeal) + weeklyStat("\(streakDays)", "连续天数", Color.zxAmber) + } + .padding(.horizontal, 20) + } + + private func weeklyStat(_ value: String, _ label: String, _ color: Color) -> some View { + VStack(spacing: 4) { + Text(value).font(.system(size: 20, weight: .heavy)).foregroundColor(color) + Text(label).font(.system(size: 10)).foregroundColor(Color.zxF04) + }.frame(maxWidth: .infinity) + } + // MARK: - Daily Thinking Card private var dailyThinkingCard: some View { VStack(alignment: .leading, spacing: 14) { HStack { - Image(systemName: "sparkles") - .font(.system(size: 13)) - .foregroundColor(Color.zxPrimary) - .frame(width: 30, height: 30) - .background(Color.zxPrimarySoft) - .clipShape(RoundedRectangle(cornerRadius: ZXRadius.sm)) - - Text("每日思考题") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color.zxInkPrimary) - + Image(systemName: "sparkles").font(.system(size: 13)).foregroundColor(Color.zxPrimary) + .frame(width: 30, height: 30).background(Color.zxPrimarySoft).clipShape(RoundedRectangle(cornerRadius: ZXRadius.sm)) + Text("每日思考题").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxInkPrimary) Spacer() - - Text("待回答") - .font(.system(size: 11, weight: .semibold)) - .foregroundColor(Color.zxAmberDeep) - .padding(.horizontal, 8) - .padding(.vertical, 3) - .background(Color.zxAmberSoft) - .clipShape(RoundedRectangle(cornerRadius: ZXRadius.xs)) + Text("待回答").font(.system(size: 11, weight: .semibold)).foregroundColor(Color.zxAmberDeep) + .padding(.horizontal, 8).padding(.vertical, 3).background(Color.zxAmberSoft).clipShape(RoundedRectangle(cornerRadius: ZXRadius.xs)) } - Text("解释「注意力机制」在 Transformer 中的作用,不能使用搜索,用你自己的话说。") - .font(.system(size: 15, weight: .regular)) - .foregroundColor(Color.zxInkPrimary) - .lineSpacing(6) - + .font(.system(size: 15, weight: .regular)).foregroundColor(Color.zxInkPrimary).lineSpacing(6) NavigationLink(value: Route.dailyThinking) { HStack(spacing: 8) { - Text("开始回答") - .font(.system(size: 14, weight: .semibold)) - Image(systemName: "arrow.right") - .font(.system(size: 12, weight: .semibold)) + Text("开始回答").font(.system(size: 14, weight: .semibold)) + Image(systemName: "arrow.right").font(.system(size: 12, weight: .semibold)) } - .foregroundColor(Color.zxOnPrimary) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(ZXGradient.brand) - .clipShape(RoundedRectangle(cornerRadius: ZXRadius.md)) - } - .accessibilityLabel("开始回答每日思考题") + .foregroundColor(Color.zxOnPrimary).frame(maxWidth: .infinity).frame(height: 48) + .background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: ZXRadius.md)) + }.accessibilityLabel("开始回答每日思考题") } .padding(16) .background(Color.zxSurfaceElevated) - .overlay( - RoundedRectangle(cornerRadius: ZXRadius.lg) - .stroke(Color.zxHairline, lineWidth: 0.5) - ) + .overlay(RoundedRectangle(cornerRadius: ZXRadius.lg).stroke(Color.zxHairline, lineWidth: 0.5)) .clipShape(RoundedRectangle(cornerRadius: ZXRadius.lg)) } + // MARK: - Helpers + + private func routeForTask(_ t: ZXSTask) -> Route? { + switch t.tp { + case "回忆测试": return .activeRecall + case "费曼练习": return .aiChat + case "薄弱点": return .weakPoints + case "间隔复习": return .reviewCard + case _ where !t.t.isEmpty: return .learningSession(taskTitle: t.t, taskType: t.tp, taskColorHex: t.ch) + default: return nil + } + } + + private func refreshData() async { + do { + let streak = try? await ActivityService.shared.streak() + streakDays = streak?.currentStreak ?? 0 + + let summary = try? await ActivityService.shared.summary() + weeklyMinutes = summary?.totalMinutes ?? 0 + reviewCount = summary?.totalCardsReviewed ?? 0 + + let dueCards = try? await ReviewService.shared.dueCards() + hasTodayReview = (dueCards?.count ?? 0) > 0 + reviewCount = max(reviewCount, dueCards?.count ?? 0) + } + } } struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let ch: String; let m: Int; var d: Bool; var c: Color { Color(hex: ch) } }