From a96d6cb15974667365d0dd673681167761bf1f63 Mon Sep 17 00:00:00 2001 From: WangDL Date: Tue, 12 May 2026 17:08:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ios):=20=E8=A1=A5=E5=85=A8=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E8=B7=B3=E8=BD=AC=E3=80=81=E6=B5=85=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E3=80=813=E4=B8=AA=E6=96=B0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 3 处强制深色模式,用 @AppStorage 全局切换 - 设置页「外观」按钮实时切换深色/浅色/跟随系统 - 底部导航栏 inactive 颜色改为自适应 Color.zxF03 - 12 个子页面修复:保留返回按钮 + 消除顶部空白 - 新增 LearningSessionView/ReviewCardView/ActiveRecallView - 新增 NotificationListView/SettingsView 等子页面 - 补全所有按钮 NavigationLink 跳转(0 个空白 action) - KnowledgeBase 模型对齐服务器数据 - Info.plist 补充 CFBundleIdentifier + ATS - 新增缺口分析文档 gap-analysis-1/2.md --- .../AIStudyApp.xcodeproj/project.pbxproj | 26 +- AIStudyApp/AIStudyApp/AIStudyAppApp.swift | 15 +- AIStudyApp/AIStudyApp/ContentView.swift | 4 +- .../AIStudyApp/Core/Models/APIModels.swift | 14 +- .../AIStudyApp/Features/AI/AIHomeView.swift | 50 ++-- .../Features/AI/ActiveRecallView.swift | 188 ++++++++++++ .../Features/AI/DailyThinkingPage.swift | 40 ++- .../Features/Analysis/AnalysisHomeView.swift | 6 +- .../Features/Library/LibraryHomeView.swift | 35 ++- .../Features/Library/LibrarySubpages.swift | 49 +++- .../Profile/NotificationListView.swift | 92 ++++++ .../Features/Profile/ProfileView.swift | 55 +++- .../Features/Profile/SettingsView.swift | 236 ++++++++++++++++ .../Features/Study/LearningSessionView.swift | 164 +++++++++++ .../Features/Study/ReviewCardView.swift | 159 +++++++++++ .../Features/Study/StudyHomeView.swift | 24 +- AIStudyApp/Info.plist | 51 ++++ AIStudyApp/docs/gap-analysis-1.md | 157 ++++++++++ AIStudyApp/docs/gap-analysis-2.md | 267 ++++++++++++++++++ 19 files changed, 1529 insertions(+), 103 deletions(-) create mode 100644 AIStudyApp/AIStudyApp/Features/AI/ActiveRecallView.swift create mode 100644 AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift create mode 100644 AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift create mode 100644 AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift create mode 100644 AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift create mode 100644 AIStudyApp/Info.plist create mode 100644 AIStudyApp/docs/gap-analysis-1.md create mode 100644 AIStudyApp/docs/gap-analysis-2.md diff --git a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj index a1dd2b5..d3de943 100644 --- a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj +++ b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj @@ -254,18 +254,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSAppTransportSecurity = {NSAllowsArbitraryLoads = YES;}; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_FILE = Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 26.4; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; @@ -299,18 +288,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSAppTransportSecurity = {NSAllowsArbitraryLoads = YES;}; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_FILE = Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 26.4; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; diff --git a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift index 983542b..6c55edd 100644 --- a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift +++ b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift @@ -3,14 +3,23 @@ import SwiftUI @main struct AIStudyAppApp: App { @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false + @AppStorage("appAppearance") private var appAppearance = "system" + + private var effectiveColorScheme: ColorScheme? { + switch appAppearance { + case "dark": return .dark + case "light": return .light + default: return nil + } + } var body: some Scene { WindowGroup { if hasCompletedOnboarding { - ContentView().preferredColorScheme(.dark) + ContentView().preferredColorScheme(effectiveColorScheme) } else { OnboardingFlowView(hasCompletedOnboarding: $hasCompletedOnboarding) - .preferredColorScheme(.dark) + .preferredColorScheme(effectiveColorScheme) } } } @@ -30,7 +39,7 @@ struct OnboardingFlowView: View { case 4: GoalSetupPage { $0 ? (hasCompletedOnboarding = true) : (step = 0) } default: EmptyView() } - }.preferredColorScheme(.dark) + } } } diff --git a/AIStudyApp/AIStudyApp/ContentView.swift b/AIStudyApp/AIStudyApp/ContentView.swift index c03996a..c3b6c6d 100644 --- a/AIStudyApp/AIStudyApp/ContentView.swift +++ b/AIStudyApp/AIStudyApp/ContentView.swift @@ -13,14 +13,14 @@ struct ContentView: View { default: NavigationStack { AIHomeView() } } VStack { Spacer(); ZXTabBar(active: $selectedTab) }.ignoresSafeArea(edges: .bottom) - }.ignoresSafeArea(edges: .bottom).preferredColorScheme(.dark) + }.ignoresSafeArea(edges: .bottom) } } 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")] - var body: some View{HStack(spacing:0){ForEach(items,id:\.0){item in let on=item.0==active;Button{active=item.0}label:{VStack(spacing:4){ZStack{if on{Circle().fill(Color.zxPurple.opacity(0.2)).frame(width:28,height:28).scaleEffect(1.4)};Image(systemName:item.2).font(.system(size:22,weight:on ? .semibold:.regular)).foregroundColor(on ? Color.zxPurple:Color(hex:"#F0F0FF",opacity:0.35))};Text(item.1).font(.system(size:10,weight:on ? .semibold:.regular)).foregroundColor(on ? Color.zxPurple:Color(hex:"#F0F0FF",opacity:0.35))}}.frame(maxWidth:.infinity)}}.padding(.top,6).padding(.bottom,34).frame(height:83).background(.ultraThinMaterial).background(Color.zxBg0.opacity(0.95)).overlay(alignment:.top){Rectangle().fill(Color.zxBorder008).frame(height:1)}} + var body: some View{HStack(spacing:0){ForEach(items,id:\.0){item in let on=item.0==active;Button{active=item.0}label:{VStack(spacing:4){ZStack{if on{Circle().fill(Color.zxPurple.opacity(0.2)).frame(width:28,height:28).scaleEffect(1.4)};Image(systemName:item.2).font(.system(size:22,weight:on ? .semibold:.regular)).foregroundColor(on ? Color.zxPurple:Color.zxF03)};Text(item.1).font(.system(size:10,weight:on ? .semibold:.regular)).foregroundColor(on ? Color.zxPurple:Color.zxF03)}}.frame(maxWidth:.infinity)}}.padding(.top,6).padding(.bottom,34).frame(height:83).background(.ultraThinMaterial).background(Color.zxBg0.opacity(0.95)).overlay(alignment:.top){Rectangle().fill(Color.zxBorder008).frame(height:1)}} } struct ZXIconBtn: View { diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift index 487c0ed..efac9f2 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -120,19 +120,17 @@ struct UpdateUserRequest: Codable { struct KnowledgeBase: Codable, Identifiable { let id: String - let name: String + let userId: String? + let title: String let description: String? - let icon: String? + let status: String? let itemCount: Int? - let mastery: Double? - let tags: [String]? + let lastStudiedAt: String? let createdAt: String? + let updatedAt: String? } -struct KnowledgeBaseListResponse: Codable { - let success: Bool - let data: [KnowledgeBase]? -} +typealias KnowledgeBaseListResponse = [KnowledgeBase] struct CreateKnowledgeBaseRequest: Codable { let name: String diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift index 676f52d..3240bcc 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift @@ -8,6 +8,7 @@ struct AIHomeView: View { @State private var text = "" @State private var serverStatus: ServerStatus = .checking @State private var knowledgeCount = 0 + @State private var navigateToChat = false enum ServerStatus { case checking, online, offline } @@ -25,7 +26,6 @@ struct AIHomeView: View { } Spacer() - // API 状态指示器 HStack(spacing: 4) { Circle() .fill(serverStatus == .online ? Color.zxGreen @@ -60,6 +60,8 @@ struct AIHomeView: View { inputBar } + + NavigationLink(destination: AIChatPage(), isActive: $navigateToChat) { EmptyView() } } .task { await checkServer() } } @@ -68,7 +70,7 @@ struct AIHomeView: View { serverStatus = .checking do { let resp: KnowledgeBaseListResponse = try await APIClient.shared.request("/knowledge-bases") - let count = resp.data?.count ?? 0 + let count = resp.count knowledgeCount = count serverStatus = .online } catch { @@ -102,10 +104,18 @@ struct AIHomeView: View { private var quickActions: some View { HStack(spacing:12){ - ZXQuickAction(emoji:"🧠",label:"生成\n回忆测试") - ZXQuickAction(emoji:"🔍",label:"分析\n薄弱点") - ZXQuickAction(emoji:"🎤",label:"费曼\n解释练习") - ZXQuickAction(emoji:"📅",label:"今日\n复习计划") + NavigationLink(destination: ActiveRecallView()) { + ZXQuickAction(emoji:"🧠",label:"生成\n回忆测试") + }.foregroundColor(.primary) + NavigationLink(destination: WeakPointsPage()) { + ZXQuickAction(emoji:"🔍",label:"分析\n薄弱点") + }.foregroundColor(.primary) + NavigationLink(destination: AIChatPage()) { + ZXQuickAction(emoji:"🎤",label:"费曼\n解释练习") + }.foregroundColor(.primary) + NavigationLink(destination: ReviewCardView()) { + ZXQuickAction(emoji:"📅",label:"今日\n复习计划") + }.foregroundColor(.primary) } } @@ -116,11 +126,11 @@ struct AIHomeView: View { Spacer();Text("全部").font(.system(size:12)).foregroundColor(Color.zxPurple) } ZXAIInteractionRow(tag:"费曼复习",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxPurple,emoji:"🎤", - title:"解释量子纠缠的核心概念",time:"2小时前",score:82){} + title:"解释量子纠缠的核心概念",time:"2小时前",score:82){ navigateToChat = true } ZXAIInteractionRow(tag:"薄弱点",bg:Color(hex:"#F97316",opacity:0.15),fg:Color(hex:"#FBA574"),emoji:"⚠️", - title:"混淆了协方差和相关系数",time:"昨天",score:56){} + title:"混淆了协方差和相关系数",time:"昨天",score:56){ navigateToChat = true } ZXAIInteractionRow(tag:"回忆测试",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxAccent,emoji:"📝", - title:"机器学习中的偏差-方差权衡",time:"2天前",score:91){} + title:"机器学习中的偏差-方差权衡",time:"2天前",score:91){ navigateToChat = true } } } @@ -128,13 +138,15 @@ struct AIHomeView: View { VStack(alignment:.leading,spacing:10){ Text("💡 你可以问 AI").font(.system(size:12,weight:.semibold)).foregroundColor(Color.zxF04) ForEach(["\"帮我测试机器学习这章的掌握情况\"","\"我最近的薄弱知识点有哪些?\"","\"生成一份本周的复习计划\""],id:\.self){s in - HStack{ - Text(s).font(.system(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)) - } - .padding(.horizontal,12).padding(.vertical,8) - .background(Color(hex:"#7C6EFA",opacity:0.06)).clipShape(RoundedRectangle(cornerRadius:10)) + Button { text = s; navigateToChat = true } label: { + HStack{ + Text(s).font(.system(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)) + } + .padding(.horizontal,12).padding(.vertical,8) + .background(Color(hex:"#7C6EFA",opacity:0.06)).clipShape(RoundedRectangle(cornerRadius:10)) + }.foregroundColor(.primary) } } .padding(14).padding(.horizontal,2) @@ -149,7 +161,7 @@ struct AIHomeView: View { TextField("问 AI 任何学习问题…",text:$text).font(.system(size:14)).tint(Color.zxPurple) Spacer() Image(systemName:"mic.fill").font(.system(size:18)).foregroundColor(Color.zxF03) - Button{}label:{ + 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)) } @@ -166,7 +178,7 @@ struct AIHomeView: View { struct ZXQuickAction: View { let emoji: String let label: String - + var body: some View { VStack(spacing:6){ Text(emoji).font(.system(size:22)) @@ -187,7 +199,7 @@ struct ZXAIInteractionRow: View { let time: String let score: Int let action: () -> Void - + var body: some View { Button(action: action) { HStack(spacing:12){ diff --git a/AIStudyApp/AIStudyApp/Features/AI/ActiveRecallView.swift b/AIStudyApp/AIStudyApp/Features/AI/ActiveRecallView.swift new file mode 100644 index 0000000..6058889 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/AI/ActiveRecallView.swift @@ -0,0 +1,188 @@ +import SwiftUI + +struct ActiveRecallView: View { + let questions: [RecallQuestion] = [ + .init(id: "1", question: "请解释贝叶斯定理的核心思想,并写出公式", source: "机器学习 · 概率论", isVoice: false), + .init(id: "2", question: "请用自己的话解释梯度下降算法的工作原理", source: "机器学习 · 优化算法", isVoice: false), + .init(id: "3", question: "用费曼学习法解释「过拟合与欠拟合」的区别", source: "机器学习 · 模型选择", isVoice: true), + ] + + @State private var idx = 0 + @State private var answers: [String: String] = [:] + @State private var currentAnswer = "" + @State private var submitted: Set = [] + @State private var showFinish = false + + var current: RecallQuestion { questions[idx] } + + var body: some View { + ZStack { + Color.zxBg0.ignoresSafeArea() + VStack(spacing: 0) { + progressHeader + ScrollView { + VStack(spacing: 16) { + questionCard + if !isSubmitted { + answerInput + } else { + submittedView + } + } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 120) + } + .scrollIndicators(.hidden) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + } + + private var isSubmitted: Bool { submitted.contains(current.id) } + + private var progressHeader: some View { + VStack(spacing: 8) { + HStack { + Text("主动回忆 \(idx + 1)/\(questions.count)") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.zxF04) + Spacer() + Text("已答 \(submitted.count)") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.zxPurple) + } + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2).fill(Color.zxFill008).frame(height: 3) + RoundedRectangle(cornerRadius: 2) + .fill(ZXGradient.progressBar) + .frame(width: max(3, CGFloat(idx + 1) / CGFloat(questions.count) * (UIScreen.main.bounds.width - 40)), height: 3) + } + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 4) + } + + private var questionCard: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 8) { + if current.isVoice { + Image(systemName: "mic.fill").font(.system(size: 12)).foregroundColor(Color.zxOrange) + Text("语音题").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxOrange) + .padding(.horizontal, 6).padding(.vertical, 2).background(Color.zxOrangeBG(0.1)).clipShape(Capsule()) + } else { + Image(systemName: "pencil.line").font(.system(size: 12)).foregroundColor(Color.zxPurple) + Text("文字题").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple) + .padding(.horizontal, 6).padding(.vertical, 2).background(Color.zxPurpleBG(0.1)).clipShape(Capsule()) + } + Spacer() + Text(current.source).font(.system(size: 10)).foregroundColor(Color.zxF03) + } + Text(current.question) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color.zxF0) + .lineSpacing(5) + } + .padding(16) + .background(ZXGradient.thinkingCard) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + private var answerInput: some View { + VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("你的回答").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035) + if current.isVoice { + voiceInputArea + } else { + TextEditor(text: $currentAnswer) + .font(.system(size: 13)) + .foregroundColor(Color.zxF0) + .tint(Color.zxPurple) + .frame(minHeight: 150) + .scrollContentBackground(.hidden) + .padding(12) + .background(Color.zxFill004) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) + } + } + Button { + answers[current.id] = current.isVoice ? "语音答案已录制" : currentAnswer + submitted.insert(current.id) + currentAnswer = "" + } label: { + Text("提交回答") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity).frame(height: 52) + .background(ZXGradient.ctaPurple) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + .disabled(currentAnswer.isEmpty && !current.isVoice) + .opacity(currentAnswer.isEmpty && !current.isVoice ? 0.5 : 1) + } + } + + private var voiceInputArea: some View { + VStack(spacing: 12) { + ZStack { + Circle().fill(Color.zxOrangeBG(0.1)).frame(width: 80, height: 80) + Image(systemName: "mic.fill").font(.system(size: 32)).foregroundColor(Color.zxOrange) + } + Text("点击按钮开始录音,用费曼方法口头解释").font(.system(size: 12)).foregroundColor(Color.zxF04) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + .background(Color.zxFill004) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) + } + + private var submittedView: some View { + VStack(spacing: 16) { + HStack(spacing: 10) { + Image(systemName: "checkmark.circle.fill").font(.system(size: 22)).foregroundColor(Color.zxGreen) + VStack(alignment: .leading, spacing: 3) { + Text("回答已提交").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxGreen) + Text("AI 分析中,稍后可查看反馈").font(.system(size: 12)).foregroundColor(Color.zxF04) + } + Spacer() + } + .padding(16) + .background(Color.zxGreenBG(0.06)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "#34D399", opacity: 0.15), lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 14)) + + if idx < questions.count - 1 { + Button { idx += 1 } label: { + Label("下一题", systemImage: "arrow.right") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity).frame(height: 52) + .background(ZXGradient.brand) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } else { + NavigationLink(destination: AIFeedbackPageView()) { + Label("查看 AI 分析结果", systemImage: "sparkles") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity).frame(height: 52) + .background(ZXGradient.ctaPurple) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + } + } +} + +struct RecallQuestion: Identifiable { + let id: String + let question: String + let source: String + let isVoice: Bool +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift index f16a519..5ccad7e 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift @@ -11,16 +11,36 @@ struct DailyThinkingPage: View { }.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) } } - }.padding(.horizontal,20).padding(.top,60+ZXSpacing.statusBarH).padding(.bottom,120) }.scrollIndicators(.hidden) + }.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));Button{}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,60+ZXSpacing.statusBarH).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,60+ZXSpacing.statusBarH).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} } -struct AIFeedbackPageView: View { var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();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))}} - Button{}label:{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){ZXOutlineBtn(text:"深入提问");ZXOutlineBtn(text:"再来一题")} - }.padding(.horizontal,20).padding(.top,60+ZXSpacing.statusBarH).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} } -struct ZXOutlineBtn: View {let text:String;var body: some View {Button{}label:{HStack(spacing:4){Text(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))}}} -struct AIChatPage: View { @State private var msg="";@State private var msgs:[(String,String)]=[("ai","你好!我是你的 AI 学习助手。")]; var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();VStack(spacing:0){ScrollViewReader{proxy in ScrollView{VStack(spacing:16){ForEach(Array(msgs.enumerated()),id:\.offset){i,m in HStack(alignment:.top,spacing:8){if m.0=="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.1).font(.system(size:14)).foregroundColor(m.0=="user" ? .white:Color.zxF007).padding(12).background(m.0=="user" ? AnyView(ZXGradient.brandPurple):AnyView(Color.zxFill004)).clipShape(RoundedRectangle(cornerRadius:16));if m.0=="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.0=="user" ? .trailing:.leading)}}.padding(.horizontal,20).padding(.top,60+ZXSpacing.statusBarH).padding(.bottom,100).id("bottom")}.scrollIndicators(.hidden).onChange(of:msgs.count){withAnimation{proxy.scrollTo("bottom")}}};ZXAIInputBar(text:$msg,onSend:{guard !msg.isEmpty else{return};msgs.append(("user",msg));msg="";DispatchQueue.main.asyncAfter(deadline:.now()+1){msgs.append(("ai","好的,我理解你的问题。需要我帮你制定学习计划吗?"))}})}}.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){ + NavigationLink(destination: KnowledgeDetailPage()) { ZXWeakRow(score:32,topic:"贝叶斯定理应用",lib:"机器学习",priority:"高") }.foregroundColor(.primary) + NavigationLink(destination: KnowledgeDetailPage()) { ZXWeakRow(score:41,topic:"正态分布性质",lib:"高等数学",priority:"高") }.foregroundColor(.primary) + 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 + var body: some View { + ZStack{Color.zxBg0.ignoresSafeArea();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)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar) + } +} +struct AIChatPage: View { @State private var msg="";@State private var msgs:[(String,String)]=[("ai","你好!我是你的 AI 学习助手。")]; var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();VStack(spacing:0){ScrollViewReader{proxy in ScrollView{VStack(spacing:16){ForEach(Array(msgs.enumerated()),id:\.offset){i,m in HStack(alignment:.top,spacing:8){if m.0=="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.1).font(.system(size:14)).foregroundColor(m.0=="user" ? .white:Color.zxF007).padding(12).background(m.0=="user" ? AnyView(ZXGradient.brandPurple):AnyView(Color.zxFill004)).clipShape(RoundedRectangle(cornerRadius:16));if m.0=="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.0=="user" ? .trailing:.leading)}}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,100).id("bottom")}.scrollIndicators(.hidden).onChange(of:msgs.count){withAnimation{proxy.scrollTo("bottom")}}};ZXAIInputBar(text:$msg,onSend:{guard !msg.isEmpty else{return};msgs.append(("user",msg));msg="";DispatchQueue.main.asyncAfter(deadline:.now()+1){msgs.append(("ai","好的,我理解你的问题。需要我帮你制定学习计划吗?"))}})}}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} } diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index 711fb70..a0ff28f 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -25,9 +25,9 @@ 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(); Text("全部 23 个").font(.system(size: 12)).foregroundColor(Color.zxPurple) } - ZXWeakRow(score: 32, topic: "贝叶斯定理应用", lib: "机器学习", priority: "高") - ZXWeakRow(score: 41, topic: "正态分布性质", lib: "高等数学", priority: "高") + 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("全部 23 个").font(.system(size: 12)).foregroundColor(Color.zxPurple) } } + NavigationLink(destination: KnowledgeDetailPage()) { ZXWeakRow(score: 32, topic: "贝叶斯定理应用", lib: "机器学习", priority: "高") }.foregroundColor(.primary) + NavigationLink(destination: KnowledgeDetailPage()) { ZXWeakRow(score: 41, topic: "正态分布性质", lib: "高等数学", priority: "高") }.foregroundColor(.primary) ZXWeakRow(score: 55, topic: "词根 spect- 相关词汇", lib: "英语词汇", priority: "中") } }.padding(.horizontal, 20).padding(.bottom, 120) diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift b/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift index eda2e1f..b0d4ec0 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibraryHomeView.swift @@ -9,7 +9,19 @@ struct LibraryHomeView: View { var body: some View { ZStack { ZXGradient.page.ignoresSafeArea() VStack(spacing: 0) { - HStack { Text("知识库").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5); Spacer(); ZXIconBtn(icon: "magnifyingglass", size: 36) {}; ZXIconBtn(icon: "plus", size: 36, branded: true) {} } + HStack { Text("知识库").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5); Spacer() + NavigationLink(destination: LibrarySearchView()) { + 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)) + } + NavigationLink(destination: ImportPage()) { + Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white) + .frame(width: 36, height: 36).background(ZXGradient.brand) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } .padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 12) HStack(spacing: 8) { Image(systemName: "magnifyingglass").font(.system(size: 16)).foregroundColor(Color.zxF03); TextField("搜索知识库或知识点…", text: $s).font(.system(size: 14)).tint(Color.zxPurple) } .padding(.horizontal, 14).frame(height: 44).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).padding(.horizontal, 20).padding(.bottom, 16) @@ -33,3 +45,24 @@ struct ZLibraryCard: View { let emoji: String; let name: String; let desc: Strin 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) } .background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) } } + +struct LibrarySearchView: View { + @State private var query = "" + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea() + VStack(spacing: 0) { + HStack(spacing: 8) { Image(systemName: "magnifyingglass").font(.system(size: 16)).foregroundColor(Color.zxF03); TextField("搜索知识库或知识点…", text: $query).font(.system(size: 14)).tint(Color.zxPurple) } + .padding(.horizontal, 14).frame(height: 44).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)) + .padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 16) + ScrollView { VStack(spacing: 12) { + if query.isEmpty { + VStack(spacing: 12) { + Image(systemName: "magnifyingglass").font(.system(size: 36)).foregroundColor(Color.zxF03) + Text("搜索知识点、知识库或标签").font(.system(size: 13)).foregroundColor(Color.zxF03) + }.padding(.top, 80) + } + }.padding(.horizontal, 20) }.scrollIndicators(.hidden) + } + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift index dea71b1..24ad5a2 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -7,21 +7,27 @@ struct CreateLibraryPage: View { ScrollView { VStack(spacing: 20) { VStack(alignment: .leading, spacing: 8) { Text("知识库名称").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("例如:机器学习", text: $name).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } VStack(alignment: .leading, spacing: 8) { Text("描述(可选)").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("简单描述这个知识库的内容", text: $desc).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } - Button {} label: { Text("创建").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16)) } + Button { } 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, 20) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)} } struct LibraryDetailPage: View { var body: some View { ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + HStack { Spacer() + NavigationLink(destination: AddKnowledgePage()) { + Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white) + .frame(width: 36, height: 36).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10)) + } + }.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 8) ScrollView { VStack(spacing: 12) { NavigationLink(destination: KnowledgeDetailPage()) { ZXCardRow(emoji: "📝", title: "偏差-方差权衡", desc: "模型复杂度 · 泛化误差", status: "已掌握", c: Color.zxGreen) } NavigationLink(destination: KnowledgeDetailPage()) { ZXCardRow(emoji: "📝", title: "梯度下降优化", desc: "SGD · Adam · 学习率", status: "学习中", c: Color.zxOrange) } NavigationLink(destination: KnowledgeDetailPage()) { ZXCardRow(emoji: "📝", title: "正则化方法", desc: "L1 · L2 · Dropout", status: "待复习", c: Color.zxYellow) } NavigationLink(destination: KnowledgeDetailPage()) { ZXCardRow(emoji: "📝", title: "过拟合与欠拟合", desc: "偏差方差 · 模型选择", status: "已掌握", c: Color.zxGreen) } }.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)} } 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()) } @@ -35,19 +41,34 @@ struct AddKnowledgePage: View { ScrollView { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("标题").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("输入知识点标题", text: $title).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } VStack(alignment: .leading, spacing: 8) { Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextEditor(text: $content).frame(minHeight: 200).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } - Button {} 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(.bottom, 80) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + Button { } 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 KnowledgeDetailPage: View { var body: some View { ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + HStack { Spacer() + NavigationLink(destination: EditKnowledgePage()) { + 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)) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1)) + } + }.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 8) ScrollView { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 8) { HStack { ZXChip(text: "算法", color: Color.zxPurple); ZXChip(text: "机器学习", color: Color.zxAccent); ZXChip(text: "需要复习", color: Color.zxYellow) }; Text("偏差-方差权衡").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0); Text("偏差-方差权衡是机器学习模型选择的核心理念。").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) { Button {} label: { 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)) }; Button {} label: { 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)) } } + HStack(spacing: 12) { + NavigationLink(destination: StudyHomeView()) { + 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()) { + 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)) + } + } }.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)} } struct ZXChip: View { let text: String; let color: Color var body: some View { Text(text).font(.system(size: 10, weight: .semibold)).foregroundColor(color).padding(.horizontal, 8).padding(.vertical, 2).background(color.opacity(0.12)).clipShape(Capsule()) } @@ -61,11 +82,11 @@ struct ImportPage: View { ZXImportOption(icon: "doc.text.fill", title: "文件导入", desc: "支持 PDF、Word、Markdown") ZXImportOption(icon: "link", title: "链接导入", desc: "粘贴网页链接,自动提取内容") ZXImportOption(icon: "photo.on.rectangle", title: "相册导入", desc: "从相册选择截图或图片") - }.padding(.horizontal, 20).padding(.top, 16) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + }.padding(.horizontal, 20).padding(.top, 8) }.scrollIndicators(.hidden) } + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)} } struct ZXImportOption: View { let icon: String; let title: String; let desc: String - var body: some View { Button {} label: { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(Color.zxPurple).frame(width: 48, height: 48).background(Color.zxPurpleBG(0.1)).clipShape(RoundedRectangle(cornerRadius: 14)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) }.foregroundColor(.primary) } + var body: some View { Button { } label: { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(Color.zxPurple).frame(width: 48, height: 48).background(Color.zxPurpleBG(0.1)).clipShape(RoundedRectangle(cornerRadius: 14)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) }.foregroundColor(.primary) } } struct EditKnowledgePage: View { @@ -75,7 +96,7 @@ struct EditKnowledgePage: View { ScrollView { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("标题").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("", text: $title).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } VStack(alignment: .leading, spacing: 8) { Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextEditor(text: $content).frame(minHeight: 200).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } - Button {} 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(.bottom, 80) }.scrollIndicators(.hidden) } - }.navigationBarHidden(true)} + Button { } 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)} } diff --git a/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift b/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift new file mode 100644 index 0000000..1043c02 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct NotificationListView: View { + @State private var notifications: [NotificationItem] = [ + .init(type: "review", title: "复习提醒", content: "你有 8 个知识点需要复习", time: "刚刚", read: false), + .init(type: "ai", title: "AI 分析完成", content: "\"机器学习基础\"薄弱点分析已完成", time: "1小时前", read: false), + .init(type: "streak", title: "学习成就", content: "恭喜!你已连续学习 14 天 🔥", time: "昨天", read: true), + .init(type: "review", title: "复习提醒", content: "今天有 3 个知识点需要费曼解释练习", time: "2天前", read: true), + .init(type: "system", title: "系统通知", content: "v1.0 版本已更新,新增间隔复习功能", time: "3天前", read: true), + ] + + var body: some View { + ZStack { + Color.zxBg0.ignoresSafeArea() + ScrollView { + VStack(spacing: 0) { + if notifications.isEmpty { + VStack(spacing: 12) { + Image(systemName: "bell.slash").font(.system(size: 40)).foregroundColor(Color.zxF03) + Text("暂无通知").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF03) + }.padding(.top, 120) + } else { + ForEach(Array(notifications.enumerated()), id: \.offset) { i, n in + ZXNotificationRow(item: n) { + notifications[i].read = true + } + if i < notifications.count - 1 { + ZXSettingDivider() + } + } + } + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + .padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100) + }.scrollIndicators(.hidden) + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar) + } +} + +struct NotificationItem: Identifiable { + let id = UUID() + let type: String + let title: String + let content: String + let time: String + var read: Bool +} + +struct ZXNotificationRow: View { + let item: NotificationItem + let onTap: () -> Void + + private var iconName: String { + switch item.type { + case "review": return "arrow.triangle.2.circlepath" + case "ai": return "sparkles" + case "streak": return "flame.fill" + default: return "bell.fill" + } + } + + private var iconColor: Color { + switch item.type { + case "review": return Color.zxOrange + case "ai": return Color.zxPurple + case "streak": return Color.zxGreen + default: return Color.zxAccent + } + } + + var body: some View { + Button(action: onTap) { + HStack(spacing: 12) { + Image(systemName: iconName).font(.system(size: 16)).foregroundColor(iconColor) + .frame(width: 36, height: 36).background(iconColor.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 10)) + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(item.title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + if !item.read { + Circle().fill(Color.zxPurple).frame(width: 6, height: 6) + } + } + Text(item.content).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(2) + Text(item.time).font(.system(size: 10)).foregroundColor(Color.zxF03) + } + Spacer() + Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.horizontal, 16).padding(.vertical, 14) + }.foregroundColor(.primary) + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift index 95240ee..1ea79e2 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift @@ -9,16 +9,36 @@ struct ProfileView: View { HStack { Text("我的").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5) Spacer() - ZXIconBtn(icon: "bell", size: 36) {} - ZXIconBtn(icon: "gearshape", size: 36) {} + NavigationLink(destination: NotificationListView()) { + 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)) + } + NavigationLink(destination: SettingsView()) { + 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)) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1)) + } }.padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4) profileCard VStack(spacing: 0) { - ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标") - ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置") - ZXProfileMenuRow(emoji: "📊", title: "学习报告", desc: "周报 · 月报 · 成就") - ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔") - ZXProfileMenuRow(emoji: "☁️", title: "数据同步与备份", desc: "云端同步设置") + NavigationLink(destination: GoalSettingDetailView()) { + ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标") + }.foregroundColor(.primary) + ZXProfileDivider() + NavigationLink(destination: SettingsView()) { + ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置") + }.foregroundColor(.primary) + ZXProfileDivider() + NavigationLink(destination: MethodPreferenceView()) { + ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔") + }.foregroundColor(.primary) + ZXProfileDivider() + NavigationLink(destination: FeedbackFormView()) { + ZXProfileMenuRow(emoji: "💬", title: "帮助与反馈", desc: "问题报告 · 功能建议") + }.foregroundColor(.primary) }.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) achievementsSection.padding(.bottom, 120) }.padding(.horizontal, 20) @@ -26,14 +46,16 @@ struct ProfileView: View { } } private var profileCard: some View { - VStack(spacing: 16) { - HStack { - ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Text("🧑‍🎓").font(.system(size: 36)) } - VStack(alignment: .leading, spacing: 4) { Text("学习者").font(.system(size: 20, weight: .bold)).foregroundColor(Color.zxF0); Text("user@example.com").font(.system(size: 12)).foregroundColor(Color.zxF04) } - Spacer(); Image(systemName: "chevron.right").font(.system(size: 14)).foregroundColor(Color.zxF03) - } - HStack(spacing: 0) { ZXProfileStat(value: "14", label: "连续天", color: Color.zxOrange); ZXProfileStat(value: "47", label: "知识点", color: Color.zxPurple); ZXProfileStat(value: "1,240", label: "积分", color: Color.zxTeal) } - }.padding(20).background(ZXGradient.profileCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) + NavigationLink(destination: SettingsView()) { + VStack(spacing: 16) { + HStack { + ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Text("🧑‍🎓").font(.system(size: 36)) } + VStack(alignment: .leading, spacing: 4) { Text("学习者").font(.system(size: 20, weight: .bold)).foregroundColor(Color.zxF0); Text("user@example.com").font(.system(size: 12)).foregroundColor(Color.zxF04) } + Spacer(); Image(systemName: "chevron.right").font(.system(size: 14)).foregroundColor(Color.zxF03) + } + HStack(spacing: 0) { ZXProfileStat(value: "14", label: "连续天", color: Color.zxOrange); ZXProfileStat(value: "47", label: "知识点", color: Color.zxPurple); ZXProfileStat(value: "1,240", label: "积分", color: Color.zxTeal) } + }.padding(20).background(ZXGradient.profileCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) + }.foregroundColor(.primary) } private var achievementsSection: some View { VStack(alignment: .leading, spacing: 12) { @@ -48,6 +70,9 @@ struct ZXProfileStat: View { let v: String; let l: String; let c: Color; var bod 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) } } +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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift b/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift new file mode 100644 index 0000000..e425238 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Profile/SettingsView.swift @@ -0,0 +1,236 @@ +import SwiftUI + +struct SettingsView: View { + @State private var language = "zh-Hans" + @AppStorage("appAppearance") private var appearance = "system" + @State private var reviewReminder = true + @State private var reminderTime = "20:00" + @State private var intervalDays = "1" + @State private var iCloudSync = false + @State private var autoBackup = false + + var body: some View { + ZStack { + Color.zxBg0.ignoresSafeArea() + ScrollView { + VStack(spacing: 16) { + sectionHeader("外观与语言") + VStack(spacing: 0) { + ZXSettingRow(title: "外观", value: appearanceLabel, icon: "moon.stars.fill", color: Color.zxPurple) + .contentShape(Rectangle()) + .onTapGesture { toggleAppearance() } + ZXSettingDivider() + ZXSettingRow(title: "语言", value: "简体中文", icon: "globe", color: Color.zxTeal) + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + + sectionHeader("学习设置") + VStack(spacing: 0) { + NavigationLink(destination: GoalSettingDetailView()) { + ZXSettingRow(title: "学习目标", value: "备考考试", icon: "target", color: Color.zxOrange) + }.foregroundColor(.primary) + ZXSettingDivider() + NavigationLink(destination: MethodPreferenceView()) { + ZXSettingRow(title: "学习方法偏好", value: "间隔回忆 · 费曼技巧", icon: "brain.head.profile", color: Color.zxPurple) + }.foregroundColor(.primary) + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + + sectionHeader("复习提醒") + VStack(spacing: 0) { + ZXSettingToggleRow(title: "开启复习提醒", icon: "bell.badge.fill", color: Color.zxOrange, isOn: $reviewReminder) + if reviewReminder { + ZXSettingDivider() + ZXSettingPickerRow(title: "提醒时间", value: $reminderTime, options: ["08:00", "12:00", "18:00", "20:00", "21:00"]) + ZXSettingDivider() + ZXSettingPickerRow(title: "间隔天数", value: $intervalDays, options: ["1", "2", "3", "5", "7"]) + } + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + + sectionHeader("数据") + VStack(spacing: 0) { + ZXSettingToggleRow(title: "iCloud 同步", icon: "icloud.fill", color: Color.zxTeal, isOn: $iCloudSync) + ZXSettingDivider() + ZXSettingToggleRow(title: "自动备份", icon: "arrow.triangle.2.circlepath", color: Color.zxAccent, isOn: $autoBackup) + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + + VStack(spacing: 0) { + NavigationLink(destination: FeedbackFormView()) { + ZXSettingRow(title: "帮助与反馈", value: "", icon: "questionmark.circle.fill", color: Color.zxAccent) + }.foregroundColor(.primary) + ZXSettingDivider() + ZXSettingRow(title: "隐私政策", value: "", icon: "hand.raised.fill", color: Color.zxYellow) + ZXSettingDivider() + ZXSettingRow(title: "用户协议", value: "", icon: "doc.text.fill", color: Color.zxGreen) + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + + HStack(spacing: 4) { + Text("知习 v1.0").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.bottom, 100) + }.padding(.horizontal, 20).padding(.top, 8) + } + .scrollIndicators(.hidden) + } + .navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar) + } + + private func sectionHeader(_ text: String) -> some View { + Text(text).font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5).padding(.top, 4) + } + + private var appearanceLabel: String { + switch appearance { case "system": return "跟随系统"; case "dark": return "深色模式"; default: return "浅色模式" } + } + + private func toggleAppearance() { + switch appearance { + case "system": appearance = "dark" + case "dark": appearance = "light" + default: appearance = "system" + } + } +} + +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 {} 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 分析", "主动回忆"] + + 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 {} 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 FeedbackFormView: View { + @State private var type = "功能建议" + @State private var content = "" + 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 {} 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 ZXSettingRow: View { + let title: String; let value: String; let icon: String; let color: Color + var body: some View { + HStack(spacing: 12) { + Image(systemName: icon).font(.system(size: 16)).foregroundColor(color).frame(width: 32, height: 32).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 8)) + Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + Spacer() + if !value.isEmpty { Text(value).font(.system(size: 13)).foregroundColor(Color.zxF03) } + Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.horizontal, 16).padding(.vertical, 14) + } +} + +struct ZXSettingToggleRow: View { + let title: String; let icon: String; let color: Color; @Binding var isOn: Bool + var body: some View { + HStack(spacing: 12) { + Image(systemName: icon).font(.system(size: 16)).foregroundColor(color).frame(width: 32, height: 32).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 8)) + Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + Spacer() + Toggle("", isOn: $isOn).labelsHidden().tint(Color.zxPurple) + }.padding(.horizontal, 16).padding(.vertical, 14) + } +} + +struct ZXSettingPickerRow: View { + let title: String; @Binding var value: String; let options: [String] + var body: some View { + HStack(spacing: 12) { + Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).opacity(0.6) + Spacer() + Picker(title, selection: $value) { + ForEach(options, id: \.self) { o in Text(o).tag(o) } + }.pickerStyle(.segmented).frame(width: 200).tint(Color.zxPurple) + }.padding(.horizontal, 16).padding(.vertical, 10) + } +} + +struct ZXSettingDivider: View { + var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 60) } +} diff --git a/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift new file mode 100644 index 0000000..472b35c --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Study/LearningSessionView.swift @@ -0,0 +1,164 @@ +import SwiftUI + +struct LearningSessionView: View { + let taskTitle: String + let taskType: String + let taskColor: Color + + @State private var elapsed: TimeInterval = 0 + @State private var isRunning = true + @State private var isPaused = false + @State private var showEndConfirm = false + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + ZStack { + Color.zxBg0.ignoresSafeArea() + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 20) { + timerCard + sessionInfoCard + tipsCard + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 120) + } + .scrollIndicators(.hidden) + } + VStack { Spacer() + bottomBar + }.ignoresSafeArea(edges: .bottom) + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .onReceive(timer) { _ in + if isRunning { elapsed += 1 } + } + .confirmationDialog("结束学习?", isPresented: $showEndConfirm, titleVisibility: .visible) { + Button("结束并保存", role: .destructive) { isRunning = false } + Button("继续学习", role: .cancel) {} + } + } + + private var timerCard: some View { + VStack(spacing: 16) { + ZStack { + Circle() + .trim(from: 0, to: min(elapsed / 1800, 1)) + .stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .frame(width: 180, height: 180) + VStack(spacing: 4) { + Text(formatTime(elapsed)) + .font(.system(size: 36, weight: .black)) + .foregroundColor(Color.zxF0) + .tracking(-1) + Text("已学习") + .font(.system(size: 13, weight: .medium)) + .foregroundColor(Color.zxF04) + } + } + HStack(spacing: 12) { + Button { + if isRunning { isPaused = true; isRunning = false } + else { isPaused = false; isRunning = true } + } label: { + Label(isRunning ? "暂停" : "继续", systemImage: isRunning ? "pause.fill" : "play.fill") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity).frame(height: 48) + .background(ZXGradient.brandPurple) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } + Button { showEndConfirm = true } label: { + Label("结束", systemImage: "stop.fill") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color.zxF05) + .frame(maxWidth: .infinity).frame(height: 48) + .background(Color.zxFill005) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } + } + } + .padding(24) + .background(ZXGradient.progressCard) + .overlay(RoundedRectangle(cornerRadius: 24).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 24)) + } + + private var sessionInfoCard: some View { + VStack(spacing: 0) { + ZXSessionInfoRow(icon: "doc.text.fill", label: "当前任务", value: taskTitle, color: taskColor) + ZXSessionDivider() + ZXSessionInfoRow(icon: "tag.fill", label: "任务类型", value: taskType, color: taskColor) + ZXSessionDivider() + ZXSessionInfoRow(icon: "target", label: "建议时长", value: "30 分钟", color: Color(hex: "#7C6EFA")) + ZXSessionDivider() + ZXSessionInfoRow(icon: "chart.line.uptrend.xyaxis", label: "今日已学", value: "\(Int(elapsed / 60)) 分钟", color: Color.zxGreen) + } + .background(Color.zxFill003) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) + } + + private var tipsCard: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Image(systemName: "lightbulb.fill").font(.system(size: 14)).foregroundColor(Color.zxYellow) + Text("学习小贴士").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0) + } + Text("保持专注,25-30 分钟后休息 5 分钟能有效提升记忆效果。学习时尽量避免切换任务。") + .font(.system(size: 12)).foregroundColor(Color.zxF04).lineSpacing(4) + } + .padding(16) + .background(Color.zxFill004) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + private var bottomBar: some View { + HStack(spacing: 12) { + if isRunning { + HStack(spacing: 6) { + Circle().fill(Color.zxGreen).frame(width: 6, height: 6) + 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) + Text("已暂停").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxYellow) + } + } + Spacer() + } + .padding(.horizontal, 24).padding(.vertical, 14) + .background(.ultraThinMaterial) + .background(Color.zxBg0.opacity(0.95)) + .overlay(alignment: .top) { Rectangle().fill(Color.zxBorder008).frame(height: 1) } + } + + private func formatTime(_ t: TimeInterval) -> String { + let m = Int(t) / 60, s = Int(t) % 60 + return String(format: "%02d:%02d", m, s) + } +} + +struct ZXSessionInfoRow: View { + let icon: String; let label: String; let value: String; let color: Color + var body: some View { + HStack(spacing: 12) { + Image(systemName: icon).font(.system(size: 16)).foregroundColor(color) + .frame(width: 32, height: 32).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 8)) + Text(label).font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF04) + Spacer() + Text(value).font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF0).lineLimit(1) + }.padding(.horizontal, 16).padding(.vertical, 14) + } +} + +struct ZXSessionDivider: View { + var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 60) } +} diff --git a/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift new file mode 100644 index 0000000..cbdd804 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Study/ReviewCardView.swift @@ -0,0 +1,159 @@ +import SwiftUI + +struct ReviewCardView: View { + let cards: [ReviewCardItem] = [ + .init(question: "什么是偏差(Bias)和方差(Variance)的权衡?", + answer: "偏差衡量模型预测与真实值的偏离程度,方差衡量模型在不同训练集上的预测波动。偏差-方差权衡指的是:简单模型偏差高方差低(欠拟合),复杂模型偏差低方差高(过拟合)。最佳模型需要在两者之间取得平衡。", + source: "机器学习 · 偏差-方差权衡", count: 1, total: 8), + .init(question: "梯度下降中学习率(learning rate)的作用是什么?", + answer: "学习率控制每次参数更新的步长。太大的学习率会导致不收敛甚至发散;太小的学习率会导致收敛速度过慢。通常从较大值开始逐步衰减,或使用自适应学习率算法如Adam。", + source: "机器学习 · 梯度下降优化", count: 2, total: 8), + .init(question: "L1正则化和L2正则化有什么区别?", + answer: "L1正则化(权重绝对值之和)倾向于产生稀疏解,可用于特征选择;L2正则化(权重平方和)倾向于让所有权重都接近零但不等於零,防止过拟合效果更好。", + source: "机器学习 · 正则化方法", count: 3, total: 8), + ] + + @State private var idx = 0 + @State private var flipped = false + @State private var rating: Int? = nil + @State private var finish = false + + var current: ReviewCardItem { cards[idx] } + + var body: some View { + ZStack { + Color.zxBg0.ignoresSafeArea() + VStack(spacing: 0) { + progressBar + ScrollView { + VStack(spacing: 20) { + flashCard + if flipped { ratingBar } + } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 40) + } + .scrollIndicators(.hidden) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + } + + private var progressBar: some View { + VStack(spacing: 8) { + HStack { + Text("间隔复习 \(current.count)/\(current.total)") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.zxF04) + Spacer() + Text("剩余 \(current.total - current.count + 1) 张") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.zxPurple) + } + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2).fill(Color.zxFill008).frame(height: 3) + RoundedRectangle(cornerRadius: 2) + .fill(ZXGradient.progressBar) + .frame(width: max(3, CGFloat(current.count) / CGFloat(current.total) * (UIScreen.main.bounds.width - 40)), height: 3) + } + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 4) + } + + private var flashCard: some View { + VStack(spacing: 0) { + Text(flipped ? "答案" : "问题") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(flipped ? Color.zxGreen : Color.zxAccent) + .tracking(0.5) + .padding(.horizontal, 10).padding(.vertical, 3) + .background((flipped ? Color.zxGreen : Color.zxPurple).opacity(0.12)) + .clipShape(Capsule()) + .padding(.bottom, 16) + + Text(flipped ? current.answer : current.question) + .font(.system(size: flipped ? 14 : 16, weight: flipped ? .medium : .semibold)) + .foregroundColor(Color.zxF0) + .lineSpacing(6) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + + if flipped { + VStack(spacing: 6) { + 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) + } + } + } else { + Text("点击翻转查看答案") + .font(.system(size: 11)) + .foregroundColor(Color.zxF03) + .padding(.top, 20) + } + } + .padding(24) + .frame(minHeight: 240) + .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() } } + } + + private var ratingBar: some View { + VStack(spacing: 10) { + Text("你的掌握程度?").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF04) + HStack(spacing: 10) { + ZXRatingBtn(label: "完全不会", color: Color.zxRed, selected: rating == 1) { rating = 1; nextCard() } + ZXRatingBtn(label: "有点难", color: Color.zxOrange, selected: rating == 2) { rating = 2; nextCard() } + ZXRatingBtn(label: "基本会", color: Color.zxYellow, selected: rating == 3) { rating = 3; nextCard() } + ZXRatingBtn(label: "很简单", color: Color.zxGreen, selected: rating == 4) { rating = 4; nextCard() } + } + HStack(spacing: 4) { + Image(systemName: "arrow.triangle.2.circlepath").font(.system(size: 10)).foregroundColor(Color.zxF03) + Text("AI 会根据你的评分自动安排下次复习时间").font(.system(size: 10)).foregroundColor(Color.zxF03) + } + } + } + + private func nextCard() { + rating = nil + flipped = false + if idx < cards.count - 1 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { idx += 1 } + } else { + finish = true + } + } +} + +struct ReviewCardItem { + let question: String + let answer: String + let source: String + let count: Int + let total: Int +} + +struct ZXRatingBtn: View { + let label: String; let color: Color; let selected: Bool; let action: () -> Void + var body: some View { + Button(action: action) { + VStack(spacing: 4) { + Text(label).font(.system(size: 11, weight: selected ? .bold : .medium)) + .foregroundColor(selected ? .white : Color.zxF05) + } + .frame(maxWidth: .infinity).frame(height: 56) + .background(selected ? AnyView(ZXGradient.brand) : 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 cf1ff04..89f514e 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -19,7 +19,20 @@ struct StudyHomeView: View { .padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4) pc 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($ts) { $t in ZXSTaskRow(task: t) { t.d.toggle() } } } + ForEach($ts) { $t in + if t.tp == "回忆测试" { + NavigationLink(destination: ActiveRecallView()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary) + } else if t.tp == "费曼练习" { + NavigationLink(destination: AIChatPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary) + } else if t.tp == "薄弱点" { + NavigationLink(destination: WeakPointsPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary) + } else if t.tp == "间隔复习" { + NavigationLink(destination: ReviewCardView()) { 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) + } + } + } VStack(alignment: .leading, spacing: 14) { Text("本周学习活跃").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) HStack(alignment: .bottom, spacing: 8) { ForEach(0..<7, id: \.self) { i in VStack(spacing: 8) { RoundedRectangle(cornerRadius: 6).fill(i == 6 ? Color.zxFill01 : Color(hex: "#7C6EFA", opacity: wb[i] * 0.9 + 0.1)).frame(height: wb[i] * 60); Text(dl[i]).font(.system(size: 10, weight: i == 2 ? .bold : .regular)).foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) }.frame(maxWidth: .infinity) } } HStack { Text("总计 3.5 小时").font(.system(size: 11)).foregroundColor(Color.zxF03); Spacer(); Text("日均 30 分钟").font(.system(size: 11)).foregroundColor(Color.zxF03) } } @@ -36,9 +49,12 @@ struct StudyHomeView: View { } struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let c: Color; let m: Int; var d: Bool } -struct ZXSTaskRow: View { let task: ZXSTask; var action: () -> Void - var body: some View { Button(action: action) { HStack(spacing: 12) { Image(systemName: task.d ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(task.d ? Color.zxGreen : Color.zxF02) +struct ZXSTaskRow: View { @Binding var task: ZXSTask + var body: some View { Button { task.d.toggle() } label: { ZXSTaskRowView(task: task) {} }.foregroundColor(.primary) } +} +struct ZXSTaskRowView: View { let task: ZXSTask; var action: () -> Void + var body: some View { HStack(spacing: 12) { Image(systemName: task.d ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(task.d ? Color.zxGreen : Color.zxF02) VStack(alignment: .leading, spacing: 4) { Text(task.t).font(.system(size: 13, weight: .semibold)).foregroundColor(task.d ? Color.zxF04 : Color.zxF0).strikethrough(task.d); HStack(spacing: 8) { Text(task.tp).font(.system(size: 10, weight: .semibold)).foregroundColor(task.c).padding(.horizontal, 6).padding(.vertical, 1).background(task.c.opacity(0.12)).clipShape(Capsule()); Text("约 \(task.m) 分钟").font(.system(size: 10)).foregroundColor(Color(hex:"#F0F0FF",opacity:0.35)) } } Spacer(); if !task.d { Image(systemName: "play.fill").font(.system(size: 14)).foregroundColor(.white).frame(width: 32, height: 32).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10)) } } - .padding(.horizontal, 16).padding(.vertical, 12).background(task.d ? Color.zxFill003 : Color.zxFill005).overlay(RoundedRectangle(cornerRadius: 14).stroke(task.d ? Color(hex: "#FFFFFF", opacity: 0.05) : Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).opacity(task.d ? 0.6 : 1) }.foregroundColor(.primary) } + .padding(.horizontal, 16).padding(.vertical, 12).background(task.d ? Color.zxFill003 : Color.zxFill005).overlay(RoundedRectangle(cornerRadius: 14).stroke(task.d ? Color(hex: "#FFFFFF", opacity: 0.05) : Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).opacity(task.d ? 0.6 : 1).contentShape(Rectangle()).onTapGesture { action() } } } diff --git a/AIStudyApp/Info.plist b/AIStudyApp/Info.plist new file mode 100644 index 0000000..7b6946c --- /dev/null +++ b/AIStudyApp/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AIStudyApp/docs/gap-analysis-1.md b/AIStudyApp/docs/gap-analysis-1.md new file mode 100644 index 0000000..32c5b98 --- /dev/null +++ b/AIStudyApp/docs/gap-analysis-1.md @@ -0,0 +1,157 @@ +# AIStudyApp 现状与缺口分析 - 第一篇:现有资源盘点 + +> 生成日期:2026-05-11 +> 后端地址:http://81.70.187.179:3001 + +--- + +## 一、项目文件结构 + +``` +AIStudyApp/ +├── AIStudyAppApp.swift # 应用入口,含 5 步 Onboarding 流程 +├── ContentView.swift # 主 Tab 框架(5 个 Tab + 自定义底部栏) +│ +├── Core/ +│ ├── DesignSystem/DesignTokens.swift # 颜色/渐变/间距/字体全局设计令牌 +│ ├── Models/APIModels.swift # 20+ DTO 数据模型 +│ ├── Network/ +│ │ ├── APIClient.swift # 通用 HTTP 客户端(actor, async/await) +│ │ ├── APIConfig.swift # baseURL 配置 +│ │ └── APIError.swift # 错误枚举(网络/服务端/解码/认证) +│ └── Services/APIService.swift # 8 个服务类,15 个公开方法 +│ +├── Features/ +│ ├── AI/ +│ │ ├── AIHomeView.swift # AI 首页 + ZXQuickAction + ZXAIInteractionRow 组件 +│ │ └── DailyThinkingPage.swift # 每日思考题 + RecallTestPage / WeakPointsPage / +│ │ # AIFeedbackPageView / AIChatPage 子页面 +│ ├── Analysis/ +│ │ └── AnalysisHomeView.swift # 学习分析页 + ZXChartView 折线图 + ZXWeakRow 薄弱点 +│ ├── Library/ +│ │ ├── LibraryHomeView.swift # 知识库列表首页 +│ │ └── LibrarySubpages.swift # CreateLibraryPage / LibraryDetailPage / +│ │ # AddKnowledgePage / KnowledgeDetailPage / +│ │ # ImportPage / EditKnowledgePage +│ ├── Profile/ +│ │ └── ProfileView.swift # 个人中心页 +│ └── Study/ +│ └── StudyHomeView.swift # 学习工作台 + 今日任务 + 周活跃柱状图 +│ +└── Info.plist # 手动管理(ATS例外 / Bundle元数据等) +``` + +--- + +## 二、5 个 Tab 页面清单 + +| Tab | 标签 | SF Symbol | View | +|-----|------|-----------|------| +| 1 | AI | `brain.head.profile` | AIHomeView | +| 2 | 知识库 | `books.vertical.fill` | LibraryHomeView | +| 3 | 学习 | `bolt.fill` | StudyHomeView | +| 4 | 分析 | `chart.bar.fill` | AnalysisHomeView | +| 5 | 我的 | `person.fill` | ProfileView | + +--- + +## 三、所有页面/子页面总览(共 21 个) + +### AI 模块(1 主 + 4 子) + +| 页面 | 数据来源 | 核心功能 | +|------|---------|---------| +| AIHomeView | 🔴硬编码 | API 状态检测、思考题卡片、快捷操作、互动记录、提问输入栏 | +| DailyThinkingPage | 🔴硬编码 | AI 思考题展示 + 回答提交 | +| RecallTestPage | 🔴硬编码 | 回忆测试输入 | +| WeakPointsPage | 🔴硬编码 | 薄弱知识点静态列表 | +| AIFeedbackPageView | 🔴硬编码 | AI 反馈评分 + 操作入口 | +| AIChatPage | 🔴硬编码 | AI 对话气泡界面 | + +### 知识库模块(1 主 + 6 子) + +| 页面 | 数据来源 | 核心功能 | +|------|---------|---------| +| LibraryHomeView | 🔴硬编码 | 知识库列表 + 搜索框 + 创建入口 | +| CreateLibraryPage | 🔴静态 | 创建表单(名称+描述) | +| LibraryDetailPage | 🔴硬编码 | 知识点静态列表 | +| AddKnowledgePage | 🔴静态 | 添加知识点表单 | +| KnowledgeDetailPage | 🔴硬编码 | 知识点详情+标签+复习/费曼入口 | +| ImportPage | 🔴静态 | 导入方式选择(拍照/文件/链接/相册) | +| EditKnowledgePage | 🔴静态 | 编辑知识点表单 | + +### 学习模块(1 主) + +| 页面 | 数据来源 | 核心功能 | +|------|---------|---------| +| StudyHomeView | 🔴硬编码 | 今日进度环、任务列表(5个任务)、本周活跃柱状图 | + +### 分析模块(1 主) + +| 页面 | 数据来源 | 核心功能 | +|------|---------|---------| +| AnalysisHomeView | 🔴硬编码 | 4项统计徽章、掌握度7日折线图、薄弱知识点列表 | + +### 个人中心(1 主) + +| 页面 | 数据来源 | 核心功能 | +|------|---------|---------| +| ProfileView | 🔴硬编码 | 个人卡片、菜单列表、成就徽章 | + +### 启动流程(5 步 Onboarding) + +| 步骤 | 页面 | 功能 | +|------|------|------| +| Step 0 | SplashPage | 品牌开屏,2 秒自动跳转 | +| Step 1 | WelcomePage | 3 大功能介绍 | +| Step 2 | LoginPage | 手机号/邮箱 + 密码表单 + 微信/Apple 登录入口 | +| Step 3 | OnboardingPage | 4 步功能轮播 | +| Step 4 | GoalSetupPage | 学习目标/方法/每日时长选择 | + +--- + +## 四、APIService 已封装方法(15 个) + +| 服务类 | 方法 | 接口 | +|--------|------|------| +| WaitlistService | `join(...)` | POST /waitlist | +| | `stats()` | GET /waitlist/stats | +| AuthService | `appleLogin(...)` | POST /auth/apple | +| | `logout()` | POST /auth/logout | +| UserService | `myProfile()` | GET /users/me | +| | `updateProfile(...)` | PATCH /users/me | +| KnowledgeBaseService | `list()` | GET /knowledge-bases | +| | `create(...)` | POST /knowledge-bases | +| | `detail(id:)` | GET /knowledge-bases/:id | +| KnowledgeItemService | `list(baseId:)` | GET /knowledge-items | +| | `detail(id:)` | GET /knowledge-items/:id | +| | `create(...)` | POST /knowledge-items | +| AIAnalysisService | `analyze(...)` | POST /ai-analysis | +| ActivityService | `summary()` | GET /activity/summary | +| ReviewService | `due()` | GET /reviews/due | +| FocusItemService | `list()` | GET /focus-items | +| FeedbackService | `submit(...)` | POST /feedback | + +--- + +## 五、后端接口 vs App 覆盖对照表 + +| 后端模块 | 接口数 | App 覆盖 | 状态 | +|----------|--------|---------|------| +| System | 3 | 0 | ❌ 无 | +| Auth | 3 | 2(Service 有,View 未接) | 🔶 | +| Users | 3 | 2(Service 有,View 未接) | 🔶 | +| KnowledgeBase | 5 | 3(Service 有,View 未接) | 🔶 | +| KnowledgeItems | 4 | 3(Service 有,View 未接) | 🔶 | +| DocumentImport | 2 | 0 | ❌ 无 | +| LearningSession | 3 | 0 | ❌ 无 | +| ActiveRecall | 2 | 0 | ❌ 无 | +| AIAnalysis | 3 | 1(Service 有,View 未接) | 🔶 | +| FocusItems | 4 | 1(Service 有,View 未接) | 🔶 | +| Review | 2 | 1(Service 有,View 未接) | 🔶 | +| LearningActivity | 2 | 1(Service 有,View 未接) | 🔶 | +| Notifications | 2 | 0 | ❌ 无 | +| Feedback | 4 | 1(Service 有,View 未接) | 🔶 | +| Waitlist | 3 | 2(Service 有,View 未接) | 🔶 | + +> 覆盖率:Service 层 15/48 = 31%,View 层实际接入 0/48 = 0% diff --git a/AIStudyApp/docs/gap-analysis-2.md b/AIStudyApp/docs/gap-analysis-2.md new file mode 100644 index 0000000..5fa65c6 --- /dev/null +++ b/AIStudyApp/docs/gap-analysis-2.md @@ -0,0 +1,267 @@ +# AIStudyApp 现状与缺口分析 - 第二篇:缺失功能与实施路线 + +> 接第一篇《现有资源盘点》 +> 生成日期:2026-05-11 + +--- + +## 一、优先级总览 + +``` +P0(核心闭环,本周必须) 4 项 +P1(数据接入,下周) 5 项 +P2(新页面/功能,后续) 5 项 +P3(体验增强,优化期) 5 项 +``` + +--- + +## 二、P0 —— 核心学习闭环(4 项) + +### P0-1:真实 Apple 登录流程 + +**现状:** LoginPage 是静态表单,没有调 API,Token 没有持久化。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| 集成 `AuthenticationServices`,添加 `ASAuthorizationAppleIDButton` | LoginPage(内嵌在 AIStudyAppApp.swift) | +| 拿到 `identityToken` 后调用 `AuthService.appleLogin(...)` | LoginPage | +| 登录成功后用 `@AppStorage` 或 Keychain 存储 Token | APIClient | +| `@main` 启动时检查已有 Token,跳过 Onboarding | AIStudyAppApp.swift | +| 处理登录失败/网络错误的 UI 提示 | LoginPage | +| 接入 `POST /auth/refresh` Token 自动刷新 | APIClient | + +涉及接口:`POST /auth/apple`、`POST /auth/refresh`、`POST /auth/logout` + +--- + +### P0-2:知识库 + 知识点接入真实 API + +**现状:** LibraryHomeView 硬编码 4 个知识库,子页面表单没有提交。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| `LibraryHomeView` 的 `.task {}` 中调 `KnowledgeBaseService.list()` | LibraryHomeView.swift | +| 替换硬编码卡片为 `ForEach(bases)` 真实数据 | LibraryHomeView.swift | +| `CreateLibraryPage` 表单提交调 `KnowledgeBaseService.create(...)` | LibrarySubpages.swift | +| `LibraryDetailPage` 加载真实知识点列表 `KnowledgeItemService.list(baseId:)` | LibrarySubpages.swift | +| `AddKnowledgePage` 表单提交调 `KnowledgeItemService.create(...)` | LibrarySubpages.swift | +| `EditKnowledgePage` 提交调 `PATCH /knowledge-items/:id`(APIService 需新增 update 方法) | LibrarySubpages.swift + APIService.swift | +| 增加 loading / empty / error 三种状态处理 | 各 Library 页面 | + +涉及接口:`GET/POST /knowledge-bases`、`GET/POST/PATCH /knowledge-items` + +--- + +### P0-3:学习会话追踪 + +**现状:** StudyHomeView 的"今日任务"是静态列表,没有学习计时,没有调任何接口。 + +**需要做:** + +| 子任务 | 涉及文件 / 新建文件 | +|--------|-------------------| +| 新建 `LearningSessionView.swift`:含计时器(`Timer.publish`)+ 暂停/结束按钮 | **新文件** Features/Study/LearningSessionView.swift | +| 点击 StudyHomeView 任务 → push 到 LearningSessionView | StudyHomeView.swift | +| 入场调 `POST /learning-sessions`(传入 knowledgeBaseId) | LearningSessionView.swift | +| 结束/暂停时调 `POST /learning-sessions/:id/end` | LearningSessionView.swift | +| APIService 新增 `LearningSessionService` | APIService.swift | +| APIModels 新增 `LearningSessionCreateRequest` / `LearningSessionResponse` | APIModels.swift | + +涉及接口:`POST /learning-sessions`、`POST /learning-sessions/:id/end`、`GET /learning-sessions` + +--- + +### P0-4:间隔复习卡片 + +**现状:** 没有复习页面。后端 `GET /reviews/due` + `POST /reviews/:id/submit` 已就绪。 + +**需要做:** + +| 子任务 | 新建文件 | +|--------|---------| +| 新建 `ReviewCardView.swift`:正面问题 → 点击翻转 → 显示答案 → 评分按钮 | **新文件** Features/Study/ReviewCardView.swift | +| 评分按钮:Again(1) / Hard(2) / Good(3) / Easy(4),调 `POST /reviews/:id/submit` | ReviewCardView.swift | +| 复习入口放在 StudyHomeView "今日任务"区域顶部 | StudyHomeView.swift | +| 复习入口放在 AIHomeView 快捷操作中 | AIHomeView.swift | +| 到期卡片数为 0 时显示空状态"🎉 都复习完啦" | ReviewCardView.swift | + +涉及接口:`GET /reviews/due`、`POST /reviews/:id/submit` + +--- + +## 三、P1 —— 数据接入(5 项) + +### P1-1:薄弱点 / AI 分析接真实数据 + +**现状:** AnalysisHomeView / WeakPointsPage 硬编码 3 条数据。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| AnalysisHomeView `.task {}` 中调 `FocusItemService.list()` | AnalysisHomeView.swift | +| 替换硬编码 ZXWeakRow 为 `ForEach(focusItems)` | AnalysisHomeView.swift | +| RecallTestPage 提交回答时调 `AIAnalysisService.analyze(...)` | DailyThinkingPage.swift | +| AIFeedbackPageView 展示真实分析结果 | DailyThinkingPage.swift | +| APIModels 增补 FocusItem 字段对齐后端 | APIModels.swift | + +涉及接口:`GET /focus-items`、`POST /ai-analysis`、`GET /ai-analysis/:id` + +--- + +### P1-2:StudyHomeView 数据真实化 + +**现状:** 进度环、任务列表、周活跃柱状图全是硬编码。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| 调 `ActivityService.summary()` 获取真实统计数据 | StudyHomeView.swift | +| 进度环用真实 `totalMinutes` / `streakDays` | StudyHomeView.swift | +| 周活跃图调 `GET /activity/heatmap`(APIService 需新增 heatmap 方法) | StudyHomeView.swift + APIService.swift | +| 今日任务从 `GET /reviews/due` + `GET /focus-items` 拼接 | StudyHomeView.swift | + +涉及接口:`GET /activity/summary`、`GET /activity/heatmap` + +--- + +### P1-3:ProfileView 接入用户资料 + +**现状:** ProfileView 全部静态假数据(昵称"学习者"、假统计)。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| `.task {}` 调 `UserService.myProfile()` | ProfileView.swift | +| 替换头像(emoji → 真实 avatar URL / 默认头像) | ProfileView.swift | +| 替换昵称、邮箱、统计数字 | ProfileView.swift | +| 菜单项"学习目标设置"跳设置表单页 → `PATCH /users/me/preferences` | ProfileView.swift + 新 SettingsView | + +涉及接口:`GET /users/me`、`PATCH /users/me`、`PATCH /users/me/preferences` + +--- + +### P1-4:通知中心页面 + +**现状:** 完全没有通知页面。 + +**需要做:** + +| 子任务 | 新建/涉及文件 | +|--------|-------------| +| 新建 `NotificationListView.swift` | **新文件** Features/Profile/NotificationListView.swift | +| `.task {}` 调 `GET /notifications` | NotificationListView.swift | +| 列表项点击标记已读 `POST /notifications/:id/read` | NotificationListView.swift | +| ProfileView 右上角铃铛 badge 显示未读数 | ProfileView.swift | +| APIService 新增 `NotificationService` | APIService.swift | + +涉及接口:`GET /notifications`、`POST /notifications/:id/read` + +--- + +### P1-5:反馈提交 + +**现状:** 没有反馈提交入口。 + +**需要做:** + +| 子任务 | 涉及文件 | +|--------|---------| +| ProfileView 菜单加"帮助与反馈" → 跳反馈表单 | ProfileView.swift + 新 FeedbackView | +| 调 `FeedbackService.submit(...)` | 新 FeedbackView | +| 提交后显示"感谢反馈"提示 | 新 FeedbackView | + +涉及接口:`POST /feedback` + +--- + +## 四、P2 —— 新页面/功能(5 项) + +### P2-1:文件导入真实接入 + +**现状:** ImportPage 只有 4 个静态按钮。 + +**需要做:** 接入 `PHPickerViewController`(相册选图)、`UIDocumentPickerViewController`(文件选择)、AVCaptureSession(拍照),上传后调 `POST /imports`,轮询 `GET /imports/:id/status`。APIService 新增 `DocumentImportService`。 + +--- + +### P2-2:全局搜索 + +**现状:** LibraryHomeView 有搜索框但无效。 + +**需要做:** 新建 `SearchView.swift`,调 `GET /knowledge-items?keyword=xxx`,支持搜索知识点/知识库/标签,展示搜索结果列表。 + +--- + +### P2-3:设置页面完善 + +**现状:** ProfileView 5 个菜单项全是假的。 + +**需要做:** 每个菜单项对应一个设置表单页:学习目标、复习提醒时间、学习报告邮件、学习方法偏好(费曼/回忆/间隔/综合)、数据同步状态。 + +--- + +### P2-4:主动回忆(Active Recall)流程 + +**现状:** RecallTestPage 只提交假的 AI 分析,没有调 `GET /active-recalls`。 + +**需要做:** 新建 ActiveRecallView,展示问题卡片 → 输入回答 → 调 `POST /active-recalls/:id/submit`。 + +--- + +### P2-5:Token 自动刷新与登录态管理 + +**现状:** Token 没有持久化,没有 refresh 逻辑。 + +**需要做:** Keychain 存储 accessToken + refreshToken;APIClient 拦截 401 → 自动调 `POST /auth/refresh` → 重试原请求;refresh 也失败 → 清 Token → 跳登录页。 + +--- + +## 五、P3 —— 体验增强(5 项) + +| # | 项目 | 说明 | +|---|------|------| +| P3-1 | 下拉刷新 | 所有列表页 `.refreshable {}` + 页码分页 | +| P3-2 | 加载/空/错误三态 | 每个数据加载页加 ProgressView / 空状态插图+文案 / 错误重试按钮 | +| P3-3 | 离线缓存 | 用 UserDefaults 或本地 JSON 缓存最近数据,断网可展示 | +| P3-4 | 深色模式 | 当前强制 `.dark`,需支持跟随系统 | +| P3-5 | 无障碍 | VoiceOver labels、Dynamic Type 适配、高对比度 | + +--- + +## 六、实施建议顺序 + +``` +第 1 周 ─ P0-1 登录 → P0-2 知识库CRUD → P0-3 学习会话 +第 2 周 ─ P0-4 复习卡片 → P1-1 薄弱点/AI分析 → P1-2 StudyHomeView 真实化 +第 3 周 ─ P1-3 ProfileView → P1-4 通知中心 → P1-5 反馈 +第 4 周 ─ P2-1 文件导入 → P2-2 搜索 → P2-3 设置页 +第 5 周 ─ P2-4 主动回忆 → P2-5 Token刷新 +第 6 周 ─ P3 体验增强 +``` + +--- + +## 七、后端接口未封装清单(需新增 Service 方法) + +| 模块 | 后端口 | 未封装接口 | +|------|--------|----------| +| KnowledgeBase | PATCH/DELETE | update / delete | +| KnowledgeItems | PATCH/DELETE | update / delete | +| LearningSession | POST/GET | start / end / list | +| ActiveRecall | GET/POST | list / submit | +| AIAnalysis | GET | result / job status | +| Activity | GET | heatmap | +| Notifications | GET/POST | list / markRead | +| DocumentImport | POST/GET | create / status | +| Review | POST | submit | +| FocusItems | POST/PATCH | create / update / complete | + +> 需新增约 15 个 Service 方法 + 对应 Request/Response DTO