refactor(ios): migrate NavigationLink to value-based routing, add Dynamic Type support, fix gesture conflicts

- Replace all deprecated NavigationLink(destination:) with NavigationLink(value: Route)
- Add Route enum with navigationDestination mapping in new Core/Navigation/
- Extract 7 new sub-page files (AIChatPage, AIFeedbackPageView, RecallTestPage, WeakPointsPage, FeedbackFormView, GoalSettingDetailView, MethodPreferenceView)
- Add @ScaledMetric-based zxFontScaled modifier for Dynamic Type
- Fix ZXPressModifier gesture conflict with ScrollView using onLongPressGesture
- Enlarge touch targets from 36pt to 44pt
- Add accessibility labels to TextField and other controls

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-19 15:21:36 +08:00
parent 9a4b4afaf4
commit a05dd09902
25 changed files with 649 additions and 461 deletions

View File

@ -112,9 +112,9 @@ struct WelcomePage: View {
HStack(spacing: 6) { Image(systemName: "sparkles").font(.system(size: 12)); Text("AI 驱动").font(.system(size: 12, weight: .semibold)) }.foregroundColor(Color.zxAccent).padding(.horizontal, 12).padding(.vertical, 6).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(Capsule())
Text("用 AI 重新定义\n你的学习方式").font(.system(size: 32, weight: .heavy)).tracking(-0.8).lineSpacing(4)
VStack(spacing: 10) {
FeatureRow(icon: "🧠", title: "主动回忆", desc: "基于间隔重复的智能复习")
FeatureRow(icon: "🎤", title: "费曼解释", desc: "用自己的话讲出来")
FeatureRow(icon: "📊", title: "AI 分析", desc: "发现知识薄弱点")
FeatureRow(icon: "brain.head.profile", title: "主动回忆", desc: "基于间隔重复的智能复习")
FeatureRow(icon: "mic.fill", title: "费曼解释", desc: "用自己的话讲出来")
FeatureRow(icon: "chart.bar.fill", title: "AI 分析", desc: "发现知识薄弱点")
}
}
VStack(spacing: 12) {
@ -131,7 +131,7 @@ struct FeatureRow: View {
let icon: String; let title: String; let desc: String
var body: some View {
HStack(spacing: 14) {
Text(icon).font(.system(size: 20)).frame(width: 40, height: 40).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12))
Image(systemName: icon).font(.system(size: 18)).foregroundColor(Color.zxPurple).frame(width: 40, height: 40).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }
}.padding(.horizontal, 16).padding(.vertical, 14).background(Color.zxFill003).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 16))
}

View File

@ -23,10 +23,10 @@ struct ZXTabBar: View {
@Binding var active: String
private let items = [
("ai", "AI", "brain.head.profile"),
("library", "知识库", "books.vertical.fill"),
("study", "学习", "bolt.fill"),
("analysis", "分析", "chart.bar.fill"),
("profile", "我的", "person.fill"),
("library", "知识库", "books.vertical"),
("study", "学习", "bolt"),
("analysis", "分析", "chart.bar"),
("profile", "我的", "person"),
]
var body: some View {
@ -37,15 +37,15 @@ struct ZXTabBar: View {
active = item.0
} label: {
VStack(spacing: 4) {
ZStack {
ZStack(alignment: .top) {
if on {
Circle()
.fill(Color.zxPurple.opacity(0.2))
.frame(width: 28, height: 28)
.scaleEffect(1.4)
Capsule()
.fill(Color.zxPurple)
.frame(width: 20, height: 3)
.offset(y: -4)
}
Image(systemName: item.2)
.font(.system(size: 22, weight: on ? .semibold : .regular))
Image(systemName: on ? "\(item.2).fill" : item.2)
.font(.system(size: 22))
.foregroundColor(on ? Color.zxPurple : Color.zxF03)
}
Text(item.1)
@ -139,6 +139,5 @@ struct ZXAIInputBar: View {
.background(.ultraThinMaterial).background(Color.zxFill004)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder008, lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding(.horizontal, 20).padding(.bottom, 34)
}
}

View File

@ -52,14 +52,14 @@ extension Color {
//
static let zxF0 = Color(light: Color(hex: "#1A1A2E"), dark: Color(hex: "#F0F0FF"))
static let zxF05 = Color(light: Color(hex: "#1A1A2E", opacity: 0.5), dark: Color(hex: "#F0F0FF", opacity: 0.5))
static let zxF04 = Color(light: Color(hex: "#1A1A2E", opacity: 0.4), dark: Color(hex: "#F0F0FF", opacity: 0.4))
static let zxF03 = Color(light: Color(hex: "#1A1A2E", opacity: 0.3), dark: Color(hex: "#F0F0FF", opacity: 0.3))
static let zxF05 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.5))
static let zxF04 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.4))
static let zxF03 = Color(light: Color(hex: "#64748B"), dark: Color(hex: "#F0F0FF", opacity: 0.3))
static let zxF007 = Color(light: Color(hex: "#1A1A2E", opacity: 0.7), dark: Color(hex: "#F0F0FF", opacity: 0.7))
static let zxF006 = Color(light: Color(hex: "#1A1A2E", opacity: 0.6), dark: Color(hex: "#F0F0FF", opacity: 0.6))
static let zxF0045 = Color(light: Color(hex: "#1A1A2E", opacity: 0.45), dark: Color(hex: "#F0F0FF", opacity: 0.45))
static let zxF035 = Color(light: Color(hex: "#1A1A2E", opacity: 0.35), dark: Color(hex: "#F0F0FF", opacity: 0.35))
static let zxF02 = Color(light: Color(hex: "#1A1A2E", opacity: 0.2), dark: Color(hex: "#F0F0FF", opacity: 0.2))
static let zxF006 = Color(light: Color(hex: "#334155"), dark: Color(hex: "#F0F0FF", opacity: 0.6))
static let zxF0045 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.45))
static let zxF035 = Color(light: Color(hex: "#586A82"), dark: Color(hex: "#F0F0FF", opacity: 0.35))
static let zxF02 = Color(light: Color(hex: "#64748B"), dark: Color(hex: "#F0F0FF", opacity: 0.2))
//
static let zxPurple = Color(hex: "#7C6EFA")
@ -72,9 +72,9 @@ extension Color {
static let zxCyan = Color(hex: "#4ECDC4")
// /线
static let zxBorder008 = Color(light: Color(hex: "#000000", opacity: 0.08), dark: Color(hex: "#FFFFFF", opacity: 0.08))
static let zxBorder006 = Color(light: Color(hex: "#000000", opacity: 0.06), dark: Color(hex: "#FFFFFF", opacity: 0.06))
static let zxBorder004 = Color(light: Color(hex: "#000000", opacity: 0.04), dark: Color(hex: "#FFFFFF", opacity: 0.04))
static let zxBorder008 = Color(light: Color(hex: "#000000", opacity: 0.12), dark: Color(hex: "#FFFFFF", opacity: 0.08))
static let zxBorder006 = Color(light: Color(hex: "#000000", opacity: 0.10), dark: Color(hex: "#FFFFFF", opacity: 0.06))
static let zxBorder004 = Color(light: Color(hex: "#000000", opacity: 0.08), dark: Color(hex: "#FFFFFF", opacity: 0.04))
static let zxBorder01 = Color(light: Color(hex: "#000000", opacity: 0.10), dark: Color(hex: "#FFFFFF", opacity: 0.10))
static let zxBorder015 = Color(light: Color(hex: "#000000", opacity: 0.15), dark: Color(hex: "#FFFFFF", opacity: 0.15))
@ -273,3 +273,25 @@ enum ZXFont {
// description: 12, regular, 0.4
static let description = (size: CGFloat(12), weight: Font.Weight.regular, spacing: CGFloat(0.4))
}
// MARK: - Dynamic Type Scaled Font
struct ZXScaledFont: ViewModifier {
@ScaledMetric var size: CGFloat
let weight: Font.Weight
init(size: CGFloat, weight: Font.Weight = .regular) {
_size = ScaledMetric(wrappedValue: size, relativeTo: .body)
self.weight = weight
}
func body(content: Content) -> some View {
content.font(.system(size: size, weight: weight))
}
}
extension View {
func zxFontScaled(size: CGFloat, weight: Font.Weight = .regular) -> some View {
modifier(ZXScaledFont(size: size, weight: weight))
}
}

View File

@ -28,11 +28,9 @@ struct ZXPressModifier: ViewModifier {
.scaleEffect(pressed ? 0.96 : 1.0)
.opacity(pressed ? 0.8 : 1.0)
.animation(.easeOut(duration: 0.12), value: pressed)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in pressed = true }
.onEnded { _ in pressed = false }
)
.onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in
pressed = pressing
}, perform: {})
.sensoryFeedback(.impact(weight: .light), trigger: pressed)
}
}
@ -72,6 +70,7 @@ struct ZXThinkingOverlay: View {
self.message = message
}
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var show = false
var body: some View {
@ -79,7 +78,6 @@ struct ZXThinkingOverlay: View {
Color.black.opacity(0.4).ignoresSafeArea()
VStack(spacing: 20) {
// Animated brain
ZStack {
Circle()
.fill(RadialGradient(
@ -87,8 +85,8 @@ struct ZXThinkingOverlay: View {
center: .center, startRadius: 8, endRadius: 32
))
.frame(width: 64, height: 64)
.scaleEffect(show ? 1.3 : 0.8)
.animation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: show)
.scaleEffect(show && !reduceMotion ? 1.3 : 1.0)
.animation(reduceMotion ? nil : .easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: show)
Image(systemName: "brain.head.profile")
.font(.system(size: 28))
@ -99,7 +97,8 @@ struct ZXThinkingOverlay: View {
Text(message)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.white)
ZXDotLoader(color: .white)
if !reduceMotion { ZXDotLoader(color: .white) }
else { Text("处理中…").font(.system(size: 12)).foregroundColor(.white.opacity(0.7)) }
}
}
.padding(32)
@ -119,6 +118,7 @@ struct ZXCelebrationView: View {
let subtitle: String
let onDismiss: () -> Void
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var particles: [ConfettiParticle] = []
@State private var showContent = false
@ -127,14 +127,15 @@ struct ZXCelebrationView: View {
Color.black.opacity(0.5).ignoresSafeArea()
.onTapGesture { dismiss() }
// Particles
ForEach(particles) { p in
Circle()
.fill(p.color)
.frame(width: p.size, height: p.size)
.position(x: p.x, y: p.y)
.opacity(p.opacity)
.scaleEffect(p.scale)
if !reduceMotion {
ForEach(particles) { p in
Circle()
.fill(p.color)
.frame(width: p.size, height: p.size)
.position(x: p.x, y: p.y)
.opacity(p.opacity)
.scaleEffect(p.scale)
}
}
// Content card
@ -152,8 +153,8 @@ struct ZXCelebrationView: View {
.font(.system(size: 36))
.foregroundColor(.white)
}
.scaleEffect(showContent ? 1 : 0.5)
.animation(.spring(response: 0.5, dampingFraction: 0.6).delay(0.2), value: showContent)
.scaleEffect(showContent && !reduceMotion ? 1 : 1)
.animation(reduceMotion ? nil : .spring(response: 0.5, dampingFraction: 0.6).delay(0.2), value: showContent)
VStack(spacing: 6) {
Text(title)
@ -163,8 +164,8 @@ struct ZXCelebrationView: View {
.font(.system(size: 14))
.foregroundColor(Color(hex: "#F0F0FF", opacity: 0.6))
}
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 20)
.opacity(reduceMotion ? 1 : (showContent ? 1 : 0))
.offset(y: reduceMotion ? 0 : (showContent ? 0 : 20))
Button(action: dismiss) {
Text("继续学习")
@ -180,7 +181,7 @@ struct ZXCelebrationView: View {
)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.opacity(showContent ? 1 : 0)
.opacity(reduceMotion ? 1 : (showContent ? 1 : 0))
}
.padding(28)
.background(
@ -191,7 +192,7 @@ struct ZXCelebrationView: View {
}
.onAppear {
showContent = true
launchConfetti()
if !reduceMotion { launchConfetti() }
}
}
@ -207,7 +208,7 @@ struct ZXCelebrationView: View {
Color(hex: "#A78BFA"), Color(hex: "#34D399"),
Color(hex: "#F59E0B"), Color(hex: "#4ECDC4")]
var ps: [ConfettiParticle] = []
for i in 0..<60 {
for i in 0..<36 {
let delay = Double(i) * 0.015
let x = CGFloat.random(in: 0...UIScreen.main.bounds.width)
let endY = CGFloat.random(in: 80...UIScreen.main.bounds.height * 0.7)
@ -249,13 +250,18 @@ private struct ConfettiParticle: Identifiable {
struct ZXAIAnalysisProgress: View {
let steps: [String]
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var currentStep = 0
@State private var progress: CGFloat = 0
var body: some View {
VStack(spacing: 24) {
ZStack {
ZXLoadingView(size: 48, lineWidth: 3)
if reduceMotion {
ProgressView().scaleEffect(1.5)
} else {
ZXLoadingView(size: 48, lineWidth: 3)
}
}
VStack(spacing: 4) {
@ -288,13 +294,18 @@ struct ZXAIAnalysisProgress: View {
)
.padding(.horizontal, 40)
.onAppear {
var delay: TimeInterval = 0.8
for i in 0..<steps.count {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
currentStep = i
progress = CGFloat(i + 1) / CGFloat(steps.count)
if reduceMotion {
currentStep = steps.count - 1
progress = 1.0
} else {
var delay: TimeInterval = 0.8
for i in 0..<steps.count {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
currentStep = i
progress = CGFloat(i + 1) / CGFloat(steps.count)
}
delay += 1.2
}
delay += 1.2
}
}
}

View File

@ -75,16 +75,22 @@ struct ZXLoadingOverlay: View {
// MARK: - Skeleton shimmer (for placeholder loading)
struct ZXShimmer: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var phase: CGFloat = -0.5
private var shimmerColor: Color {
colorScheme == .dark ? Color.white.opacity(0.06) : Color.black.opacity(0.06)
}
func body(content: Content) -> some View {
content
.overlay(
LinearGradient(
colors: [
Color.white.opacity(0),
Color.white.opacity(0.06),
Color.white.opacity(0),
shimmerColor.opacity(0),
shimmerColor,
shimmerColor.opacity(0),
],
startPoint: .leading,
endPoint: .trailing
@ -92,10 +98,10 @@ struct ZXShimmer: ViewModifier {
.rotationEffect(.degrees(15))
.scaleEffect(2)
.offset(x: phase * 400)
.animation(.linear(duration: 1.5).repeatForever(autoreverses: false), value: phase)
.animation(reduceMotion ? nil : .linear(duration: 1.5).repeatForever(autoreverses: false), value: phase)
)
.clipped()
.onAppear { phase = 1.5 }
.onAppear { phase = reduceMotion ? 0 : 1.5 }
}
}

View File

@ -176,7 +176,7 @@ struct CreateKnowledgeBaseRequest: Codable {
// MARK: - Knowledge Items (matches Prisma KnowledgeItem model)
struct KnowledgeItem: Codable, Identifiable {
struct KnowledgeItem: Codable, Identifiable, Hashable {
let id: String
let userId: String?
let knowledgeBaseId: String?

View File

@ -0,0 +1,65 @@
import SwiftUI
enum Route: Hashable {
// AI
case aiChat
case dailyThinking
case aiFeedback
case activeRecall
case weakPoints
case reviewCard
// Library
case librarySearch
case libraryDetail(knowledgeBaseId: String)
case libraryImport
case libraryCreate
case addKnowledge(knowledgeBaseId: String)
case knowledgeDetail(item: KnowledgeItem)
case editKnowledge(item: KnowledgeItem)
// Study
case learningSession(taskTitle: String, taskType: String, taskColorHex: String)
case studyHome
// Profile
case notificationList
case settings
case goalSetting
case methodPreference
case feedbackForm
case editProfile
}
extension Route {
@ViewBuilder
var destination: some View {
switch self {
case .aiChat: AIChatPage()
case .dailyThinking: DailyThinkingPage()
case .aiFeedback: AIFeedbackPageView()
case .activeRecall: ActiveRecallView()
case .weakPoints: WeakPointsPage()
case .reviewCard: ReviewCardView()
case .librarySearch: LibrarySearchView()
case .libraryDetail(let id): LibraryDetailPage(knowledgeBaseId: id)
case .libraryImport: ImportPage()
case .libraryCreate: CreateLibraryPage()
case .addKnowledge(let id): AddKnowledgePage(knowledgeBaseId: id)
case .knowledgeDetail(let item): KnowledgeDetailPage(item: item)
case .editKnowledge(let item): EditKnowledgePage(item: item)
case .learningSession(let title, let type, let colorHex):
LearningSessionView(taskTitle: title, taskType: type, taskColor: Color(hex: colorHex))
case .studyHome: StudyHomeView()
case .notificationList: NotificationListView()
case .settings: SettingsView()
case .goalSetting: GoalSettingDetailView()
case .methodPreference: MethodPreferenceView()
case .feedbackForm: FeedbackFormView()
case .editProfile: EditProfilePage()
}
}
}

View File

@ -0,0 +1,66 @@
import SwiftUI
struct AIChatPage: View {
@StateObject private var vm = AIChatViewModel()
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
VStack(spacing: 0) {
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 16) {
ForEach(vm.messages) { m in
chatBubble(m)
.id(m.id)
}
if vm.isSending {
HStack(spacing: 8) {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28)
.background(Color(hex: "#7C6EFA", opacity: 0.15))
.clipShape(Circle())
ZXDotLoader(color: Color.zxPurple)
.padding(.leading, 4)
Spacer()
}
.padding(.horizontal, 20)
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
}
.scrollIndicators(.hidden)
.onChange(of: vm.messages.count) { _ in
withAnimation { proxy.scrollTo(vm.messages.last?.id) }
}
}
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() })
.padding(.horizontal, 20).padding(.bottom, 34)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
private func chatBubble(_ m: AIMessage) -> some View {
HStack(alignment: .top, spacing: 8) {
if m.role == .ai {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28)
.background(Color(hex: "#7C6EFA", opacity: 0.15))
.clipShape(Circle())
}
Text(m.content).zxFontScaled(size: 14)
.foregroundColor(m.role == .user ? .white : Color.zxF007)
.padding(12)
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
.clipShape(RoundedRectangle(cornerRadius: 16))
if m.role == .user {
Circle().frame(width: 28, height: 28)
.foregroundColor(Color.zxPurpleBG(0.2))
.overlay(Text("").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
}
}
.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
}
}

View File

@ -0,0 +1,107 @@
import SwiftUI
struct AIFeedbackPageView: View {
@State private var navigateToChat = false
@State private var isAnalyzing = true
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
if isAnalyzing {
ZXAIAnalysisProgress(steps: [
"解析你的回答结构…",
"对比知识库标准答案…",
"评估概念理解深度…",
"生成个性化反馈…"
])
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
withAnimation(.easeOut(duration: 0.4)) { isAnalyzing = false }
}
}
} else {
ScrollView {
VStack(spacing: 16) {
HStack(spacing: 20) {
ZStack {
Circle().trim(from: 0, to: 0.78).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 10, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 80, height: 80)
VStack(spacing: 0) {
Text("78").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxPurple)
Text("/ 100").font(.system(size: 9)).foregroundColor(Color.zxF04)
}
}
VStack(alignment: .leading, spacing: 2) {
Text("良好掌握").zxFontScaled(size: 18, weight: .heavy).foregroundColor(Color.zxF0)
Text("理解核心概念,但缺少理论深度和解决方案").zxFontScaled(size: 12).foregroundColor(Color.zxF0045).lineSpacing(4)
}
Spacer()
}
.padding(20)
.background(ZXGradient.feedbackScore)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1))
VStack(alignment: .leading, spacing: 8) {
Text("你的回答").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF04)
Text("过拟合就像一个学生只会「死记硬背」考题,而不是真正理解知识…").zxFontScaled(size: 13).foregroundColor(Color.zxF007).lineSpacing(6).padding(14).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1))
}
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill").foregroundColor(Color.zxGreen)
Text("答对的部分").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0)
}
ForEach(["正确识别出过拟合是\"记住训练数据\"而非\"学习规律\"", "使用了死记硬背类比,方向正确且贴切"], id: \.self) { s in
HStack(alignment: .top, spacing: 12) {
Circle().fill(Color.zxGreen).frame(width: 6, height: 6).padding(.top, 6)
Text(s).zxFontScaled(size: 13).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.75)).lineSpacing(4)
}
.padding(12)
.background(Color(hex: "#34D399", opacity: 0.07))
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color(hex: "#34D399", opacity: 0.18), lineWidth: 1))
}
}
NavigationLink(value: Route.studyHome) {
Label("加入待巩固,安排间隔复习", systemImage: "bolt.fill")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity).frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: Color(hex: "#7C6EFA", opacity: 0.3), radius: 24)
}
HStack(spacing: 12) {
NavigationLink(value: Route.aiChat) {
HStack(spacing: 4) {
Text("深入提问").font(.system(size: 13))
Image(systemName: "chevron.right").font(.system(size: 14))
}
.foregroundColor(Color.zxF05)
.frame(maxWidth: .infinity).frame(height: 44)
.background(Color.zxFill005)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
NavigationLink(value: Route.dailyThinking) {
HStack(spacing: 4) {
Text("再来一题").font(.system(size: 13))
Image(systemName: "chevron.right").font(.system(size: 14))
}
.foregroundColor(Color.zxF05)
.frame(maxWidth: .infinity).frame(height: 44)
.background(Color.zxFill005)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}
.scrollIndicators(.hidden)
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -31,7 +31,7 @@ struct AIHomeView: View {
.fill(serverStatus == .online ? Color.zxGreen
: serverStatus == .checking ? Color.zxYellow
: Color.zxRed)
.frame(width: 6, height: 6)
.frame(width: 8, height: 8)
Text(serverStatus == .online ? serverMessage
: serverStatus == .checking ? "检测中…"
: "离线")
@ -43,7 +43,7 @@ struct AIHomeView: View {
.padding(.horizontal, 8).padding(.vertical, 4)
.background(Color.zxFill005).clipShape(Capsule())
ZXIconBtn(icon:"arrow.clockwise",size:36){ Task { await checkServer() } }
ZXIconBtn(icon:"arrow.clockwise",size:44){ Task { await checkServer() } }
}
.padding(.horizontal,20).padding(.top,ZXSpacing.statusBarH+16).padding(.bottom,12)
@ -63,6 +63,7 @@ struct AIHomeView: View {
NavigationLink(destination: AIChatPage(), isActive: $navigateToChat) { EmptyView() }
}
.navigationDestination(for: Route.self) { $0.destination }
.task { await checkServer() }
}
@ -90,8 +91,8 @@ struct AIHomeView: View {
.padding(.horizontal,8).padding(.vertical,2).background(Color(hex:"#F97316",opacity:0.2)).clipShape(Capsule())
}
Text("解释\"注意力机制\"在 Transformer 中的作用,不能使用搜索,用你自己的话说。")
.font(.system(size:14,weight:.medium)).foregroundColor(Color.zxF0).lineSpacing(4)
NavigationLink(destination: DailyThinkingPage()) {
.zxFontScaled(size:14,weight:.medium).foregroundColor(Color.zxF0).lineSpacing(4)
NavigationLink(value: Route.dailyThinking) {
Text("开始回答").font(.system(size:13,weight:.bold)).foregroundColor(.white)
.frame(maxWidth:.infinity).frame(height:42)
.background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius:12))
@ -106,17 +107,17 @@ struct AIHomeView: View {
private var quickActions: some View {
HStack(spacing:12){
NavigationLink(destination: ActiveRecallView()) {
ZXQuickAction(emoji:"🧠",label:"生成\n回忆测试")
NavigationLink(value: Route.activeRecall) {
ZXQuickAction(icon:"brain.head.profile",label:"生成\n回忆测试")
}.foregroundColor(.primary)
NavigationLink(destination: WeakPointsPage()) {
ZXQuickAction(emoji:"🔍",label:"分析\n薄弱点")
NavigationLink(value: Route.weakPoints) {
ZXQuickAction(icon:"magnifyingglass",label:"分析\n薄弱点")
}.foregroundColor(.primary)
NavigationLink(destination: AIChatPage()) {
ZXQuickAction(emoji:"🎤",label:"费曼\n解释练习")
NavigationLink(value: Route.aiChat) {
ZXQuickAction(icon:"mic.fill",label:"费曼\n解释练习")
}.foregroundColor(.primary)
NavigationLink(destination: ReviewCardView()) {
ZXQuickAction(emoji:"📅",label:"今日\n复习计划")
NavigationLink(value: Route.reviewCard) {
ZXQuickAction(icon:"calendar",label:"今日\n复习计划")
}.foregroundColor(.primary)
}
}
@ -127,22 +128,22 @@ struct AIHomeView: View {
Text("最近 AI 互动").font(.system(size:14,weight:.bold)).foregroundColor(Color.zxF0)
Spacer();Text("全部").font(.system(size:12)).foregroundColor(Color.zxPurple)
}
ZXAIInteractionRow(tag:"费曼复习",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxPurple,emoji:"🎤",
ZXAIInteractionRow(tag:"费曼复习",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxPurple,icon:"mic.fill",
title:"解释量子纠缠的核心概念",time:"2小时前",score:82){ navigateToChat = true }
ZXAIInteractionRow(tag:"薄弱点",bg:Color(hex:"#F97316",opacity:0.15),fg:Color(hex:"#FBA574"),emoji:"⚠️",
ZXAIInteractionRow(tag:"薄弱点",bg:Color(hex:"#F97316",opacity:0.15),fg:Color(hex:"#FBA574"),icon:"exclamationmark.triangle.fill",
title:"混淆了协方差和相关系数",time:"昨天",score:56){ navigateToChat = true }
ZXAIInteractionRow(tag:"回忆测试",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxAccent,emoji:"📝",
ZXAIInteractionRow(tag:"回忆测试",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxAccent,icon:"doc.text.fill",
title:"机器学习中的偏差-方差权衡",time:"2天前",score:91){ navigateToChat = true }
}
}
private var suggestionSection: some View {
VStack(alignment:.leading,spacing:10){
Text("💡 你可以问 AI").font(.system(size:12,weight:.semibold)).foregroundColor(Color.zxF04)
(Text(Image(systemName:"lightbulb.fill")).foregroundColor(Color.zxYellow) + Text(" 你可以问 AI")).font(.system(size:12,weight:.semibold)).foregroundColor(Color.zxF04)
ForEach(["\"帮我测试机器学习这章的掌握情况\"","\"我最近的薄弱知识点有哪些?\"","\"生成一份本周的复习计划\""],id:\.self){s in
Button { text = s; navigateToChat = true } label: {
HStack{
Text(s).font(.system(size:12)).foregroundColor(Color(hex:"#F0F0FF",opacity:0.55)).lineSpacing(4)
Text(s).zxFontScaled(size:12).foregroundColor(Color(hex:"#F0F0FF",opacity:0.55)).lineSpacing(4)
Spacer()
Image(systemName:"arrow.up").font(.system(size:12)).foregroundColor(Color(hex:"#7C6EFA",opacity:0.5))
}
@ -158,33 +159,19 @@ struct AIHomeView: View {
}
private var inputBar: some View {
HStack(spacing:10){
Image(systemName:"sparkles").font(.system(size:16)).foregroundColor(Color.zxPurple)
TextField("问 AI 任何学习问题…",text:$text).font(.system(size:14)).tint(Color.zxPurple)
Spacer()
Image(systemName:"mic.fill").font(.system(size:18)).foregroundColor(Color.zxF03)
Button{ navigateToChat = true }label:{
Image(systemName:"arrow.up").font(.system(size:14,weight:.bold)).foregroundColor(.white)
.frame(width:30,height:30).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius:9))
}
.accessibilityLabel("发送消息,开始 AI 对话")
}
.padding(.horizontal,14).padding(.vertical,10)
.background(.ultraThinMaterial).background(Color.zxFill004)
.overlay(RoundedRectangle(cornerRadius:20).stroke(Color.zxBorder008,lineWidth:1))
.clipShape(RoundedRectangle(cornerRadius:20))
.padding(.horizontal,20)
.padding(.bottom,ZXSpacing.tabBarH+20)
ZXAIInputBar(text: $text, onSend: { navigateToChat = true })
.padding(.horizontal, 20)
.padding(.bottom, ZXSpacing.tabBarH + 20)
}
}
struct ZXQuickAction: View {
let emoji: String
let icon: String
let label: String
var body: some View {
VStack(spacing:6){
Text(emoji).font(.system(size:22))
Image(systemName:icon).font(.system(size:22)).foregroundColor(Color.zxPurple)
Text(label).font(.system(size:10,weight:.medium)).foregroundColor(Color.zxF03)
.multilineTextAlignment(.center).lineSpacing(2)
}
@ -198,7 +185,7 @@ struct ZXAIInteractionRow: View {
let tag: String
let bg: Color
let fg: Color
let emoji: String
let icon: String
let title: String
let time: String
let score: Int
@ -207,7 +194,7 @@ struct ZXAIInteractionRow: View {
var body: some View {
Button(action: action) {
HStack(spacing:12){
Text(emoji).font(.system(size:16))
Image(systemName:icon).font(.system(size:16)).foregroundColor(fg)
.frame(width:36,height:36).background(bg).clipShape(RoundedRectangle(cornerRadius:10))
VStack(alignment:.leading,spacing:4){
HStack{

View File

@ -6,190 +6,12 @@ struct DailyThinkingPage: View {
ZStack { Color.zxBg0.ignoresSafeArea()
ScrollView { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 12) {
HStack { Image(systemName:"sparkles").foregroundColor(Color.zxAccent); Text("解释\"注意力机制\"在 Transformer 中的作用").font(.system(size:15,weight:.bold)).foregroundColor(Color.zxF0) }
Text("AI会从三个方面评估你的回答核心概念理解 · 理论深度 · 实际应用能力").font(.system(size:12)).foregroundColor(Color.zxF04)
HStack { Image(systemName:"sparkles").foregroundColor(Color.zxAccent); Text("解释\"注意力机制\"在 Transformer 中的作用").zxFontScaled(size:15,weight:.bold).foregroundColor(Color.zxF0) }
Text("AI会从三个方面评估你的回答核心概念理解 · 理论深度 · 实际应用能力").zxFontScaled(size:12).foregroundColor(Color.zxF04)
}.padding(16).background(ZXGradient.thinkingCard).clipShape(RoundedRectangle(cornerRadius:16))
VStack(alignment:.leading,spacing:8){Text("你的回答").font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF04);TextEditor(text:$answer).font(.system(size:13)).foregroundColor(Color.zxF0).tint(Color.zxPurple).frame(minHeight:160).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1))}
if !submitted{ NavigationLink(destination:AIFeedbackPageView()){ Text("提交回答,获取 AI 反馈").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16)).shadow(color:Color(hex:"#7C6EFA",opacity:0.3),radius:24) }.zxPressable() }
VStack(alignment:.leading,spacing:8){Text("你的回答").font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF04);TextEditor(text:$answer).zxFontScaled(size:13).foregroundColor(Color.zxF0).tint(Color.zxPurple).frame(minHeight:160).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1))}
if !submitted{ NavigationLink(value: Route.aiFeedback){ Text("提交回答,获取 AI 反馈").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16)).shadow(color:Color(hex:"#7C6EFA",opacity:0.3),radius:24) }.zxPressable() }
}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,120) }.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)
}
}
struct RecallTestPage: View { @State private var input = ""; var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();ScrollView{VStack(spacing:16){Text("请回忆并写下你对「偏差-方差权衡」的理解").font(.system(size:14)).foregroundColor(Color.zxF04);TextEditor(text:$input).frame(minHeight:200).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1));NavigationLink(destination: AIFeedbackPageView()){Text("提交").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16))}}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} }
struct WeakPointsPage: View { var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();ScrollView{VStack(spacing:12){
ZXWeakRow(score:32,topic:"贝叶斯定理应用",lib:"机器学习",priority:"")
ZXWeakRow(score:41,topic:"正态分布性质",lib:"高等数学",priority:"")
ZXWeakRow(score:55,topic:"词根 spect- 相关词汇",lib:"英语词汇",priority:"")
ZXWeakRow(score:48,topic:"协方差与相关系数",lib:"机器学习",priority:"")
ZXWeakRow(score:36,topic:"梯度下降优化",lib:"机器学习",priority:"")
}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} }
struct AIFeedbackPageView: View {
@State private var navigateToChat = false
@State private var isAnalyzing = true
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
if isAnalyzing {
ZXAIAnalysisProgress(steps: [
"解析你的回答结构…",
"对比知识库标准答案…",
"评估概念理解深度…",
"生成个性化反馈…"
])
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
withAnimation(.easeOut(duration: 0.4)) { isAnalyzing = false }
}
}
} else {
ScrollView {
VStack(spacing: 16) {
HStack(spacing: 20) {
ZStack {
Circle().trim(from: 0, to: 0.78).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 10, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 80, height: 80)
VStack(spacing: 0) {
Text("78").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxPurple)
Text("/ 100").font(.system(size: 9)).foregroundColor(Color.zxF04)
}
}
VStack(alignment: .leading, spacing: 2) {
Text("良好掌握").font(.system(size: 18, weight: .heavy)).foregroundColor(Color.zxF0)
Text("理解核心概念,但缺少理论深度和解决方案").font(.system(size: 12)).foregroundColor(Color.zxF0045).lineSpacing(4)
}
Spacer()
}
.padding(20)
.background(ZXGradient.feedbackScore)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1))
VStack(alignment: .leading, spacing: 8) {
Text("你的回答").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF04)
Text("过拟合就像一个学生只会「死记硬背」考题,而不是真正理解知识…").font(.system(size: 13)).foregroundColor(Color.zxF007).lineSpacing(6).padding(14).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1))
}
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill").foregroundColor(Color.zxGreen)
Text("答对的部分").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0)
}
ForEach(["正确识别出过拟合是\"记住训练数据\"而非\"学习规律\"", "使用了死记硬背类比,方向正确且贴切"], id: \.self) { s in
HStack(alignment: .top, spacing: 12) {
Circle().fill(Color.zxGreen).frame(width: 6, height: 6).padding(.top, 6)
Text(s).font(.system(size: 13)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.75)).lineSpacing(4)
}
.padding(12)
.background(Color(hex: "#34D399", opacity: 0.07))
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color(hex: "#34D399", opacity: 0.18), lineWidth: 1))
}
}
NavigationLink(destination: StudyHomeView()) {
Label("加入待巩固,安排间隔复习", systemImage: "bolt.fill")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity).frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: Color(hex: "#7C6EFA", opacity: 0.3), radius: 24)
}
HStack(spacing: 12) {
NavigationLink(destination: AIChatPage()) {
HStack(spacing: 4) {
Text("深入提问").font(.system(size: 13))
Image(systemName: "chevron.right").font(.system(size: 14))
}
.foregroundColor(Color.zxF05)
.frame(maxWidth: .infinity).frame(height: 44)
.background(Color.zxFill005)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
NavigationLink(destination: DailyThinkingPage()) {
HStack(spacing: 4) {
Text("再来一题").font(.system(size: 13))
Image(systemName: "chevron.right").font(.system(size: 14))
}
.foregroundColor(Color.zxF05)
.frame(maxWidth: .infinity).frame(height: 44)
.background(Color.zxFill005)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}
.scrollIndicators(.hidden)
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
}
// MARK: - AI Chat
struct AIChatPage: View {
@StateObject private var vm = AIChatViewModel()
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
VStack(spacing: 0) {
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 16) {
ForEach(vm.messages) { m in
chatBubble(m)
.id(m.id)
}
if vm.isSending {
HStack(spacing: 8) {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28)
.background(Color(hex: "#7C6EFA", opacity: 0.15))
.clipShape(Circle())
ZXDotLoader(color: Color.zxPurple)
.padding(.leading, 4)
Spacer()
}
.padding(.horizontal, 20)
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
}
.scrollIndicators(.hidden)
.onChange(of: vm.messages.count) { _ in
withAnimation { proxy.scrollTo(vm.messages.last?.id) }
}
}
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() })
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
private func chatBubble(_ m: AIMessage) -> some View {
HStack(alignment: .top, spacing: 8) {
if m.role == .ai {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28)
.background(Color(hex: "#7C6EFA", opacity: 0.15))
.clipShape(Circle())
}
Text(m.content).font(.system(size: 14))
.foregroundColor(m.role == .user ? .white : Color.zxF007)
.padding(12)
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
.clipShape(RoundedRectangle(cornerRadius: 16))
if m.role == .user {
Circle().frame(width: 28, height: 28)
.foregroundColor(Color.zxPurpleBG(0.2))
.overlay(Text("").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
}
}
.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
}
}

View File

@ -0,0 +1,38 @@
import SwiftUI
struct RecallTestPage: View {
@State private var input = ""
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
Text("请回忆并写下你对「偏差-方差权衡」的理解")
.zxFontScaled(size: 14)
.foregroundColor(Color.zxF04)
TextEditor(text: $input)
.frame(minHeight: 200)
.scrollContentBackground(.hidden)
.padding(12)
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
NavigationLink(value: Route.aiFeedback) {
Text("提交")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}
.scrollIndicators(.hidden)
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -0,0 +1,22 @@
import SwiftUI
struct WeakPointsPage: View {
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 12) {
ZXWeakRow(score: 32, topic: "贝叶斯定理应用", lib: "机器学习", priority: "")
ZXWeakRow(score: 41, topic: "正态分布性质", lib: "高等数学", priority: "")
ZXWeakRow(score: 55, topic: "词根 spect- 相关词汇", lib: "英语词汇", priority: "")
ZXWeakRow(score: 48, topic: "协方差与相关系数", lib: "机器学习", priority: "")
ZXWeakRow(score: 36, topic: "梯度下降优化", lib: "机器学习", priority: "")
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}
.scrollIndicators(.hidden)
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -30,7 +30,7 @@ struct AnalysisHomeView: View {
ZXChartView()
}.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(destination: WeakPointsPage()) { 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
ZXWeakRow(score: item.masteryScore ?? 0, topic: item.title, lib: item.knowledgeBaseId ?? "", priority: item.priority ?? "normal")
}
@ -45,6 +45,7 @@ struct AnalysisHomeView: View {
}
}
.task { await viewModel.loadAll() }
.navigationDestination(for: Route.self) { $0.destination }
}
}
@ -60,6 +61,7 @@ struct ZXStatBadge: View { let icon: String; let label: String; let value: Strin
struct ZXChartView: View {
let data: [(String, CGFloat)] = [("", 0.62), ("", 0.65), ("", 0.71), ("", 0.68), ("", 0.75), ("", 0.79), ("", 0.78)]
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var showChart = false
var body: some View {
@ -81,7 +83,7 @@ struct ZXChartView: View {
)
)
.opacity(showChart ? 1 : 0)
.animation(.easeOut(duration: 0.8).delay(0.3), value: showChart)
.animation(reduceMotion ? nil : .easeOut(duration: 0.8).delay(0.3), value: showChart)
// Animated line
Path { path in let w = g.size.width / 7
@ -90,11 +92,12 @@ struct ZXChartView: View {
}
.trim(from: 0, to: showChart ? 1 : 0)
.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.animation(.easeOut(duration: 1.0), value: showChart)
.animation(reduceMotion ? nil : .easeOut(duration: 1.0), value: showChart)
}
}.frame(height: 100)
HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } }
}
.onAppear { showChart = true }
.animation(reduceMotion ? nil : .default, value: showChart)
}
}

View File

@ -11,14 +11,14 @@ struct LibraryHomeView: View {
ZStack { ZXGradient.page.ignoresSafeArea()
VStack(spacing: 0) {
HStack { Text("知识库").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5); Spacer()
NavigationLink(destination: LibrarySearchView()) {
NavigationLink(value: Route.librarySearch) {
Image(systemName: "magnifyingglass").font(.system(size: 18)).foregroundColor(Color.zxF05)
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
}
.accessibilityLabel("搜索知识库")
NavigationLink(destination: ImportPage()) {
NavigationLink(value: Route.libraryImport) {
Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white)
.frame(width: 36, height: 36).background(ZXGradient.brand)
.clipShape(RoundedRectangle(cornerRadius: 10))
@ -35,8 +35,8 @@ struct LibraryHomeView: View {
.frame(maxWidth: .infinity).padding(.top, 80)
}
ForEach(viewModel.knowledgeBases) { kb in
NavigationLink(destination: LibraryDetailPage(knowledgeBaseId: kb.id)) {
ZLibraryCard(emoji: "📚", name: kb.title, desc: kb.description ?? "", color: Color.zxPurple, items: kb.itemCount ?? 0, mastery: 50, tags: [], last: lastStudiedText(kb.lastStudiedAt))
NavigationLink(value: Route.libraryDetail(knowledgeBaseId: kb.id)) {
ZLibraryCard(icon: "books.vertical.fill", name: kb.title, desc: kb.description ?? "", color: Color.zxPurple, items: kb.itemCount ?? 0, mastery: 50, tags: [], last: lastStudiedText(kb.lastStudiedAt))
}
}
if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading {
@ -45,7 +45,7 @@ struct LibraryHomeView: View {
if viewModel.hasMore {
ZXLoadMoreFooter { await viewModel.loadMore() }
}
NavigationLink(destination: CreateLibraryPage()) {
NavigationLink(value: Route.libraryCreate) {
HStack(spacing: 8) { Image(systemName: "plus").font(.system(size: 16)); Text("创建新知识库").font(.system(size: 14, weight: .semibold)) }
.foregroundColor(Color.zxF05).frame(maxWidth: .infinity).frame(height: 52).background(Color.zxFill003)
.overlay(RoundedRectangle(cornerRadius: 16).strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [6, 4]), antialiased: true).foregroundColor(Color.zxBorder01))
@ -59,6 +59,7 @@ struct LibraryHomeView: View {
}
}
.task { await viewModel.loadKnowledgeBases() }
.navigationDestination(for: Route.self) { $0.destination }
}
private func lastStudiedText(_ iso: String?) -> String {
@ -66,8 +67,8 @@ struct LibraryHomeView: View {
return iso.prefix(10).description
}
}
struct ZLibraryCard: View { let emoji: String; let name: String; let desc: String; let color: Color; let items: Int; let mastery: Int; let tags: [String]; let last: String
var body: some View { VStack(spacing: 0) { Rectangle().fill(ZXGradient.progressBar).frame(height: 3); HStack(spacing: 12) { Text(emoji).font(.system(size: 22)).frame(width: 44, height: 44).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 13)).overlay(RoundedRectangle(cornerRadius: 13).stroke(color.opacity(0.3), lineWidth: 1)); VStack(alignment: .leading, spacing: 2) { Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04); Text("掌握 \(mastery)%").font(.system(size: 11)).foregroundColor(Color.zxF04) }; Spacer() }.padding(16); HStack { HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03); Spacer(); ForEach(tags.prefix(2), id: \.self) { t in Text(t).font(.system(size: 10, weight: .medium)).foregroundColor(Color.zxPurple).padding(.horizontal, 7).padding(.vertical, 2).background(Color(hex: "#7C6EFA", opacity: 0.08)).clipShape(Capsule()) } }.padding(.horizontal, 16).padding(.bottom, 12) }
struct ZLibraryCard: View { let icon: String; let name: String; let desc: String; let color: Color; let items: Int; let mastery: Int; let tags: [String]; let last: String
var body: some View { VStack(spacing: 0) { Rectangle().fill(ZXGradient.progressBar).frame(height: 3); HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 20)).foregroundColor(color).frame(width: 44, height: 44).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 13)).overlay(RoundedRectangle(cornerRadius: 13).stroke(color.opacity(0.3), lineWidth: 1)); VStack(alignment: .leading, spacing: 2) { Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04); Text("掌握 \(mastery)%").font(.system(size: 11)).foregroundColor(Color.zxF04) }; Spacer() }.padding(16); HStack { HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03); Spacer(); ForEach(tags.prefix(2), id: \.self) { t in Text(t).font(.system(size: 10, weight: .medium)).foregroundColor(Color.zxPurple).padding(.horizontal, 7).padding(.vertical, 2).background(Color(hex: "#7C6EFA", opacity: 0.08)).clipShape(Capsule()) } }.padding(.horizontal, 16).padding(.bottom, 12) }
.background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) }
}

View File

@ -20,7 +20,7 @@ struct LibraryDetailPage: View {
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
HStack { Spacer()
NavigationLink(destination: AddKnowledgePage(knowledgeBaseId: knowledgeBaseId)) {
NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) {
Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white)
.frame(width: 36, height: 36).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10))
}
@ -31,8 +31,8 @@ struct LibraryDetailPage: View {
.frame(maxWidth: .infinity).padding(.top, 80)
}
ForEach(viewModel.items) { item in
NavigationLink(destination: KnowledgeDetailPage(item: item)) {
ZXCardRow(emoji: "📝", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen)
NavigationLink(value: Route.knowledgeDetail(item: item)) {
ZXCardRow(icon: "doc.text", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen)
}
}
if viewModel.items.isEmpty && !viewModel.isLoading {
@ -48,8 +48,8 @@ struct LibraryDetailPage: View {
.task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) }
}
}
struct ZXCardRow: View { let emoji: String; let title: String; let desc: String; let status: String; let c: Color
var body: some View { HStack(spacing: 12) { Text(emoji).font(.system(size: 20)).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
struct ZXCardRow: View { let icon: String; let title: String; let desc: String; let status: String; let c: Color
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(c).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
.padding(14).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1)) }
}
@ -73,7 +73,7 @@ struct KnowledgeDetailPage: View {
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
HStack { Spacer()
NavigationLink(destination: EditKnowledgePage(item: item)) {
NavigationLink(value: Route.editKnowledge(item: item)) {
Image(systemName: "pencil").font(.system(size: 16)).foregroundColor(Color.zxF05)
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
@ -90,10 +90,10 @@ struct KnowledgeDetailPage: View {
if let content = item.content { Text(content).font(.system(size: 14)).foregroundColor(Color.zxF007).lineSpacing(6) }
}.padding(20).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
HStack(spacing: 12) {
NavigationLink(destination: StudyHomeView()) {
NavigationLink(value: Route.studyHome) {
Label("开始复习", systemImage: "arrow.triangle.2.circlepath").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 44).background(ZXGradient.brandPurple).clipShape(RoundedRectangle(cornerRadius: 14))
}
NavigationLink(destination: AIChatPage()) {
NavigationLink(value: Route.aiChat) {
Label("费曼解释", systemImage: "mic.fill").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF05).frame(maxWidth: .infinity).frame(height: 44).background(Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
}

View File

@ -0,0 +1,44 @@
import SwiftUI
struct FeedbackFormView: View {
@State private var type = "功能建议"
@State private var content = ""
@State private var submitted = false
@State private var isSubmitting = false
let types = ["Bug 反馈", "功能建议", "内容问题", "其他"]
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("反馈类型").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
HStack(spacing: 8) { ForEach(types, id: \.self) { t in let sel = type == t; Button { type = t } label: { Text(t).font(.system(size: 12)).foregroundColor(sel ? .white : Color.zxF05).padding(.horizontal, 12).padding(.vertical, 6).background(sel ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill005)).clipShape(Capsule()) } } }
}
VStack(alignment: .leading, spacing: 8) {
Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
TextEditor(text: $content).frame(minHeight: 150).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
Button {
Task {
isSubmitting = true
_ = try? await FeedbackService.shared.submit(category: type, content: content)
submitted = true
isSubmitting = false
}
} label: {
HStack(spacing: 8) {
if isSubmitting { ProgressView().tint(.white) }
Text(submitted ? "已提交" : "提交").font(.system(size: 14, weight: .bold))
}
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
.background(isSubmitting ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.disabled(isSubmitting)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -0,0 +1,54 @@
import SwiftUI
struct GoalSettingDetailView: View {
@State private var selectedGoal = "备考考试"
let goals = [("person.crop.circle.fill","备考考试","公考、考研、考证等"),("briefcase.fill","职业技能","编程、设计、产品等"),("books.vertical.fill","通识学习","扩充知识面"),("target","自定义","设定自己的目标")]
@State private var dailyMins = "30 分钟"
let times = ["15 分钟","30 分钟","1 小时","不限制"]
@State private var isSaving = false
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 10) {
Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
ForEach(goals, id: \.1) { g in let sel = selectedGoal == g.1
Button { selectedGoal = g.1 } label: {
HStack(spacing: 12) {
Image(systemName: g.0).font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(width: 44, height: 44).background(sel ? Color(hex: "#7C6EFA", opacity: 0.15) : Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 2) { Text(g.1).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0); Text(g.2).font(.system(size: 12)).foregroundColor(Color.zxF04) }
Spacer()
Circle().stroke(sel ? Color.zxPurple : Color(hex: "#FFFFFF", opacity: 0.2), lineWidth: 2).frame(width: 22, height: 22).overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } }
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16))
}.foregroundColor(.primary)
}
}
VStack(alignment: .leading, spacing: 10) {
Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
HStack(spacing: 8) { ForEach(times, id: \.self) { t in let sel = dailyMins == t; Button { dailyMins = t } label: { Text(t).font(.system(size: 12)).fontWeight(sel ? .semibold : .regular).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(maxWidth: .infinity).frame(height: 40).background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12)) }.foregroundColor(.primary) } }
}
Button {
Task {
isSaving = true
_ = try? await UserService.shared.updateProfileDetail(UpdateProfileDataRequest(
learningIdentity: nil, learningDirection: nil, bio: nil, currentGoal: selectedGoal
))
isSaving = false
}
} label: {
HStack(spacing: 8) {
if isSaving { ProgressView().tint(.white) }
Text("保存").font(.system(size: 14, weight: .bold))
}
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
.background(isSaving ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.disabled(isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -0,0 +1,51 @@
import SwiftUI
struct MethodPreferenceView: View {
@State private var methods: Set<String> = ["间隔回忆", "费曼技巧"]
let allMethods = ["间隔回忆", "费曼技巧", "AI 分析", "主动回忆"]
@State private var saved = false
@State private var isSaving = false
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 10) {
Text("选择你偏好的学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
ForEach(allMethods, id: \.self) { m in let sel = methods.contains(m)
Button { if sel { methods.remove(m) } else { methods.insert(m) } } label: {
HStack(spacing: 12) {
Image(systemName: sel ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF02)
Text(m).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0)
Spacer()
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14))
}.foregroundColor(.primary)
}
}
Button {
Task {
isSaving = true
_ = try? await UserService.shared.updatePreferences(UpdatePreferencesRequest(
preferredMethods: Array(methods), defaultFocusMinutes: nil,
aiSuggestionLevel: nil, language: nil, appearance: nil,
notificationEnabled: nil
))
saved = true
isSaving = false
}
} label: {
HStack(spacing: 8) {
if isSaving { ProgressView().tint(.white) }
Text(saved ? "已保存" : "保存").font(.system(size: 14, weight: .bold))
}
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
.background(isSaving ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.disabled(isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}

View File

@ -11,14 +11,14 @@ struct ProfileView: View {
HStack {
Text("我的").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5)
Spacer()
NavigationLink(destination: NotificationListView()) {
NavigationLink(value: Route.notificationList) {
Image(systemName: "bell").font(.system(size: 18)).foregroundColor(Color.zxF05)
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
}
.accessibilityLabel("通知中心")
NavigationLink(destination: SettingsView()) {
NavigationLink(value: Route.settings) {
Image(systemName: "gearshape").font(.system(size: 18)).foregroundColor(Color.zxF05)
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
@ -28,20 +28,20 @@ struct ProfileView: View {
}.padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4)
profileCard
VStack(spacing: 0) {
NavigationLink(destination: GoalSettingDetailView()) {
ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标")
NavigationLink(value: Route.goalSetting) {
ZXProfileMenuRow(icon: "target", title: "学习目标设置", desc: "调整你的学习目标")
}.foregroundColor(.primary)
ZXProfileDivider()
NavigationLink(destination: SettingsView()) {
ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置")
NavigationLink(value: Route.settings) {
ZXProfileMenuRow(icon: "bell.fill", title: "复习提醒", desc: "间隔复习通知设置")
}.foregroundColor(.primary)
ZXProfileDivider()
NavigationLink(destination: MethodPreferenceView()) {
ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔")
NavigationLink(value: Route.methodPreference) {
ZXProfileMenuRow(icon: "puzzlepiece.fill", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔")
}.foregroundColor(.primary)
ZXProfileDivider()
NavigationLink(destination: FeedbackFormView()) {
ZXProfileMenuRow(emoji: "💬", title: "帮助与反馈", desc: "问题报告 · 功能建议")
NavigationLink(value: Route.feedbackForm) {
ZXProfileMenuRow(icon: "bubble.left.fill", title: "帮助与反馈", desc: "问题报告 · 功能建议")
}.foregroundColor(.primary)
}.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
achievementsSection.padding(.bottom, 120)
@ -49,13 +49,14 @@ struct ProfileView: View {
}.scrollIndicators(.hidden)
}
.task { await viewModel.loadAll() }
.navigationDestination(for: Route.self) { $0.destination }
}
private var profileCard: some View {
let profile = viewModel.userProfile
return NavigationLink(destination: EditProfilePage()) {
return NavigationLink(value: Route.editProfile) {
VStack(spacing: 16) {
HStack {
ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Text("🧑‍🎓").font(.system(size: 36)) }
ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Image(systemName: "person.crop.circle.fill").font(.system(size: 36)).foregroundColor(Color.zxPurple) }
VStack(alignment: .leading, spacing: 4) {
Text(profile?.nickname ?? "学习者").font(.system(size: 20, weight: .bold)).foregroundColor(Color.zxF0)
Text(profile?.email ?? "").font(.system(size: 12)).foregroundColor(Color.zxF04)
@ -71,19 +72,19 @@ struct ProfileView: View {
private var achievementsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("成就").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
HStack(spacing: 8) { ZXAchievementBadge(emoji: "🔥", label: "连续 14 天", color: Color.zxOrange); ZXAchievementBadge(emoji: "🧠", label: "费曼达人", color: Color.zxPurple); ZXAchievementBadge(emoji: "📚", label: "知识收藏家", color: Color.zxTeal); ZXAchievementBadge(emoji: "", label: "速学者", color: Color.zxYellow) }
HStack(spacing: 8) { ZXAchievementBadge(icon: "flame.fill", label: "连续 14 天", color: Color.zxOrange); ZXAchievementBadge(icon: "brain.head.profile", label: "费曼达人", color: Color.zxPurple); ZXAchievementBadge(icon: "books.vertical.fill", label: "知识收藏家", color: Color.zxTeal); ZXAchievementBadge(icon: "bolt.fill", label: "速学者", color: Color.zxYellow) }
}
}
}
struct ZXProfileStat: View { let v: String; let l: String; let c: Color; var body: some View { VStack(spacing: 2) { Text(v).font(.system(size: 18, weight: .bold)).foregroundColor(c); Text(l).font(.system(size: 11)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
init(value: String, label: String, color: Color) { self.v = value; self.l = label; self.c = color }
}
struct ZXProfileMenuRow: View { let emoji: String; let title: String; let desc: String
var body: some View { HStack(spacing: 12) { Text(emoji).font(.system(size: 20)).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14).accessibilityLabel("\(title)\(desc)") }
struct ZXProfileMenuRow: View { let icon: String; let title: String; let desc: String
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(Color.zxF05).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14).accessibilityLabel("\(title)\(desc)") }
}
struct ZXProfileDivider: View {
var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 64) }
}
struct ZXAchievementBadge: View { let emoji: String; let label: String; let color: Color
var body: some View { VStack(spacing: 6) { Text(emoji).font(.system(size: 24)).frame(width: 48, height: 48).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.25), lineWidth: 1)); Text(label).font(.system(size: 10, weight: .semibold)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
struct ZXAchievementBadge: View { let icon: String; let label: String; let color: Color
var body: some View { VStack(spacing: 6) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(color).frame(width: 48, height: 48).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.25), lineWidth: 1)); Text(label).font(.system(size: 10, weight: .semibold)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
}

View File

@ -32,11 +32,11 @@ struct SettingsView: View {
sectionHeader("学习设置")
VStack(spacing: 0) {
NavigationLink(destination: GoalSettingDetailView()) {
NavigationLink(value: Route.goalSetting) {
ZXSettingRow(title: "学习目标", value: "备考考试", icon: "target", color: Color.zxOrange)
}.foregroundColor(.primary)
ZXSettingDivider()
NavigationLink(destination: MethodPreferenceView()) {
NavigationLink(value: Route.methodPreference) {
ZXSettingRow(title: "学习方法偏好", value: "间隔回忆 · 费曼技巧", icon: "brain.head.profile", color: Color.zxPurple)
}.foregroundColor(.primary)
}
@ -66,7 +66,7 @@ struct SettingsView: View {
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
VStack(spacing: 0) {
NavigationLink(destination: FeedbackFormView()) {
NavigationLink(value: Route.feedbackForm) {
ZXSettingRow(title: "帮助与反馈", value: "", icon: "questionmark.circle.fill", color: Color.zxAccent)
}.foregroundColor(.primary)
ZXSettingDivider()
@ -152,121 +152,7 @@ struct SettingsView: View {
}
}
struct GoalSettingDetailView: View {
@State private var selectedGoal = "备考考试"
let goals = [("🧑‍🎓","备考考试","公考、考研、考证等"),("💼","职业技能","编程、设计、产品等"),("📚","通识学习","扩充知识面"),("🎯","自定义","设定自己的目标")]
@State private var dailyMins = "30 分钟"
let times = ["15 分钟","30 分钟","1 小时","不限制"]
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 10) {
Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
ForEach(goals, id: \.1) { g in let sel = selectedGoal == g.1
Button { selectedGoal = g.1 } label: {
HStack(spacing: 12) {
Text(g.0).font(.system(size: 22)).frame(width: 44, height: 44).background(sel ? Color(hex: "#7C6EFA", opacity: 0.15) : Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 2) { Text(g.1).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0); Text(g.2).font(.system(size: 12)).foregroundColor(Color.zxF04) }
Spacer()
Circle().stroke(sel ? Color.zxPurple : Color(hex: "#FFFFFF", opacity: 0.2), lineWidth: 2).frame(width: 22, height: 22).overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } }
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16))
}.foregroundColor(.primary)
}
}
VStack(alignment: .leading, spacing: 10) {
Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
HStack(spacing: 8) { ForEach(times, id: \.self) { t in let sel = dailyMins == t; Button { dailyMins = t } label: { Text(t).font(.system(size: 12)).fontWeight(sel ? .semibold : .regular).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(maxWidth: .infinity).frame(height: 40).background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12)) }.foregroundColor(.primary) } }
}
Button {
Task {
_ = try? await UserService.shared.updateProfileDetail(UpdateProfileDataRequest(
learningIdentity: nil, learningDirection: nil, bio: nil, currentGoal: selectedGoal
))
}
} label: {
Text("保存").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
}
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}
struct MethodPreferenceView: View {
@State private var methods: Set<String> = ["间隔回忆", "费曼技巧"]
let allMethods = ["间隔回忆", "费曼技巧", "AI 分析", "主动回忆"]
@State private var saved = false
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 10) {
Text("选择你偏好的学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
ForEach(allMethods, id: \.self) { m in let sel = methods.contains(m)
Button { if sel { methods.remove(m) } else { methods.insert(m) } } label: {
HStack(spacing: 12) {
Image(systemName: sel ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF02)
Text(m).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0)
Spacer()
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14))
}.foregroundColor(.primary)
}
}
Button {
Task {
_ = try? await UserService.shared.updatePreferences(UpdatePreferencesRequest(
preferredMethods: Array(methods), defaultFocusMinutes: nil,
aiSuggestionLevel: nil, language: nil, appearance: nil,
notificationEnabled: nil
))
saved = true
}
} label: {
Text(saved ? "已保存" : "保存").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
}
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}
struct FeedbackFormView: View {
@State private var type = "功能建议"
@State private var content = ""
@State private var submitted = false
let types = ["Bug 反馈", "功能建议", "内容问题", "其他"]
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("反馈类型").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
HStack(spacing: 8) { ForEach(types, id: \.self) { t in let sel = type == t; Button { type = t } label: { Text(t).font(.system(size: 12)).foregroundColor(sel ? .white : Color.zxF05).padding(.horizontal, 12).padding(.vertical, 6).background(sel ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill005)).clipShape(Capsule()) } } }
}
VStack(alignment: .leading, spacing: 8) {
Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
TextEditor(text: $content).frame(minHeight: 150).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
Button {
Task {
_ = try? await FeedbackService.shared.submit(category: type, content: content)
submitted = true
}
} label: {
Text(submitted ? "已提交" : "提交").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
}
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}.scrollIndicators(.hidden)
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
}
}
// MARK: - Setting row components
struct ZXSettingRow: View {
let title: String; let value: String; let icon: String; let color: Color

View File

@ -6,6 +6,7 @@ struct LearningSessionView: View {
let taskType: String
let taskColor: Color
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var elapsed: TimeInterval = 0
@State private var animatedProgress: CGFloat = 0
@State private var isRunning = true
@ -74,7 +75,7 @@ struct LearningSessionView: View {
)
.rotationEffect(.degrees(-90))
.frame(width: 180, height: 180)
.animation(.easeInOut(duration: 0.5), value: animatedProgress)
.animation(reduceMotion ? nil : .easeInOut(duration: 0.5), value: animatedProgress)
VStack(spacing: 4) {
Text(formatTime(elapsed))
.font(.system(size: 36, weight: .black))
@ -87,9 +88,9 @@ struct LearningSessionView: View {
}
}
.onChange(of: elapsed) { newElapsed in
withAnimation(.easeInOut(duration: 0.5)) {
animatedProgress = min(CGFloat(newElapsed) / 1800, 1)
}
let pct = min(CGFloat(newElapsed) / 1800, 1)
if reduceMotion { animatedProgress = pct }
else { withAnimation(.easeInOut(duration: 0.5)) { animatedProgress = pct } }
}
HStack(spacing: 12) {
Button {
@ -147,7 +148,7 @@ struct LearningSessionView: View {
Text("学习小贴士").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0)
}
Text("保持专注25-30 分钟后休息 5 分钟能有效提升记忆效果。学习时尽量避免切换任务。")
.font(.system(size: 12)).foregroundColor(Color.zxF04).lineSpacing(4)
.zxFontScaled(size: 12).foregroundColor(Color.zxF04).lineSpacing(4)
}
.padding(16)
.background(Color.zxFill004)
@ -159,12 +160,12 @@ struct LearningSessionView: View {
HStack(spacing: 12) {
if isRunning {
HStack(spacing: 6) {
Circle().fill(Color.zxGreen).frame(width: 6, height: 6)
Circle().fill(Color.zxGreen).frame(width: 8, height: 8)
Text("学习中…").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxGreen)
}
} else if isPaused {
HStack(spacing: 6) {
Circle().fill(Color.zxYellow).frame(width: 6, height: 6)
Circle().fill(Color.zxYellow).frame(width: 8, height: 8)
Text("已暂停").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxYellow)
}
}

View File

@ -14,6 +14,7 @@ struct ReviewCardView: View {
source: "机器学习 · 正则化方法", count: 3, total: 8),
]
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var idx = 0
@State private var flipped = false
@State private var rating: Int? = nil
@ -78,7 +79,7 @@ struct ReviewCardView: View {
private var flashCard: some View {
VStack(spacing: 0) {
Text(flipped ? "答案" : "问题")
.font(.system(size: 10, weight: .bold))
.zxFontScaled(size: 10, weight: .bold)
.foregroundColor(flipped ? Color.zxGreen : Color.zxAccent)
.tracking(0.5)
.padding(.horizontal, 10).padding(.vertical, 3)
@ -87,7 +88,7 @@ struct ReviewCardView: View {
.padding(.bottom, 16)
Text(flipped ? current.answer : current.question)
.font(.system(size: flipped ? 14 : 16, weight: flipped ? .medium : .semibold))
.zxFontScaled(size: flipped ? 14 : 16, weight: flipped ? .medium : .semibold)
.foregroundColor(Color.zxF0)
.lineSpacing(6)
.multilineTextAlignment(.center)
@ -98,12 +99,12 @@ struct ReviewCardView: View {
Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.vertical, 12)
HStack(spacing: 4) {
Image(systemName: "book.closed.fill").font(.system(size: 10)).foregroundColor(Color.zxF03)
Text(current.source).font(.system(size: 11)).foregroundColor(Color.zxF04)
Text(current.source).zxFontScaled(size: 11).foregroundColor(Color.zxF04)
}
}
} else {
Text("点击翻转查看答案")
.font(.system(size: 11))
.zxFontScaled(size: 11)
.foregroundColor(Color.zxF03)
.padding(.top, 20)
}
@ -113,7 +114,7 @@ struct ReviewCardView: View {
.background(flipped ? ZXGradient.progressCard : ZXGradient.thinkingCard)
.overlay(RoundedRectangle(cornerRadius: 20).stroke((flipped ? Color.zxPurple : Color.zxAccent).opacity(0.15), lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 20))
.onTapGesture { withAnimation(.easeInOut(duration: 0.4)) { flipped.toggle() } }
.onTapGesture { if reduceMotion { flipped.toggle() } else { withAnimation(.easeInOut(duration: 0.4)) { flipped.toggle() } } }
.zxPressable()
.accessibilityElement(children: .combine)
.accessibilityLabel(flipped ? "答案:\(current.answer)" : "问题:\(current.question)")
@ -163,10 +164,10 @@ struct ZXRatingBtn: View {
Button(action: action) {
VStack(spacing: 4) {
Text(label).font(.system(size: 11, weight: selected ? .bold : .medium))
.foregroundColor(selected ? .white : Color.zxF05)
.foregroundColor(selected ? .white : color)
}
.frame(maxWidth: .infinity).frame(height: 56)
.background(selected ? AnyView(ZXGradient.brand) : AnyView(Color.zxFill005))
.background(selected ? AnyView(color) : AnyView(Color.zxFill005))
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay {
if !selected { RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1) }

View File

@ -16,15 +16,15 @@ struct StudyHomeView: View {
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
if t.tp == "回忆测试" {
NavigationLink(destination: ActiveRecallView()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
NavigationLink(value: Route.activeRecall) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
} else if t.tp == "费曼练习" {
NavigationLink(destination: AIChatPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
NavigationLink(value: Route.aiChat) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
} else if t.tp == "薄弱点" {
NavigationLink(destination: WeakPointsPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
NavigationLink(value: Route.weakPoints) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
} else if t.tp == "间隔复习" {
NavigationLink(destination: ReviewCardView()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
NavigationLink(value: Route.reviewCard) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
} else {
NavigationLink(destination: LearningSessionView(taskTitle: t.t, taskType: t.tp, taskColor: t.c)) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
NavigationLink(value: Route.learningSession(taskTitle: t.t, taskType: t.tp, taskColorHex: t.ch)) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
}
}
}
@ -37,6 +37,7 @@ struct StudyHomeView: View {
.zxPullToRefresh { await studyVM.loadSessions() }
}
.task { await studyVM.loadSessions() }
.navigationDestination(for: Route.self) { $0.destination }
}
private var pc: 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) } }; Spacer()
@ -46,7 +47,7 @@ struct StudyHomeView: View {
.padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }
}
struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let c: Color; let m: Int; var d: Bool }
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) }
}

View File

@ -4,11 +4,11 @@ import Foundation
@MainActor
final class StudyHomeViewModel: ObservableObject {
@Published var tasks: [ZXSTask] = [
ZXSTask(t: "机器学习 - 回忆测试", tp: "回忆测试", c: .zxPurple, m: 10, d: true),
ZXSTask(t: "高数 - 间隔复习 8 题", tp: "间隔复习", c: .zxOrange, m: 15, d: true),
ZXSTask(t: "英语词汇 - 25 个待复习", tp: "词汇复习", c: .zxTeal, m: 8, d: false),
ZXSTask(t: "注意力机制 - 费曼解释", tp: "费曼练习", c: .zxAccent, m: 12, d: false),
ZXSTask(t: "产品设计 - 薄弱点复习", tp: "薄弱点", c: .zxYellow, m: 10, d: false),
ZXSTask(t: "机器学习 - 回忆测试", tp: "回忆测试", ch: "#7C6EFA", m: 10, d: true),
ZXSTask(t: "高数 - 间隔复习 8 题", tp: "间隔复习", ch: "#F97316", m: 15, d: true),
ZXSTask(t: "英语词汇 - 25 个待复习", tp: "词汇复习", ch: "#2DD4BF", m: 8, d: false),
ZXSTask(t: "注意力机制 - 费曼解释", tp: "费曼练习", ch: "#A78BFA", m: 12, d: false),
ZXSTask(t: "产品设计 - 薄弱点复习", tp: "薄弱点", ch: "#F59E0B", m: 10, d: false),
]
@Published var weekActivity: [CGFloat] = [0.3, 0.7, 1.0, 0.4, 0.9, 0.6, 0.2]