diff --git a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift index bba1b3b..c373713 100644 --- a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift +++ b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift @@ -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)) } diff --git a/AIStudyApp/AIStudyApp/ContentView.swift b/AIStudyApp/AIStudyApp/ContentView.swift index 1732bd1..c8adee2 100644 --- a/AIStudyApp/AIStudyApp/ContentView.swift +++ b/AIStudyApp/AIStudyApp/ContentView.swift @@ -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) } } diff --git a/AIStudyApp/AIStudyApp/Core/DesignSystem/DesignTokens.swift b/AIStudyApp/AIStudyApp/Core/DesignSystem/DesignTokens.swift index 1232742..8c2cdb5 100644 --- a/AIStudyApp/AIStudyApp/Core/DesignSystem/DesignTokens.swift +++ b/AIStudyApp/AIStudyApp/Core/DesignSystem/DesignTokens.swift @@ -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)) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/DesignSystem/ZXAnimations.swift b/AIStudyApp/AIStudyApp/Core/DesignSystem/ZXAnimations.swift index be40087..06e239e 100644 --- a/AIStudyApp/AIStudyApp/Core/DesignSystem/ZXAnimations.swift +++ b/AIStudyApp/AIStudyApp/Core/DesignSystem/ZXAnimations.swift @@ -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.. 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 } } } diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift index 40fcb5d..00bdc46 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -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? diff --git a/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift b/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift new file mode 100644 index 0000000..5a464e6 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift @@ -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() + } + } +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift new file mode 100644 index 0000000..3d504a1 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPageView.swift b/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPageView.swift new file mode 100644 index 0000000..0352df3 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPageView.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift index 8d255fe..a2ec696 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift @@ -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{ diff --git a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift index f369a1a..e3a9568 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift @@ -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) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift b/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift new file mode 100644 index 0000000..6ab8c13 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift b/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift new file mode 100644 index 0000000..ec265ba --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index ea816fc..c184677 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift b/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift index da19a4e..7700a37 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift @@ -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)) } } diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift index ffacb36..5c1859b 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -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)) } } diff --git a/AIStudyApp/AIStudyApp/Features/Profile/FeedbackFormView.swift b/AIStudyApp/AIStudyApp/Features/Profile/FeedbackFormView.swift new file mode 100644 index 0000000..4e6beaf --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Profile/FeedbackFormView.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Profile/GoalSettingDetailView.swift b/AIStudyApp/AIStudyApp/Features/Profile/GoalSettingDetailView.swift new file mode 100644 index 0000000..2ab21fd --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Profile/GoalSettingDetailView.swift @@ -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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Profile/MethodPreferenceView.swift b/AIStudyApp/AIStudyApp/Features/Profile/MethodPreferenceView.swift new file mode 100644 index 0000000..510d4d7 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Profile/MethodPreferenceView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct MethodPreferenceView: View { + @State private var methods: Set = ["间隔回忆", "费曼技巧"] + 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) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift index 2d5b334..4d00195 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift @@ -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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift b/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift index 15ed115..1b274f9 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift @@ -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 = ["间隔回忆", "费曼技巧"] - 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 diff --git a/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift index cfbf0db..9a16fd9 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift @@ -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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift index 6e3d8ba..2f7720a 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift @@ -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) } diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift index 52ab951..54ebf32 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift index f77be9f..e47596b 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift @@ -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]