- 新增主行动卡片(今日复习/开始学习,基于优先级) - 新增本周摘要行(本周分钟/完成任务/复习卡片/连续天数) - 接入 ActivityService.streak()/summary() + ReviewService.dueCards() - 保留今日任务列表 + 每日思考题 - 移除旧的进度环卡片 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
209 lines
11 KiB
Swift
209 lines
11 KiB
Swift
import SwiftUI
|
|
|
|
struct StudyHomeView: View {
|
|
@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 {
|
|
Color.zxCanvas.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 16) {
|
|
// Header + streak
|
|
HStack {
|
|
Spacer()
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "flame.fill")
|
|
.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())
|
|
.overlay(Capsule().stroke(Color(hex: "#F97316", opacity: 0.2), lineWidth: 1))
|
|
}
|
|
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 4)
|
|
|
|
// 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: - Weekly Summary
|
|
weeklySummaryCard
|
|
|
|
// MARK: - Today Tasks
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
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)
|
|
}
|
|
}
|
|
ForEach($studyHomeVM.tasks) { $t in
|
|
routeForTask(t).map { route in
|
|
NavigationLink(value: route) {
|
|
ZXSTaskRowView(task: t) { t.d.toggle() }
|
|
}.foregroundColor(.primary)
|
|
}
|
|
}
|
|
}.padding(.horizontal, 20)
|
|
|
|
// MARK: - Daily Thinking
|
|
dailyThinkingCard.padding(.horizontal, 20)
|
|
|
|
Color.clear.frame(height: 100)
|
|
}
|
|
}
|
|
.scrollIndicators(.hidden)
|
|
.zxPullToRefresh { await refreshData() }
|
|
}
|
|
.task { await refreshData() }
|
|
.navigationDestination(for: Route.self) { $0.destination }
|
|
}
|
|
|
|
// MARK: - Main Action Card
|
|
|
|
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)
|
|
}
|
|
}
|
|
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))
|
|
}
|
|
.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)
|
|
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("解释「注意力机制」在 Transformer 中的作用,不能使用搜索,用你自己的话说。")
|
|
.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))
|
|
}
|
|
.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))
|
|
.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) } }
|
|
struct ZXSTaskRow: View { @Binding var task: ZXSTask
|
|
var body: some View { Button { task.d.toggle() } label: { ZXSTaskRowView(task: task) {} }.foregroundColor(.primary) }
|
|
}
|
|
struct ZXSTaskRowView: View { let task: ZXSTask; var action: () -> Void
|
|
var body: some View { HStack(spacing: 12) { Image(systemName: task.d ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(task.d ? Color.zxGreen : Color.zxF02)
|
|
VStack(alignment: .leading, spacing: 4) { Text(task.t).font(.system(size: 13, weight: .semibold)).foregroundColor(task.d ? Color.zxF04 : Color.zxF0).strikethrough(task.d); HStack(spacing: 8) { Text(task.tp).font(.system(size: 10, weight: .semibold)).foregroundColor(task.c).padding(.horizontal, 6).padding(.vertical, 1).background(task.c.opacity(0.12)).clipShape(Capsule()); Text("约 \(task.m) 分钟").font(.system(size: 10)).foregroundColor(Color(hex:"#F0F0FF",opacity:0.35)) } }
|
|
Spacer(); if !task.d { Image(systemName: "play.fill").font(.system(size: 14)).foregroundColor(.white).frame(width: 32, height: 32).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10)) } }
|
|
.padding(.horizontal, 16).padding(.vertical, 12).background(task.d ? Color.zxFill003 : Color.zxFill005).overlay(RoundedRectangle(cornerRadius: 14).stroke(task.d ? Color(hex: "#FFFFFF", opacity: 0.05) : Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).opacity(task.d ? 0.6 : 1).contentShape(Rectangle()).onTapGesture { action() }.zxPressable()
|
|
.accessibilityLabel("\(task.t), \(task.tp), 约\(task.m)分钟")
|
|
.accessibilityAddTraits(task.d ? .isSelected : [])
|
|
.accessibilityHint(task.d ? "已完成" : "双击开始学习") }
|
|
}
|