diff --git a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj index 215aeb2..a1dd2b5 100644 --- a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj +++ b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj @@ -255,6 +255,7 @@ 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; @@ -299,6 +300,7 @@ 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; diff --git a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift index cff4eb9..983542b 100644 --- a/AIStudyApp/AIStudyApp/AIStudyAppApp.swift +++ b/AIStudyApp/AIStudyApp/AIStudyAppApp.swift @@ -2,75 +2,114 @@ import SwiftUI @main struct AIStudyAppApp: App { - @StateObject private var session: AppSession - @StateObject private var colorSchemeManager = ColorSchemeManager.shared - - init() { - let tokenStore = TokenStore() - let baseURL = URL(string: "https://api.longde.cloud")! - let apiClient = APIClient(baseURL: baseURL, tokenStore: tokenStore) - let authService = AuthService(apiClient: apiClient, tokenStore: tokenStore) - _session = StateObject(wrappedValue: AppSession(authService: authService, tokenStore: tokenStore)) - } + @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false var body: some Scene { WindowGroup { - AppRootView(session: session) - .preferredColorScheme(colorSchemeManager.current.colorScheme) - .dynamicTypeClamped() - .task { - await session.bootstrap() - } - } - } -} - -// MARK: - Root Router - -struct AppRootView: View { - @ObservedObject var session: AppSession - - var body: some View { - Group { - if session.isLoading { - SplashPage {} - } else if !session.isAuthenticated { - LoginView(appSession: session) { _ in - session.loginAsDemo() - } - } else if session.needsOnboarding { - OnboardingFlowView(session: session) + if hasCompletedOnboarding { + ContentView().preferredColorScheme(.dark) } else { - ContentView() + OnboardingFlowView(hasCompletedOnboarding: $hasCompletedOnboarding) + .preferredColorScheme(.dark) } } } } -// MARK: - Onboarding Flow (Welcome → Onboarding → GoalSetup) - struct OnboardingFlowView: View { - @ObservedObject var session: AppSession + @Binding var hasCompletedOnboarding: Bool @State private var step = 0 var body: some View { ZStack { switch step { - case 0: - WelcomePage { - withAnimation(.easeInOut(duration: 0.5)) { step = 1 } - } onSkip: { - session.logout() - } - case 1: - OnboardingPage { step = 2 } - case 2: - GoalSetupPage { _ in - session.completeOnboarding() - } - default: - EmptyView() + case 0: SplashPage { withAnimation(.easeInOut(duration: 0.5)) { step = 1 } } + case 1: WelcomePage { withAnimation { step = 2 } } onSkip: { hasCompletedOnboarding = true } + case 2: LoginPage { step = 3 } onSkip: { hasCompletedOnboarding = true } + case 3: OnboardingPage { step = 4 } + case 4: GoalSetupPage { $0 ? (hasCompletedOnboarding = true) : (step = 0) } + default: EmptyView() } - } + }.preferredColorScheme(.dark) } } + +// Splash +struct SplashPage: View { + let onFinish: () -> Void + var body: some View { + ZStack { + LinearGradient(colors: [Color(hex: "#0D0D20"), Color(hex: "#0F0F1A"), Color(hex: "#130D20")], startPoint: .top, endPoint: .bottom).ignoresSafeArea() + Circle().fill(RadialGradient(colors: [Color(hex: "#7C6EFA", opacity: 0.25), .clear], center: .center, startRadius: 0, endRadius: 140)).frame(width: 280, height: 280).offset(y: -60).allowsHitTesting(false) + Circle().fill(RadialGradient(colors: [Color(hex: "#F97316", opacity: 0.15), .clear], center: .center, startRadius: 0, endRadius: 100)).frame(width: 200, height: 200).offset(y: 180).allowsHitTesting(false) + VStack(spacing: 0) { + RoundedRectangle(cornerRadius: 28).fill(LinearGradient(colors: [Color(hex: "#7C6EFA"), Color(hex: "#A78BFA"), Color(hex: "#F97316")], startPoint: .topLeading, endPoint: .bottomTrailing)).frame(width: 96, height: 96).overlay(Image(systemName: "brain.head.profile").font(.system(size: 44)).foregroundColor(.white.opacity(0.8))).shadow(color: Color(hex: "#7C6EFA", opacity: 0.5), radius: 40).padding(.bottom, 24) + Text("知习").font(.system(size: 36, weight: .heavy)).tracking(-1).foregroundStyle(LinearGradient(colors: [Color(hex: "#A78BFA"), Color(hex: "#F0F0FF"), Color(hex: "#F97316")], startPoint: .leading, endPoint: .trailing)) + Text("Z H I X I").font(.system(size: 13, weight: .medium)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.4)).tracking(3).padding(.top, 6) + Text("AI-first 系统化学习").font(.system(size: 14)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.45)).tracking(0.5).padding(.top, 24) + } + VStack { Spacer(); ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 2).fill(Color(hex: "#FFFFFF", opacity: 0.1)).frame(width: 40, height: 3); RoundedRectangle(cornerRadius: 2).fill(LinearGradient(colors: [Color.zxPurple, Color.zxOrange], startPoint: .leading, endPoint: .trailing)).frame(width: 24, height: 3) }.padding(.bottom, 80) } + }.onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { onFinish() } } + } +} + +// Welcome +struct WelcomePage: View { let onContinue: () -> Void; let onSkip: () -> Void + var body: some View { ZStack { ZXGradient.page.ignoresSafeArea(); Circle().fill(RadialGradient(colors: [Color(hex: "#7C6EFA", opacity: 0.12), .clear], center: .topTrailing, startRadius: 0, endRadius: 260)).frame(width: 260, height: 260).offset(x: 80, y: -120).allowsHitTesting(false) + VStack { Spacer() + VStack(spacing: 14) { HStack(spacing: 6) { Image(systemName: "sparkles").font(.system(size: 12)); Text("AI 驱动").font(.system(size: 12, weight: .semibold)) }.foregroundColor(Color.zxAccent).padding(.horizontal, 12).padding(.vertical, 6).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(Capsule()) + Text("用 AI 重新定义\n你的学习方式").font(.system(size: 32, weight: .heavy)).tracking(-0.8).lineSpacing(4) + VStack(spacing: 10) { FeatureRow(icon: "🧠", title: "主动回忆", desc: "基于间隔重复的智能复习"); FeatureRow(icon: "🎤", title: "费曼解释", desc: "用自己的话讲出来"); FeatureRow(icon: "📊", title: "AI 分析", desc: "发现知识薄弱点") } } + VStack(spacing: 12) { Button { onContinue() } label: { Text("开始使用").font(.system(size: 16, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 56).background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)).shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) }; Button { onSkip() } label: { Text("已有账号?立即登录").font(.system(size: 14, weight: .medium)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.7)) }.padding(.bottom, 32) } }.padding(.horizontal, 20) } } +} +struct FeatureRow: View { let icon: String; let title: String; let desc: String + var body: some View { HStack(spacing: 14) { Text(icon).font(.system(size: 20)).frame(width: 40, height: 40).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) } }.padding(.horizontal, 16).padding(.vertical, 14).background(Color.zxFill003).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 16)) } +} + +// Login +struct LoginPage: View { let onContinue: () -> Void; let onSkip: () -> Void + @State private var isEmail = false; @State private var phone = ""; @State private var email = ""; @State private var pw = ""; @State private var showPw = false + var body: some View { ZStack { Color.zxBg0.ignoresSafeArea(); Circle().fill(RadialGradient(colors: [Color(hex: "#7C6EFA", opacity: 0.1), .clear], center: .top, startRadius: 0, endRadius: 200)).frame(width: 200, height: 200).offset(y: -60).allowsHitTesting(false) + VStack { Spacer() + VStack(spacing: 24) { VStack(spacing: 6) { Text("欢迎登录").font(.system(size: 28, weight: .heavy)).tracking(-0.6); Text("使用手机号或邮箱登录").font(.system(size: 14)).foregroundColor(Color.zxF05) }; HStack(spacing: 4) { ZXTabBtn(t: "手机号", active: !isEmail) { isEmail = false }; ZXTabBtn(t: "邮箱", active: isEmail) { isEmail = true } }.padding(4).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 12)) + if isEmail { VStack(alignment: .leading, spacing: 8) { Text("邮箱").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5); ZXInputField(placeholder: "your@email.com", text: $email) } } + else { VStack(alignment: .leading, spacing: 8) { Text("手机号").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5); HStack(spacing: 0) { Text("+86").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).padding(.trailing, 12).overlay(alignment: .trailing) { Rectangle().fill(Color.zxBorder01).frame(width: 1).padding(.vertical, 4) }.padding(.trailing, 12); TextField("手机号", text: $phone).keyboardType(.phonePad).font(.system(size: 15)).tint(Color.zxPurple) }.padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)) } } + ZXInputField(placeholder: "密码", text: $pw, isSecure: !showPw); HStack { Spacer(); Button { showPw.toggle() } label: { Image(systemName: showPw ? "eye" : "eye.slash").font(.system(size: 16)).foregroundColor(Color.zxF03) } }.padding(.trailing, 4) + HStack { Spacer(); Button("忘记密码?") {}.font(.system(size: 13)).foregroundColor(Color.zxPurple) } + Button { onContinue() } label: { Text("登录").font(.system(size: 16, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 56).background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)).shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) } + HStack(spacing: 12) { Rectangle().fill(Color.zxBorder008).frame(height: 1); Text("或").font(.system(size: 12)).foregroundColor(Color.zxF03); Rectangle().fill(Color.zxBorder008).frame(height: 1) } + HStack(spacing: 12) { SocialLoginBtn(emoji: "💬", text: "微信登陆", color: .green) {}; SocialLoginBtn(emoji: "🍎", text: "Apple 登录", color: .white) {} } }.padding(.horizontal, 20).padding(.bottom, 32) } } } +} +struct ZXTabBtn: View { let t: String; let active: Bool; let a: () -> Void; var body: some View { Button(action: a) { Text(t).font(.system(size: 13, weight: .semibold)).foregroundColor(active ? .white : Color.zxF05).frame(maxWidth: .infinity).frame(height: 36).background(active ? AnyView(ZXGradient.brand) : AnyView(Color.clear)).clipShape(RoundedRectangle(cornerRadius: 9)) } } } +struct ZXInputField: View { let placeholder: String; @Binding var text: String; var isSecure = false; var body: some View { HStack { if isSecure { SecureField(placeholder, text: $text) } else { TextField(placeholder, text: $text) } }.font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)) } } +struct SocialLoginBtn: View { let emoji: String; let text: String; let color: Color; let action: () -> Void; var body: some View { Button(action: action) { HStack(spacing: 10) { Text(emoji).font(.system(size: 18)); Text(text).font(.system(size: 11, weight: .medium)) }.foregroundColor(Color.zxF007).frame(maxWidth: .infinity).frame(height: 52).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)) } } } + +// Onboarding +struct OnboardingPage: View { let onContinue: () -> Void; @State private var step = 0 + let titles = ["输入知识", "主动输出", "AI 分析", "掌握知识"] + let descs = ["从任何地方收集并导入学习资料,构建你的专属知识库。", "通过间隔回忆和费曼解释法,将知识转化为长期记忆。", "AI 自动定位薄弱知识点,给出针对性的学习建议。", "系统性掌握每一个知识点,建立牢固的知识体系。"] + var body: some View { ZStack { ZXGradient.page.ignoresSafeArea() + VStack(spacing: 0) { Spacer() + HStack(spacing: 6) { ForEach(0..<4, id: \.self) { i in RoundedRectangle(cornerRadius: 2).fill(i == step ? AnyShapeStyle(ZXGradient.brand) : AnyShapeStyle(Color(hex: "#FFFFFF", opacity: 0.1))).frame(width: i == step ? 24 : 8, height: 4) } } + VStack(spacing: 12) { Text(titles[step]).font(.system(size: 24, weight: .heavy)).tracking(-0.5); Text(descs[step]).font(.system(size: 14)).foregroundColor(Color.zxF04).lineSpacing(4).multilineTextAlignment(.center) }.padding(.top, 32).padding(.bottom, 40) + Button { if step < 3 { withAnimation { step += 1 } } else { onContinue() } } label: { Text(step < 3 ? "下一步" : "开始使用").font(.system(size: 16, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 56).background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)).shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) } + Button("跳过") { onContinue() }.font(.system(size: 12)).foregroundColor(Color.zxF03).padding(.top, 12).padding(.bottom, 32) } }.padding(.horizontal, 20) } +} + +// GoalSetup +struct GoalSetupPage: View { let onComplete: (Bool) -> Void + @State private var selectedGoal = ""; let goals = [("🧑‍🎓","备考考试","公考、考研、考证等"),("💼","职业技能","编程、设计、产品等"),("📚","通识学习","扩充知识面"),("🎯","自定义","设定自己的目标")] + @State private var selectedMethod = ""; let methods = ["间隔回忆","费曼技巧","AI 分析"] + @State private var dailyMins = "30 分钟"; let times = ["15 分钟","30 分钟","1 小时","不限制"] + var body: some View { ZStack { ZXGradient.page.ignoresSafeArea() + VStack(spacing: 0) { Spacer() + Text("设定你的学习目标").font(.system(size: 24, weight: .heavy)).tracking(-0.5).foregroundColor(Color.zxF0).padding(.bottom, 24) + ScrollView { VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 10) { Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) + 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).overlay(RoundedRectangle(cornerRadius: 16).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 16)) }.foregroundColor(.primary) } } + VStack(alignment: .leading, spacing: 10) { Text("学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) + HStack(spacing: 8) { ForEach(methods, id: \.self) { m in let sel = selectedMethod == m; Button { selectedMethod = m } label: { Text(m).font(.system(size: 13)).fontWeight(sel ? .semibold : .regular).foregroundColor(sel ? Color.zxPurple : Color.zxF05).padding(.horizontal, 16).padding(.vertical, 10).background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003).overlay(RoundedRectangle(cornerRadius: 20).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }.foregroundColor(.primary) } } } + VStack(alignment: .leading, spacing: 10) { Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) + 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).overlay(RoundedRectangle(cornerRadius: 12).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 12)) }.foregroundColor(.primary) } } } } } + Button { onComplete(true) } label: { Text("开始学习").font(.system(size: 16, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 56).background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)).shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) }.padding(.top, 24).padding(.bottom, 32).padding(.horizontal, 20) } } } +} diff --git a/AIStudyApp/AIStudyApp/App/AppSession.swift b/AIStudyApp/AIStudyApp/App/AppSession.swift deleted file mode 100644 index 8b99990..0000000 --- a/AIStudyApp/AIStudyApp/App/AppSession.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftUI -import Combine - -@MainActor -final class AppSession: ObservableObject { - @Published var currentUser: User? - @Published var isAuthenticated = false - @Published var isLoading = true - @Published var authError: String? - - private let authService: AuthServiceProtocol - private let tokenStore: TokenStoreProtocol - - init(authService: AuthServiceProtocol, tokenStore: TokenStoreProtocol) { - self.authService = authService - self.tokenStore = tokenStore - } - - // MARK: - Bootstrap - - func bootstrap() async { - isLoading = true - authError = nil - - guard let _ = try? tokenStore.getRefreshToken() else { - isAuthenticated = false - isLoading = false - return - } - - do { - let response = try await authService.refreshSession() - currentUser = response.user - isAuthenticated = true - } catch { - try? tokenStore.clearAll() - isAuthenticated = false - if let apiError = error as? APIError, apiError.isAuthenticationError { - authError = nil - } else { - authError = error.localizedDescription - } - } - - isLoading = false - } - - // MARK: - Login - - func loginWithApple() async { - isLoading = true - authError = nil - - do { - let response = try await authService.loginWithApple() - currentUser = response.user - isAuthenticated = true - } catch { - authError = error.localizedDescription - isAuthenticated = false - } - - isLoading = false - } - - // MARK: - Logout - - func logout() { - Task { try? await authService.logout() } - currentUser = nil - isAuthenticated = false - authError = nil - } - - // MARK: - Demo Mode - - func loginAsDemo() { - currentUser = User( - id: "demo", - appleUserId: "demo", - displayName: "演示用户", - email: nil, - preferredLanguage: "zh-Hans", - onboardingCompleted: true, - createdAt: ISO8601DateFormatter().string(from: Date()), - lastLoginAt: ISO8601DateFormatter().string(from: Date()), - status: .active - ) - isAuthenticated = true - isLoading = false - authError = nil - } - - // MARK: - Computed - - var needsOnboarding: Bool { - currentUser?.onboardingCompleted == false - } - - // MARK: - Onboarding - - func completeOnboarding() { - guard var user = currentUser else { return } - // In production, this would call PATCH /api/users/me/onboarding - // For now, locally mutate to allow flow to proceed - user = User( - id: user.id, - appleUserId: user.appleUserId, - displayName: user.displayName, - email: user.email, - preferredLanguage: user.preferredLanguage, - onboardingCompleted: true, - createdAt: user.createdAt, - lastLoginAt: user.lastLoginAt, - status: user.status - ) - currentUser = user - } -} diff --git a/AIStudyApp/AIStudyApp/ContentView.swift b/AIStudyApp/AIStudyApp/ContentView.swift index 9dc0322..c03996a 100644 --- a/AIStudyApp/AIStudyApp/ContentView.swift +++ b/AIStudyApp/AIStudyApp/ContentView.swift @@ -2,24 +2,59 @@ import SwiftUI struct ContentView: View { @State private var selectedTab = "ai" - var body: some View { ZStack { - Group { - if selectedTab == "ai" { NavigationStack { AIHomeView() } } - else if selectedTab == "library" { NavigationStack { LibraryHomeView() } } - else if selectedTab == "study" { NavigationStack { StudyHomeView() } } - else if selectedTab == "analysis" { NavigationStack { AnalysisHomeView() } } - else if selectedTab == "profile" { NavigationStack { ProfileView() } } + switch selectedTab { + case "ai": NavigationStack { AIHomeView().background(Color.zxBg0.ignoresSafeArea()) } + case "library": NavigationStack { LibraryHomeView().background(Color.zxBg0.ignoresSafeArea()) } + case "study": NavigationStack { StudyHomeView().background(Color.zxBg0.ignoresSafeArea()) } + case "analysis": NavigationStack { AnalysisHomeView().background(Color.zxBg0.ignoresSafeArea()) } + case "profile": NavigationStack { ProfileView().background(Color.zxBg0.ignoresSafeArea()) } + default: NavigationStack { AIHomeView() } } - .transition(.opacity.combined(with: .scale(scale: 0.98))) - .animation(.spring(response: 0.35, dampingFraction: 0.85), value: selectedTab) - - VStack { Spacer(); ZXTabBar(active: $selectedTab) } - .ignoresSafeArea(edges: .bottom) - } - .ignoresSafeArea(edges: .bottom) + VStack { Spacer(); ZXTabBar(active: $selectedTab) }.ignoresSafeArea(edges: .bottom) + }.ignoresSafeArea(edges: .bottom).preferredColorScheme(.dark) } } -// Shared components: ZXTabBar, ZXAIInputBar, ZXScoreBox, ZXIconBtn → Shared/Components/ +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)}} +} + +struct ZXIconBtn: View { + let icon: String; let size: CGFloat; var branded = false; let action: () -> Void + var body: some View {Button(action:action){Image(systemName:icon).font(.system(size:size*0.44)).frame(width:size,height:size)}.foregroundColor(branded ? .white:Color.zxF05).background(branded ? AnyView(ZXGradient.brand):AnyView(Color(hex:"#FFFFFF",opacity:0.05))).clipShape(RoundedRectangle(cornerRadius:10)).overlay{if !branded{RoundedRectangle(cornerRadius:10).stroke(Color.zxBorder008,lineWidth:1)}}} +} + +struct ZXScoreBox: View {let score:Int;let bg:Color;let fg:Color + var body: some View {Text("\(score)").font(.system(size:12,weight:.heavy)).foregroundColor(fg).frame(width:36,height:36).background(bg).clipShape(RoundedRectangle(cornerRadius:10))} +} + +struct ZXWeakRow: View { + let score: Int; let topic: String; let lib: String; let priority: String + var body: some View { + HStack(spacing: 12) { + Text("\(score)").font(.system(size:13,weight:.heavy)).foregroundColor(Color.zxYellow) + .frame(width:40,height:40).background(Color.zxYellowBG(0.15)).clipShape(RoundedRectangle(cornerRadius:12)) + VStack(alignment:.leading,spacing:2){ + Text(topic).font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF0) + Text(lib).font(.system(size:11)).foregroundColor(Color.zxF04) + }.frame(maxWidth:.infinity,alignment:.leading) + Text("\(priority)优先").font(.system(size:11,weight:.bold)) + .foregroundColor(priority=="高" ? Color.zxRed:Color.zxYellow) + .padding(.horizontal,8).padding(.vertical,3) + .background((priority=="高" ? Color.zxRedBG(0.15):Color.zxYellowBG(0.15))).clipShape(Capsule()) + } + .padding(.horizontal,16).padding(.vertical,12) + .background(Color.zxYellowBG(0.06)) + .overlay(RoundedRectangle(cornerRadius:14).stroke(Color(hex:"#F59E0B",opacity:0.15),lineWidth:1)) + .clipShape(RoundedRectangle(cornerRadius:14)) + } +} + +struct ZXAIInputBar: View { + @Binding var text:String;let onSend:()->Void + var body: some View {HStack(spacing:10){Image(systemName:"sparkles").font(.system(size:16)).foregroundColor(Color.zxPurple);TextField("问 AI 任何学习问题…",text:$text).font(.system(size:14)).tint(Color.zxPurple);Spacer();Image(systemName:"mic.fill").font(.system(size:18)).foregroundColor(Color.zxF03);Button(action:onSend){Image(systemName:"arrow.up").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(width:30,height:30).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius:9))}}.padding(.horizontal,14).padding(.vertical,10).background(.ultraThinMaterial).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius:20).stroke(Color.zxBorder008,lineWidth:1)).clipShape(RoundedRectangle(cornerRadius:20)).padding(.horizontal,20).padding(.bottom,34)} +} diff --git a/AIStudyApp/AIStudyApp/Core/Appearance/ColorSchemeManager.swift b/AIStudyApp/AIStudyApp/Core/Appearance/ColorSchemeManager.swift deleted file mode 100644 index 07ee28c..0000000 --- a/AIStudyApp/AIStudyApp/Core/Appearance/ColorSchemeManager.swift +++ /dev/null @@ -1,35 +0,0 @@ -import SwiftUI -import Combine - -enum AppColorScheme: String, CaseIterable, Identifiable { - case system - case light - case dark - - var id: String { rawValue } - - var displayName: String { - switch self { - case .system: return String(localized: "跟随系统") - case .light: return String(localized: "浅色模式") - case .dark: return String(localized: "深色模式") - } - } - - var colorScheme: ColorScheme? { - switch self { - case .system: return nil - case .light: return .light - case .dark: return .dark - } - } -} - -@MainActor -final class ColorSchemeManager: ObservableObject { - static let shared = ColorSchemeManager() - - @AppStorage("app_color_scheme") var current: AppColorScheme = .dark - - private init() {} -} diff --git a/AIStudyApp/AIStudyApp/Core/Extensions/Font+DynamicType.swift b/AIStudyApp/AIStudyApp/Core/Extensions/Font+DynamicType.swift deleted file mode 100644 index 745714e..0000000 --- a/AIStudyApp/AIStudyApp/Core/Extensions/Font+DynamicType.swift +++ /dev/null @@ -1,35 +0,0 @@ -import SwiftUI - -// MARK: - Scaled Font Modifier - -/// Apply Dynamic Type scaling to a fixed font size, clamped to a reasonable range. -struct ScaledFont: ViewModifier { - @ScaledMetric var size: CGFloat - let weight: Font.Weight - let design: Font.Design - - init(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) { - _size = ScaledMetric(wrappedValue: size, relativeTo: .body) - self.weight = weight - self.design = design - } - - func body(content: Content) -> some View { - content.font(.system(size: size, weight: weight, design: design)) - } -} - -extension View { - func scaledFont(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> some View { - modifier(ScaledFont(size: size, weight: weight, design: design)) - } -} - -// MARK: - Dynamic Type Range - -extension View { - /// Clamp Dynamic Type to prevent layout breaking at extreme sizes. - func dynamicTypeClamped() -> some View { - self.dynamicTypeSize(.xSmall ... .xxxLarge) - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Extensions/View+StaggeredAppear.swift b/AIStudyApp/AIStudyApp/Core/Extensions/View+StaggeredAppear.swift deleted file mode 100644 index dadad7d..0000000 --- a/AIStudyApp/AIStudyApp/Core/Extensions/View+StaggeredAppear.swift +++ /dev/null @@ -1,45 +0,0 @@ -import SwiftUI - -// MARK: - Staggered Appear - -extension View { - func staggeredAppear(index: Int, baseDelay: Double = 0.05) -> some View { - self - .opacity(0) - .offset(y: 8) - .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(Double(index) * baseDelay), value: index) - } -} - -// MARK: - Animated Visibility - -struct AnimatedVisibility: ViewModifier { - let visible: Bool - - func body(content: Content) -> some View { - content - .opacity(visible ? 1 : 0) - .scaleEffect(visible ? 1 : 0.95) - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: visible) - } -} - -extension View { - func animatedVisible(_ visible: Bool) -> some View { - modifier(AnimatedVisibility(visible: visible)) - } -} - -// MARK: - Spring Press - -struct SpringPress: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .scaleEffect(configuration.isPressed ? 0.96 : 1) - .animation(.spring(response: 0.2, dampingFraction: 0.7), value: configuration.isPressed) - } -} - -extension ButtonStyle where Self == SpringPress { - static var springPress: SpringPress { SpringPress() } -} diff --git a/AIStudyApp/AIStudyApp/Core/Localization/LanguageManager.swift b/AIStudyApp/AIStudyApp/Core/Localization/LanguageManager.swift deleted file mode 100644 index ef962e2..0000000 --- a/AIStudyApp/AIStudyApp/Core/Localization/LanguageManager.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// LanguageManager.swift -// AIStudyApp -// -// 语言管理:当前仅支持中文,预留多语言架构 -// - -import SwiftUI -import Combine - -// MARK: - Supported Language - -enum AppLanguage: String, CaseIterable, Identifiable { - case chinese = "zh-Hans" - - var id: String { rawValue } - - var displayName: String { - switch self { - case .chinese: return "中文" - } - } -} - -// MARK: - Language Manager - -@MainActor -final class LanguageManager: ObservableObject { - static let shared = LanguageManager() - - @AppStorage("app_language") var current: AppLanguage = .chinese { - didSet { - apply() - } - } - - let supported: [AppLanguage] = AppLanguage.allCases - - private init() { - apply() - } - - /// Sets the AppleLanguages default. Takes effect on next app launch. - private func apply() { - UserDefaults.standard.set([current.rawValue], forKey: "AppleLanguages") - UserDefaults.standard.synchronize() - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Localization/ZXStrings.swift b/AIStudyApp/AIStudyApp/Core/Localization/ZXStrings.swift deleted file mode 100644 index a51fb50..0000000 --- a/AIStudyApp/AIStudyApp/Core/Localization/ZXStrings.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ZXStrings.swift -// AIStudyApp -// -// 非 SwiftUI Text 场景的本地化字符串引用。 -// SwiftUI Text("中文") 自动走 LocalizedStringKey,无需改动。 -// 此文件供 ViewModel、Service、Alert 等需要 String 值的场景使用。 -// - -import Foundation - -enum ZXStrings { - - // MARK: - General - - static let ok = String(localized: "好的") - static let cancel = String(localized: "取消") - static let retry = String(localized: "重试") - static let loading = String(localized: "加载中…") - static let confirm = String(localized: "确认") - static let skip = String(localized: "跳过") - static let save = String(localized: "保存") - static let submit = String(localized: "提交") - static let submitting = String(localized: "提交中…") - static let search = String(localized: "搜索") - - // MARK: - Login - - static let loginWithApple = String(localized: "使用 Apple 继续") - static let loginTerms = String(localized: "登录即代表你同意《用户服务协议》和《隐私政策》") - static let debugSkip = String(localized: "跳过,进入演示模式") - static let brandName = String(localized: "知习") - static let brandTagline = String(localized: "更懂你,更会学。") - static let brandDescription = String(localized: "用 AI 把知识库、主动回忆和间隔复习连接起来,\n从\"看过\"走向\"真正学会\"。") - static let existingAccount = String(localized: "已有账号?立即登录") - static let redefineLearning = String(localized: "用 AI 重新定义\n你的学习方式") - static let getStarted = String(localized: "开始使用") - static let startLearning = String(localized: "开始学习") - static let nextStep = String(localized: "下一步") - - // MARK: - Onboarding - - static let setGoal = String(localized: "设定你的学习目标") - static let learningGoal = String(localized: "学习目标") - static let learningMethod = String(localized: "学习方法") - static let dailyMinutes = String(localized: "每日学习时间") - static let examPrep = String(localized: "备考考试") - static let careerSkill = String(localized: "职业技能") - static let generalLearning = String(localized: "通识学习") - static let customGoal = String(localized: "自定义") - static let inputKnowledge = String(localized: "输入知识") - static let activeOutput = String(localized: "主动输出") - static let aiAnalysis = String(localized: "AI 分析") - static let masterKnowledge = String(localized: "掌握知识") - - // MARK: - Study - - static let studyWorkspace = String(localized: "学习工作台") - static let todayTasks = String(localized: "今日任务") - static let todayProgress = String(localized: "今日进度") - static let weeklyActivity = String(localized: "本周学习活跃") - static let studied = String(localized: "已学") - static let remaining = String(localized: "剩余") - static let mastery = String(localized: "掌握") - static let tasksUnit = String(localized: "个任务") - static let minutesUnit = String(localized: "分钟") - static let aiAutoSchedule = String(localized: "AI 自动排期") - static let streak14Days = String(localized: "14 天连续") - - // MARK: - Review - - static let reviewPlan = String(localized: "复习计划") - static let today = String(localized: "今天") - static let tomorrow = String(localized: "明天") - static let thisWeek = String(localized: "本周") - static let noReviewTasks = String(localized: "暂无复习任务") - static let noReviewHint = String(localized: "完成学习后 AI 会自动生成复习计划") - - // MARK: - Feedback - - static let feedbackSubmitted = String(localized: "反馈已提交") - static let feedbackThanks = String(localized: "感谢你的反馈,我们会尽快处理。") - static let feedbackPlaceholder = String(localized: "请描述你遇到的问题或建议…") - static let feedbackCategory = String(localized: "反馈类型") - static let submitFeedback = String(localized: "提交反馈") - - // MARK: - Settings - - static let language = String(localized: "语言") - static let appearance = String(localized: "外观") - static let followSystem = String(localized: "跟随系统") - static let darkMode = String(localized: "深色模式") - static let lightMode = String(localized: "浅色模式") - static let learningGoalSettings = String(localized: "学习目标设置") - static let reviewReminder = String(localized: "复习提醒") - static let learningReport = String(localized: "学习报告") - static let learningMethodPref = String(localized: "学习方法偏好") - static let dataSync = String(localized: "数据同步与备份") - - // MARK: - Error - - static let networkError = String(localized: "网络请求失败") - static let authExpired = String(localized: "登录状态已失效") - static let parseError = String(localized: "数据解析失败") - static let invalidURL = String(localized: "无效的请求地址") - static let serverError = String(localized: "服务器返回错误") - static let tokenExpired = String(localized: "Token 已过期") - static let appleCredentialFailed = String(localized: "无法获取 Apple 登录凭证") - static let missingIdentityToken = String(localized: "未获取到身份验证信息") - - // MARK: - Content Categories - - static let catBug = String(localized: "Bug 反馈") - static let catFeature = String(localized: "功能建议") - static let catContent = String(localized: "内容问题") - static let catOther = String(localized: "其他") -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/AIAnalysis.swift b/AIStudyApp/AIStudyApp/Core/Models/AIAnalysis.swift deleted file mode 100644 index ad12bfe..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/AIAnalysis.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -struct AIAnalysis: Codable, Identifiable { - let id: String - let userId: String - let sessionId: String - let inputText: String - let outputJson: String - let masteryScore: Int - let weakPoints: [String] - let suggestions: [String] - let modelName: String - let createdAt: String - let costEstimate: Double? -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift new file mode 100644 index 0000000..487c0ed --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -0,0 +1,315 @@ +// +// Models.swift - 对应 api-server 的所有 DTO +// + +import Foundation + +// MARK: - Waitlist + +struct WaitlistEntry: Codable, Identifiable { + let id: String + let email: String + let nickname: String? + let devices: [String]? + let interests: [String]? + let painpoint: String? + let willingBeta: Bool? + let createdAt: String +} + +struct WaitlistCreateRequest: Codable { + let email: String + let nickname: String? + let devices: [String]? + let interests: [String]? + let painpoint: String? + let willingBeta: Bool? + + init(email: String, nickname: String? = nil, devices: [String]? = nil, + interests: [String]? = nil, painpoint: String? = nil, willingBeta: Bool = true) { + self.email = email + self.nickname = nickname + self.devices = devices + self.interests = interests + self.painpoint = painpoint + self.willingBeta = willingBeta + } +} + +struct WaitlistResponse: Codable { + let success: Bool + let message: String? + let data: WaitlistEntry? +} + +struct WaitlistStats: Codable { + let total: Int? + let today: Int? + let deviceBreakdown: [String: Int]? + let interestBreakdown: [String: Int]? + + enum CodingKeys: String, CodingKey { + case total, today + case deviceBreakdown = "deviceBreakdown" + case interestBreakdown = "interestBreakdown" + } +} + +// MARK: - Auth + +struct AuthResponse: Codable { + let success: Bool + let data: AuthTokens? +} + +struct AuthTokens: Codable { + let accessToken: String + let refreshToken: String? + let expiresIn: Int? + + enum CodingKeys: String, CodingKey { + case accessToken, refreshToken, expiresIn + } +} + +struct AppleAuthRequest: Codable { + let identityToken: String + let fullName: AppleFullName? + + struct AppleFullName: Codable { + let givenName: String? + let familyName: String? + } +} + +// MARK: - User + +struct UserProfileResponse: Codable { + let success: Bool + let data: UserProfileData? +} + +struct UserProfileData: Codable, Identifiable { + let id: String + let email: String + let nickname: String? + let avatar: String? + let preferences: UserPreferences? + let stats: UserStats? + let createdAt: String? +} + +struct UserPreferences: Codable { + let dailyGoal: Int? + let reminderTime: String? + let theme: String? +} + +struct UserStats: Codable { + let totalLearningDays: Int? + let completedCourses: Int? + let totalMinutes: Int? +} + +struct UpdateUserRequest: Codable { + let nickname: String? + let preferences: UserPreferences? +} + +// MARK: - Knowledge Base + +struct KnowledgeBase: Codable, Identifiable { + let id: String + let name: String + let description: String? + let icon: String? + let itemCount: Int? + let mastery: Double? + let tags: [String]? + let createdAt: String? +} + +struct KnowledgeBaseListResponse: Codable { + let success: Bool + let data: [KnowledgeBase]? +} + +struct CreateKnowledgeBaseRequest: Codable { + let name: String + let description: String? + let icon: String? +} + +// MARK: - Knowledge Items + +struct KnowledgeItem: Codable, Identifiable { + let id: String + let title: String + let content: String? + let baseId: String? + let tags: [String]? + let mastery: Double? + let status: String? + let createdAt: String? + + enum CodingKeys: String, CodingKey { + case id, title, content, tags, mastery, status, createdAt + case baseId = "baseId" + } +} + +struct KnowledgeItemListResponse: Codable { + let success: Bool + let data: [KnowledgeItem]? +} + +struct CreateKnowledgeItemRequest: Codable { + let title: String + let content: String? + let baseId: String + let tags: [String]? + + enum CodingKeys: String, CodingKey { + case title, content, tags + case baseId = "baseId" + } +} + +// MARK: - AI Analysis + +struct AIAnalysisRequest: Codable { + let text: String + let type: String + let context: AIAnalysisContext? + + struct AIAnalysisContext: Codable { + let knowledgeBaseIds: [String]? + let focusItemIds: [String]? + } +} + +struct AIAnalysisResponse: Codable { + let success: Bool + let data: AIAnalysisResult? +} + +struct AIAnalysisResult: Codable, Identifiable { + let id: String + let type: String? + let summary: String? + let strengths: [String]? + let weaknesses: [String]? + let suggestions: [String]? + let score: Double? + let createdAt: String? +} + +// MARK: - Feedback + +struct FeedbackCreateRequest: Codable { + let type: String + let content: String + let contact: String? + + init(type: String = "general", content: String, contact: String? = nil) { + self.type = type + self.content = content + self.contact = contact + } +} + +struct FeedbackResponse: Codable { + let success: Bool + let message: String? + let data: FeedbackData? +} + +struct FeedbackData: Codable, Identifiable { + let id: String + let type: String? + let content: String? + let status: String? + let createdAt: String? +} + +// MARK: - Learning Session + +struct LearningSessionCreateRequest: Codable { + let knowledgeBaseId: String? + let notes: String? + + enum CodingKeys: String, CodingKey { + case notes + case knowledgeBaseId = "baseId" + } +} + +struct LearningSessionResponse: Codable { + let success: Bool + let data: LearningSessionData? +} + +struct LearningSessionData: Codable, Identifiable { + let id: String + let startedAt: String? + let endedAt: String? + let durationMinutes: Double? +} + +// MARK: - Activity + +struct ActivitySummary: Codable { + let totalSessions: Int? + let totalMinutes: Double? + let streakDays: Int? + let weeklyMinutes: Double? +} + +struct ActivitySummaryResponse: Codable { + let success: Bool + let data: ActivitySummary? +} + +// MARK: - Reviews + +struct ReviewTask: Codable, Identifiable { + let id: String + let itemId: String? + let itemName: String? + let dueDate: String? + let type: String? +} + +struct ReviewListResponse: Codable { + let success: Bool + let data: [ReviewTask]? +} + +// MARK: - Focus Items / Weak Points + +struct FocusItem: Codable, Identifiable { + let id: String + let itemId: String? + let itemName: String? + let reason: String? + let priority: String? + let completed: Bool? + let createdAt: String? +} + +struct FocusItemListResponse: Codable { + let success: Bool + let data: [FocusItem]? +} + +// MARK: - Generic API Response + +struct APIStatusResponse: Codable { + let status: String? + let success: Bool? +} + +struct GenericSuccessResponse: Codable { + let success: Bool + let message: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/AuthModels.swift b/AIStudyApp/AIStudyApp/Core/Models/AuthModels.swift deleted file mode 100644 index 7742806..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/AuthModels.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -// MARK: - Request - -struct AppleLoginRequest: Encodable { - let identityToken: String - let authorizationCode: String? - let userIdentifier: String - let fullName: AppleFullName? - let email: String? -} - -struct AppleFullName: Encodable { - let givenName: String? - let familyName: String? -} - -// MARK: - Response - -struct AuthResponse: Decodable { - let accessToken: String - let refreshToken: String - let expiresIn: Int - let user: User -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Feedback.swift b/AIStudyApp/AIStudyApp/Core/Models/Feedback.swift deleted file mode 100644 index 24d0587..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/Feedback.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -struct Feedback: Codable, Identifiable { - let id: String - let userId: String - let category: FeedbackCategory - let content: String - let createdAt: String -} - -enum FeedbackCategory: String, Codable, CaseIterable, Identifiable { - case bug = "Bug 反馈" - case feature = "功能建议" - case content = "内容问题" - case other = "其他" - - var id: String { rawValue } -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift deleted file mode 100644 index b515d1a..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -struct KnowledgeBase: Codable, Identifiable { - let id: String - let title: String - let description: String - let language: String - let targetUser: String - let createdAt: String - let updatedAt: String -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/LearningPath.swift b/AIStudyApp/AIStudyApp/Core/Models/LearningPath.swift deleted file mode 100644 index 051f000..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/LearningPath.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -struct LearningPath: Codable, Identifiable { - let id: String - let knowledgeBaseId: String - let title: String - let description: String - let estimatedDays: Int - let order: Int -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/LearningSession.swift b/AIStudyApp/AIStudyApp/Core/Models/LearningSession.swift deleted file mode 100644 index d733a68..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/LearningSession.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -struct LearningSession: Codable, Identifiable { - let id: String - let userId: String - let lessonId: String - let startedAt: String - let endedAt: String? - let userInput: String - let aiAnalysis: AIAnalysis? - let masteryScore: Int - let weakPoints: [String] - let nextSuggestion: String? - let reviewAt: String? -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Lesson.swift b/AIStudyApp/AIStudyApp/Core/Models/Lesson.swift deleted file mode 100644 index 17a4cfd..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/Lesson.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -struct Lesson: Codable, Identifiable { - let id: String - let pathId: String - let title: String - let content: String - let objectives: [String] - let keyPoints: [String] - let recallQuestions: [String] - let practicePrompt: String - let order: Int - let estimatedMinutes: Int -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/ReviewTask.swift b/AIStudyApp/AIStudyApp/Core/Models/ReviewTask.swift deleted file mode 100644 index 265bb0a..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/ReviewTask.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -struct ReviewTask: Codable, Identifiable { - let id: String - let userId: String - let lessonId: String - let sourceSessionId: String - let reviewType: ReviewType - let scheduledAt: String - let completedAt: String? - let status: ReviewTaskStatus -} - -enum ReviewType: String, Codable { - case spacedRepetition - case feynman - case recall - case weakPoint -} - -enum ReviewTaskStatus: String, Codable { - case pending - case completed - case skipped - case overdue -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/User.swift b/AIStudyApp/AIStudyApp/Core/Models/User.swift deleted file mode 100644 index 09a47ee..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/User.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -struct User: Codable, Identifiable { - let id: String - let appleUserId: String - let displayName: String? - let email: String? - let preferredLanguage: String - let onboardingCompleted: Bool - let createdAt: String - let lastLoginAt: String? - let status: UserStatus -} - -enum UserStatus: String, Codable { - case active - case inactive - case suspended -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/UserLearningProfile.swift b/AIStudyApp/AIStudyApp/Core/Models/UserLearningProfile.swift deleted file mode 100644 index 306cbdb..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/UserLearningProfile.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -struct UserLearningProfile: Codable, Identifiable { - let id: String - let userId: String - let currentKnowledgeBaseId: String? - let currentPathId: String? - let currentLessonId: String? - let overallLevel: Int - let weakPoints: [String] - let strengths: [String] - let recentMistakes: [String] - let reviewQueue: [String] - let learningStreak: Int - let updatedAt: String -} diff --git a/AIStudyApp/AIStudyApp/Core/Models/WaitlistEntry.swift b/AIStudyApp/AIStudyApp/Core/Models/WaitlistEntry.swift deleted file mode 100644 index a334047..0000000 --- a/AIStudyApp/AIStudyApp/Core/Models/WaitlistEntry.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -struct WaitlistEntry: Codable { - let email: String - let name: String? - let source: String - - init(email: String, name: String? = nil, source: String = "ios_app") { - self.email = email - self.name = name - self.source = source - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift b/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift index 9321cc2..53c3bd4 100644 --- a/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift +++ b/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift @@ -1,75 +1,78 @@ +// +// APIClient.swift - 通用 HTTP 客户端 +// + import Foundation -protocol APIClientProtocol { - func request(_ endpoint: APIEndpoint) async throws -> T - func requestVoid(_ endpoint: APIEndpoint) async throws -} +actor APIClient { + static let shared = APIClient() -final class APIClient: APIClientProtocol { - private let baseURL: URL - private let tokenStore: TokenStoreProtocol? private let session: URLSession - private let decoder: JSONDecoder + private var token: String? - init(baseURL: URL, tokenStore: TokenStoreProtocol? = nil, session: URLSession = .shared) { - self.baseURL = baseURL - self.tokenStore = tokenStore - self.session = session - self.decoder = JSONDecoder() + private init() { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = APIConfig.timeout + config.timeoutIntervalForResource = 60 + session = URLSession(configuration: config) } - func request(_ endpoint: APIEndpoint) async throws -> T { - let (data, response) = try await perform(endpoint) - return try decodeResponse(data, response: response) + func setToken(_ token: String?) { + self.token = token } - func requestVoid(_ endpoint: APIEndpoint) async throws { - let (_, response) = try await perform(endpoint) - guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { - throw APIError.httpError((response as? HTTPURLResponse)?.statusCode ?? 0, nil) - } - } + // MARK: - Generic request - // MARK: - Private + func request( + _ path: String, + method: String = "GET", + body: Encodable? = nil, + queryItems: [URLQueryItem]? = nil + ) async throws -> T { + var components = URLComponents(url: APIConfig.url(path), resolvingAgainstBaseURL: true)! + if let queryItems { components.queryItems = queryItems } - private func perform(_ endpoint: APIEndpoint) async throws -> (Data, URLResponse) { - let url = baseURL.appendingPathComponent(endpoint.path) - var request = URLRequest(url: url) - request.httpMethod = endpoint.method.rawValue + var request = URLRequest(url: components.url!) + request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") - request.httpBody = endpoint.body - request.timeoutInterval = 30 - if endpoint.requiresAuth { - if let token = try tokenStore?.getAccessToken() { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } else { - throw APIError.unauthorized - } + if let token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + if let body { + request.httpBody = try JSONEncoder().encode(AnyEncodable(body)) } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { - throw APIError.network(URLError(.badServerResponse)) + throw APIError.networkError(NSError(domain: "", code: -1)) } switch httpResponse.statusCode { - case 200...299: - return (data, response) + case 200, 201: + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw APIError.decodingFailed(error.localizedDescription) + } case 401: throw APIError.unauthorized + case 400..<500: + let msg = String(data: data, encoding: .utf8) ?? "" + throw APIError.serverError(msg) default: - throw APIError.httpError(httpResponse.statusCode, data) - } - } - - private func decodeResponse(_ data: Data, response: URLResponse) throws -> T { - do { - return try decoder.decode(T.self, from: data) - } catch { - throw APIError.decoding(error) + throw APIError.requestFailed(httpResponse.statusCode) } } } + +// MARK: - Helper for encoding arbitrary Encodable + +struct AnyEncodable: Encodable { + let value: Encodable + init(_ value: Encodable) { self.value = value } + func encode(to encoder: Encoder) throws { try value.encode(to: encoder) } +} diff --git a/AIStudyApp/AIStudyApp/Core/Network/APIConfig.swift b/AIStudyApp/AIStudyApp/Core/Network/APIConfig.swift new file mode 100644 index 0000000..b848971 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Network/APIConfig.swift @@ -0,0 +1,15 @@ +// +// APIConfig.swift +// + +import Foundation + +enum APIConfig { + static let baseURL = "http://81.70.187.179:3001" + + static let timeout: TimeInterval = 30 + + static func url(_ path: String) -> URL { + URL(string: "\(baseURL)\(path)")! + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Network/APIEndpoint.swift b/AIStudyApp/AIStudyApp/Core/Network/APIEndpoint.swift deleted file mode 100644 index a1dd994..0000000 --- a/AIStudyApp/AIStudyApp/Core/Network/APIEndpoint.swift +++ /dev/null @@ -1,123 +0,0 @@ -import Foundation - -enum APIEndpoint { - // Auth - case appleLogin(AppleLoginRequest) - case refreshToken(String) - case me - - // Sessions - case sessions - case session(String) - case createSession(CreateSessionRequest) - case updateSession(String, UpdateSessionRequest) - case progress - - // AI - case analyze(AIAnalyzeRequest) - case analysis(String) - case recallQuestions(String) - case feynmanPrompt(String) - - // Reviews - case reviews - case reviewsToday - case reviewsTomorrow - case reviewsWeek - case updateReview(String, UpdateReviewRequest) - case generateReviews - - // Knowledge - case knowledgeBases - case knowledgeBase(String) - case paths(String) - case path(String) - case lesson(String) - - // Feedback - case submitFeedback(SubmitFeedbackRequest) - - // MARK: - Path - - var path: String { - switch self { - case .appleLogin: return "/api/auth/apple" - case .refreshToken: return "/api/auth/refresh" - case .me: return "/api/users/me" - case .sessions: return "/api/sessions" - case .session(let id): return "/api/sessions/\(id)" - case .createSession: return "/api/sessions" - case .updateSession(let id, _): return "/api/sessions/\(id)" - case .progress: return "/api/progress" - case .analyze: return "/api/ai/analyze" - case .analysis(let id): return "/api/ai/analysis/\(id)" - case .recallQuestions(let id): return "/api/ai/recall/\(id)" - case .feynmanPrompt(let id): return "/api/ai/feynman/\(id)" - case .reviews: return "/api/reviews" - case .reviewsToday: return "/api/reviews/today" - case .reviewsTomorrow: return "/api/reviews/tomorrow" - case .reviewsWeek: return "/api/reviews/week" - case .updateReview(let id, _): return "/api/reviews/\(id)" - case .generateReviews: return "/api/reviews/generate" - case .knowledgeBases: return "/api/knowledge-bases" - case .knowledgeBase(let id): return "/api/knowledge-bases/\(id)" - case .paths(let kbId): return "/api/knowledge-bases/\(kbId)/paths" - case .path(let id): return "/api/paths/\(id)" - case .lesson(let id): return "/api/lessons/\(id)" - case .submitFeedback: return "/api/feedback" - } - } - - // MARK: - Method - - var method: HTTPMethod { - switch self { - case .appleLogin, .refreshToken, .createSession, .analyze, - .recallQuestions, .feynmanPrompt, .generateReviews, .submitFeedback: - return .post - case .updateSession, .updateReview: - return .put - case .me, .sessions, .session, .progress, .analysis, - .reviews, .reviewsToday, .reviewsTomorrow, .reviewsWeek, - .knowledgeBases, .knowledgeBase, .paths, .path, .lesson: - return .get - } - } - - // MARK: - Body - - var body: Data? { - let encoder = JSONEncoder() - switch self { - case .appleLogin(let r): return try? encoder.encode(r) - case .refreshToken(let t): return try? encoder.encode(["refreshToken": t]) - case .createSession(let r): return try? encoder.encode(r) - case .updateSession(_, let r): return try? encoder.encode(r) - case .analyze(let r): return try? encoder.encode(r) - case .recallQuestions(let id): return try? encoder.encode(["lessonId": id]) - case .feynmanPrompt(let id): return try? encoder.encode(["lessonId": id]) - case .updateReview(_, let r): return try? encoder.encode(r) - case .submitFeedback(let r): return try? encoder.encode(r) - case .generateReviews: return try? encoder.encode([:] as [String: String]) - default: return nil - } - } - - // MARK: - Auth - - var requiresAuth: Bool { - switch self { - case .appleLogin, .refreshToken: - return false - default: - return true - } - } -} - -enum HTTPMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case delete = "DELETE" -} diff --git a/AIStudyApp/AIStudyApp/Core/Network/APIError.swift b/AIStudyApp/AIStudyApp/Core/Network/APIError.swift index 0843658..82f8c85 100644 --- a/AIStudyApp/AIStudyApp/Core/Network/APIError.swift +++ b/AIStudyApp/AIStudyApp/Core/Network/APIError.swift @@ -1,39 +1,25 @@ +// +// APIError.swift +// + import Foundation -enum APIError: Error, LocalizedError { +enum APIError: LocalizedError { case invalidURL - case network(Error) - case httpError(Int, Data?) - case decoding(Error) + case requestFailed(Int) + case decodingFailed(String) + case networkError(Error) case unauthorized - case tokenExpired case serverError(String) var errorDescription: String? { switch self { - case .invalidURL: - return ZXStrings.invalidURL - case .network(let error): - return "\(ZXStrings.networkError):\(error.localizedDescription)" - case .httpError(let code, _): - return "\(ZXStrings.serverError)(\(code))" - case .decoding: - return ZXStrings.parseError - case .unauthorized: - return ZXStrings.authExpired - case .tokenExpired: - return ZXStrings.tokenExpired - case .serverError(let msg): - return msg - } - } - - var isAuthenticationError: Bool { - switch self { - case .unauthorized, .tokenExpired: - return true - default: - return false + case .invalidURL: return "无效的请求地址" + case .requestFailed(let code): return "请求失败 (\(code))" + case .decodingFailed(let msg): return "数据解析失败: \(msg)" + case .networkError(let e): return e.localizedDescription + case .unauthorized: return "未授权,请重新登录" + case .serverError(let msg): return msg } } } diff --git a/AIStudyApp/AIStudyApp/Core/Repository/FileCache.swift b/AIStudyApp/AIStudyApp/Core/Repository/FileCache.swift deleted file mode 100644 index 6a80c2d..0000000 --- a/AIStudyApp/AIStudyApp/Core/Repository/FileCache.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -// MARK: - File Cache Protocol - -protocol FileCacheProtocol { - func load(_ type: T.Type, forKey key: String) throws -> T? - func save(_ value: T, forKey key: String) throws - func remove(forKey key: String) throws - func clear() throws -} - -// MARK: - JSON File Cache - -final class FileCache: FileCacheProtocol { - private let directory: URL - - init(suite: String = "repository_cache") { - let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! - directory = base.appendingPathComponent(suite) - try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - } - - func load(_ type: T.Type, forKey key: String) throws -> T? { - let url = fileURL(forKey: key) - guard FileManager.default.fileExists(atPath: url.path) else { return nil } - let data = try Data(contentsOf: url) - return try JSONDecoder().decode(T.self, from: data) - } - - func save(_ value: T, forKey key: String) throws { - let data = try JSONEncoder().encode(value) - try data.write(to: fileURL(forKey: key), options: .atomic) - } - - func remove(forKey key: String) throws { - let url = fileURL(forKey: key) - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(at: url) - } - } - - func clear() throws { - if FileManager.default.fileExists(atPath: directory.path) { - try FileManager.default.removeItem(at: directory) - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - } - } - - private func fileURL(forKey key: String) -> URL { - directory.appendingPathComponent(key.replacingOccurrences(of: "/", with: "_") + ".json") - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Repository/KnowledgeRepository.swift b/AIStudyApp/AIStudyApp/Core/Repository/KnowledgeRepository.swift deleted file mode 100644 index 86c572a..0000000 --- a/AIStudyApp/AIStudyApp/Core/Repository/KnowledgeRepository.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation - -// MARK: - Knowledge Repository - -final class KnowledgeRepository { - let bases: BaseRepository - let paths: PathRepository - let lessons: LessonRepository - - init(knowledgeService: KnowledgeServiceProtocol) { - self.bases = BaseRepository( - cacheKey: "knowledge_bases", - ttl: 600 - ) { - try await knowledgeService.fetchKnowledgeBases() - } - - self.paths = PathRepository(service: knowledgeService) - self.lessons = LessonRepository(service: knowledgeService) - } -} - -// MARK: - Path Repository (keyed by knowledge base ID) - -final class PathRepository { - private let service: KnowledgeServiceProtocol - private let cache = FileCache() - - init(service: KnowledgeServiceProtocol) { - self.service = service - } - - func fetch(for knowledgeBaseId: String) async throws -> [LearningPath] { - let key = "paths_\(knowledgeBaseId)" - if let cached: Cached<[LearningPath]> = try? cache.load(Cached<[LearningPath]>.self, forKey: key) { - if Date().timeIntervalSince(cached.timestamp) < 600 { return cached.value } - } - let items = try await service.fetchPaths(knowledgeBaseId: knowledgeBaseId) - try cache.save(Cached(value: items, timestamp: Date()), forKey: key) - return items - } -} - -// MARK: - Lesson Repository (keyed by path ID) - -final class LessonRepository { - private let service: KnowledgeServiceProtocol - private let cache = FileCache() - - init(service: KnowledgeServiceProtocol) { - self.service = service - } - - func fetch(for pathId: String) async throws -> [Lesson] { - let key = "lessons_\(pathId)" - if let cached: Cached<[Lesson]> = try? cache.load(Cached<[Lesson]>.self, forKey: key) { - if Date().timeIntervalSince(cached.timestamp) < 600 { return cached.value } - } - let detail = try await service.fetchPath(id: pathId) - try cache.save(Cached(value: detail.lessons, timestamp: Date()), forKey: key) - return detail.lessons - } -} - -// MARK: - Cached Wrapper - -private struct Cached: Codable { - let value: T - let timestamp: Date -} diff --git a/AIStudyApp/AIStudyApp/Core/Repository/Repository.swift b/AIStudyApp/AIStudyApp/Core/Repository/Repository.swift deleted file mode 100644 index a13488b..0000000 --- a/AIStudyApp/AIStudyApp/Core/Repository/Repository.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -// MARK: - Repository Protocol - -protocol RepositoryProtocol { - associatedtype Item: Codable - func fetch() async throws -> [Item] - func sync() async throws -> [Item] - func clearCache() throws -} - -// MARK: - Base Repository - -/// Generic repository: cache-first with stale-then-refresh strategy. -class BaseRepository: RepositoryProtocol { - private let remote: () async throws -> [Item] - private let cache: FileCacheProtocol - private let cacheKey: String - private let ttl: TimeInterval - - init( - cache: FileCacheProtocol = FileCache(), - cacheKey: String, - ttl: TimeInterval = 300, - remote: @escaping () async throws -> [Item] - ) { - self.cache = cache - self.cacheKey = cacheKey - self.ttl = ttl - self.remote = remote - } - - /// Returns cached data immediately if fresh, then refreshes in background. - /// If cache is stale or missing, fetches from remote. - func fetch() async throws -> [Item] { - if let cached: Timestamped<[Item]> = try? cache.load(Timestamped<[Item]>.self, forKey: cacheKey) { - if Date().timeIntervalSince(cached.timestamp) < ttl { - return cached.value - } - } - return try await sync() - } - - /// Force-fetches from remote and updates cache. - func sync() async throws -> [Item] { - let items = try await remote() - let stamped = Timestamped(value: items, timestamp: Date()) - try cache.save(stamped, forKey: cacheKey) - return items - } - - func clearCache() throws { - try cache.remove(forKey: cacheKey) - } -} - -// MARK: - Timestamped Wrapper - -private struct Timestamped: Codable { - let value: T - let timestamp: Date -} diff --git a/AIStudyApp/AIStudyApp/Core/Repository/ReviewRepository.swift b/AIStudyApp/AIStudyApp/Core/Repository/ReviewRepository.swift deleted file mode 100644 index bd8252e..0000000 --- a/AIStudyApp/AIStudyApp/Core/Repository/ReviewRepository.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -// MARK: - Review Repository - -final class ReviewRepository { - let today: BaseRepository - let tomorrow: BaseRepository - let week: BaseRepository - let all: BaseRepository - - init(reviewService: ReviewServiceProtocol) { - self.today = BaseRepository( - cacheKey: "reviews_today", - ttl: 120 - ) { - try await reviewService.fetchTodayReviews() - } - - self.tomorrow = BaseRepository( - cacheKey: "reviews_tomorrow", - ttl: 300 - ) { - try await reviewService.fetchTomorrowReviews() - } - - self.week = BaseRepository( - cacheKey: "reviews_week", - ttl: 600 - ) { - try await reviewService.fetchWeekReviews() - } - - self.all = BaseRepository( - cacheKey: "reviews_all", - ttl: 300 - ) { - try await reviewService.fetchReviews() - } - } - - func clearAll() throws { - try today.clearCache() - try tomorrow.clearCache() - try week.clearCache() - try all.clearCache() - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/AIService.swift b/AIStudyApp/AIStudyApp/Core/Services/AIService.swift deleted file mode 100644 index f3833c0..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/AIService.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation - -// MARK: - AI Service Protocol - -protocol AIServiceProtocol { - func analyze(request: AIAnalyzeRequest) async throws -> AIAnalysis - func fetchAnalysis(id: String) async throws -> AIAnalysis - func generateRecallQuestions(lessonId: String) async throws -> [String] - func generateFeynmanPrompt(lessonId: String) async throws -> String -} - -// MARK: - Request / Response Models - -struct AIAnalyzeRequest: Codable { - let sessionId: String - let inputText: String - let lessonId: String -} - -struct RecallQuestionsResponse: Codable { - let questions: [String] -} - -struct FeynmanPromptResponse: Codable { - let prompt: String -} - -// MARK: - AI Service Implementation - -final class AIService: AIServiceProtocol { - private let apiClient: APIClientProtocol - - init(apiClient: APIClientProtocol) { - self.apiClient = apiClient - } - - func analyze(request: AIAnalyzeRequest) async throws -> AIAnalysis { - try await apiClient.request(.analyze(request)) - } - - func fetchAnalysis(id: String) async throws -> AIAnalysis { - try await apiClient.request(.analysis(id)) - } - - func generateRecallQuestions(lessonId: String) async throws -> [String] { - let response: RecallQuestionsResponse = try await apiClient.request(.recallQuestions(lessonId)) - return response.questions - } - - func generateFeynmanPrompt(lessonId: String) async throws -> String { - let response: FeynmanPromptResponse = try await apiClient.request(.feynmanPrompt(lessonId)) - return response.prompt - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift new file mode 100644 index 0000000..07b498a --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -0,0 +1,172 @@ +// +// APIService.swift - 各模块 API 调用封装 +// + +import Foundation + +// MARK: - Waitlist + +@MainActor +class WaitlistService { + static let shared = WaitlistService() + private let client = APIClient.shared + + func join(email: String, nickname: String?, devices: [String]?, + interests: [String]?, painpoint: String?, willingBeta: Bool = true) async throws -> WaitlistResponse { + let body = WaitlistCreateRequest(email: email, nickname: nickname, devices: devices, + interests: interests, painpoint: painpoint, willingBeta: willingBeta) + return try await client.request("/waitlist", method: "POST", body: body) + } + + func stats() async throws -> WaitlistStats { + return try await client.request("/waitlist/stats") + } +} + +// MARK: - Auth + +@MainActor +class AuthService { + static let shared = AuthService() + private let client = APIClient.shared + + func appleLogin(identityToken: String, givenName: String?, familyName: String?) async throws -> AuthResponse { + let body = AppleAuthRequest( + identityToken: identityToken, + fullName: givenName != nil + ? AppleAuthRequest.AppleFullName(givenName: givenName, familyName: familyName) + : nil + ) + let resp: AuthResponse = try await client.request("/auth/apple", method: "POST", body: body) + if let token = resp.data?.accessToken { + await client.setToken(token) + } + return resp + } + + func logout() async throws { + let _: GenericSuccessResponse = try await client.request("/auth/logout", method: "POST") + await client.setToken(nil) + } +} + +// MARK: - User + +@MainActor +class UserService { + static let shared = UserService() + private let client = APIClient.shared + + func myProfile() async throws -> UserProfileResponse { + return try await client.request("/users/me") + } + + func updateProfile(nickname: String?, preferences: UserPreferences?) async throws -> UserProfileResponse { + let body = UpdateUserRequest(nickname: nickname, preferences: preferences) + return try await client.request("/users/me", method: "PATCH", body: body) + } +} + +// MARK: - Knowledge Base + +@MainActor +class KnowledgeBaseService { + static let shared = KnowledgeBaseService() + private let client = APIClient.shared + + func list() async throws -> KnowledgeBaseListResponse { + return try await client.request("/knowledge-bases") + } + + func create(name: String, description: String?, icon: String?) async throws -> KnowledgeBaseListResponse { + let body = CreateKnowledgeBaseRequest(name: name, description: description, icon: icon) + return try await client.request("/knowledge-bases", method: "POST", body: body) + } + + func detail(id: String) async throws -> KnowledgeBaseListResponse { + return try await client.request("/knowledge-bases/\(id)") + } +} + +// MARK: - Knowledge Items + +@MainActor +class KnowledgeItemService { + static let shared = KnowledgeItemService() + private let client = APIClient.shared + + func list(baseId: String) async throws -> KnowledgeItemListResponse { + return try await client.request("/knowledge-items", queryItems: [URLQueryItem(name: "baseId", value: baseId)]) + } + + func detail(id: String) async throws -> KnowledgeItemListResponse { + return try await client.request("/knowledge-items/\(id)") + } + + func create(baseId: String, title: String, content: String?, tags: [String]?) async throws -> KnowledgeItemListResponse { + let body = CreateKnowledgeItemRequest(title: title, content: content, baseId: baseId, tags: tags) + return try await client.request("/knowledge-items", method: "POST", body: body) + } +} + +// MARK: - AI Analysis + +@MainActor +class AIAnalysisService { + static let shared = AIAnalysisService() + private let client = APIClient.shared + + func analyze(text: String, type: String = "weakness", context: AIAnalysisRequest.AIAnalysisContext? = nil) async throws -> AIAnalysisResponse { + let body = AIAnalysisRequest(text: text, type: type, context: context) + return try await client.request("/ai-analysis", method: "POST", body: body) + } +} + +// MARK: - Activity & Stats + +@MainActor +class ActivityService { + static let shared = ActivityService() + private let client = APIClient.shared + + func summary() async throws -> ActivitySummaryResponse { + return try await client.request("/activity/summary") + } +} + +// MARK: - Reviews + +@MainActor +class ReviewService { + static let shared = ReviewService() + private let client = APIClient.shared + + func due() async throws -> ReviewListResponse { + return try await client.request("/reviews/due") + } +} + +// MARK: - Focus Items / Weak Points + +@MainActor +class FocusItemService { + static let shared = FocusItemService() + private let client = APIClient.shared + + func list() async throws -> FocusItemListResponse { + return try await client.request("/focus-items") + } +} + +// MARK: - Feedback + +@MainActor +class FeedbackService { + static let shared = FeedbackService() + private let client = APIClient.shared + + func submit(type: String = "general", content: String, contact: String? = nil) async throws -> FeedbackResponse { + let body = FeedbackCreateRequest(type: type, content: content, contact: contact) + return try await client.request("/feedback", method: "POST", body: body) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/AuthService.swift b/AIStudyApp/AIStudyApp/Core/Services/AuthService.swift deleted file mode 100644 index 206ecde..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/AuthService.swift +++ /dev/null @@ -1,118 +0,0 @@ -import AuthenticationServices -import Foundation - -final class AuthService: NSObject, AuthServiceProtocol { - private let apiClient: APIClientProtocol - private let tokenStore: TokenStoreProtocol - - private var continuation: CheckedContinuation? - - init(apiClient: APIClientProtocol, tokenStore: TokenStoreProtocol) { - self.apiClient = apiClient - self.tokenStore = tokenStore - } - - // MARK: - Public - - func loginWithApple() async throws -> AuthResponse { - let credential = try await requestAppleIDCredential() - let identityTokenData = credential.identityToken! - let identityToken = String(data: identityTokenData, encoding: .utf8)! - - let request = AppleLoginRequest( - identityToken: identityToken, - authorizationCode: credential.authorizationCode.map { String(data: $0, encoding: .utf8)! }, - userIdentifier: credential.user, - fullName: credential.fullName.map { - AppleFullName(givenName: $0.givenName, familyName: $0.familyName) - }, - email: credential.email - ) - - let authResponse: AuthResponse = try await apiClient.request(.appleLogin(request)) - try tokenStore.saveAccessToken(authResponse.accessToken) - try tokenStore.saveRefreshToken(authResponse.refreshToken) - return authResponse - } - - func refreshSession() async throws -> AuthResponse { - guard let refreshToken = try tokenStore.getRefreshToken() else { - throw APIError.unauthorized - } - let authResponse: AuthResponse = try await apiClient.request(.refreshToken(refreshToken)) - try tokenStore.saveAccessToken(authResponse.accessToken) - try tokenStore.saveRefreshToken(authResponse.refreshToken) - return authResponse - } - - func logout() async throws { - try tokenStore.clearAll() - } - - func fetchCurrentUser() async throws -> User { - try await apiClient.request(.me) - } - - // MARK: - ASAuthorizationController - - private func requestAppleIDCredential() async throws -> ASAuthorizationAppleIDCredential { - return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation - let provider = ASAuthorizationAppleIDProvider() - let request = provider.createRequest() - request.requestedScopes = [.fullName, .email] - - let controller = ASAuthorizationController(authorizationRequests: [request]) - controller.delegate = self - controller.presentationContextProvider = self - controller.performRequests() - } - } -} - -// MARK: - ASAuthorizationControllerDelegate - -extension AuthService: ASAuthorizationControllerDelegate { - func authorizationController( - controller: ASAuthorizationController, - didCompleteWithAuthorization authorization: ASAuthorization - ) { - guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { - continuation?.resume(throwing: AuthError.invalidCredential) - return - } - continuation?.resume(returning: credential) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - continuation?.resume(throwing: error) - } -} - -// MARK: - ASAuthorizationControllerPresentationContextProviding - -extension AuthService: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = scene.windows.first(where: { $0.isKeyWindow }) else { - return UIWindow() - } - return window - } -} - -// MARK: - AuthError - -enum AuthError: LocalizedError { - case invalidCredential - case missingIdentityToken - - var errorDescription: String? { - switch self { - case .invalidCredential: - return ZXStrings.appleCredentialFailed - case .missingIdentityToken: - return ZXStrings.missingIdentityToken - } - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/AuthServiceProtocol.swift b/AIStudyApp/AIStudyApp/Core/Services/AuthServiceProtocol.swift deleted file mode 100644 index 897ae51..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/AuthServiceProtocol.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -protocol AuthServiceProtocol { - func loginWithApple() async throws -> AuthResponse - func refreshSession() async throws -> AuthResponse - func logout() async throws - func fetchCurrentUser() async throws -> User -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/FeedbackService.swift b/AIStudyApp/AIStudyApp/Core/Services/FeedbackService.swift deleted file mode 100644 index 58845ab..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/FeedbackService.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -// MARK: - Feedback Service Protocol - -protocol FeedbackServiceProtocol { - func submit(_ feedback: SubmitFeedbackRequest) async throws -> Feedback -} - -// MARK: - Request Models - -struct SubmitFeedbackRequest: Codable { - let category: String - let content: String -} - -// MARK: - Feedback Service Implementation - -final class FeedbackService: FeedbackServiceProtocol { - private let apiClient: APIClientProtocol - - init(apiClient: APIClientProtocol) { - self.apiClient = apiClient - } - - func submit(_ feedback: SubmitFeedbackRequest) async throws -> Feedback { - try await apiClient.request(.submitFeedback(feedback)) - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/KnowledgeService.swift b/AIStudyApp/AIStudyApp/Core/Services/KnowledgeService.swift deleted file mode 100644 index 5b91323..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/KnowledgeService.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -// MARK: - Knowledge Service Protocol - -protocol KnowledgeServiceProtocol { - func fetchKnowledgeBases() async throws -> [KnowledgeBase] - func fetchKnowledgeBase(id: String) async throws -> KnowledgeBase - func fetchPaths(knowledgeBaseId: String) async throws -> [LearningPath] - func fetchPath(id: String) async throws -> LearningPathDetail - func fetchLesson(id: String) async throws -> Lesson -} - -// MARK: - Response Models - -struct LearningPathDetail: Codable { - let path: LearningPath - let lessons: [Lesson] -} - -// MARK: - Knowledge Service Implementation - -final class KnowledgeService: KnowledgeServiceProtocol { - private let apiClient: APIClientProtocol - - init(apiClient: APIClientProtocol) { - self.apiClient = apiClient - } - - func fetchKnowledgeBases() async throws -> [KnowledgeBase] { - try await apiClient.request(.knowledgeBases) - } - - func fetchKnowledgeBase(id: String) async throws -> KnowledgeBase { - try await apiClient.request(.knowledgeBase(id)) - } - - func fetchPaths(knowledgeBaseId: String) async throws -> [LearningPath] { - try await apiClient.request(.paths(knowledgeBaseId)) - } - - func fetchPath(id: String) async throws -> LearningPathDetail { - try await apiClient.request(.path(id)) - } - - func fetchLesson(id: String) async throws -> Lesson { - try await apiClient.request(.lesson(id)) - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/LearningService.swift b/AIStudyApp/AIStudyApp/Core/Services/LearningService.swift deleted file mode 100644 index ae67efa..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/LearningService.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation - -// MARK: - Learning Service Protocol - -protocol LearningServiceProtocol { - func fetchSessions() async throws -> [LearningSession] - func fetchSession(id: String) async throws -> LearningSession - func createSession(request: CreateSessionRequest) async throws -> LearningSession - func updateSession(id: String, request: UpdateSessionRequest) async throws -> LearningSession - func fetchProgress() async throws -> LearningProgress -} - -// MARK: - Request / Response Models - -struct CreateSessionRequest: Codable { - let lessonId: String - let userInput: String -} - -struct UpdateSessionRequest: Codable { - let endedAt: String? - let userInput: String? - let masteryScore: Int? -} - -struct LearningProgress: Codable { - let totalSessions: Int - let completedSessions: Int - let totalMinutes: Int - let averageScore: Int - let streak: Int - let weeklyActivity: [CGFloat] -} - -// MARK: - Learning Service Implementation - -final class LearningService: LearningServiceProtocol { - private let apiClient: APIClientProtocol - - init(apiClient: APIClientProtocol) { - self.apiClient = apiClient - } - - func fetchSessions() async throws -> [LearningSession] { - try await apiClient.request(.sessions) - } - - func fetchSession(id: String) async throws -> LearningSession { - try await apiClient.request(.session(id)) - } - - func createSession(request: CreateSessionRequest) async throws -> LearningSession { - try await apiClient.request(.createSession(request)) - } - - func updateSession(id: String, request: UpdateSessionRequest) async throws -> LearningSession { - try await apiClient.request(.updateSession(id, request)) - } - - func fetchProgress() async throws -> LearningProgress { - try await apiClient.request(.progress) - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/ReviewService.swift b/AIStudyApp/AIStudyApp/Core/Services/ReviewService.swift deleted file mode 100644 index 60ecaf9..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/ReviewService.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -// MARK: - Review Service Protocol - -protocol ReviewServiceProtocol { - func fetchReviews() async throws -> [ReviewTask] - func fetchTodayReviews() async throws -> [ReviewTask] - func fetchTomorrowReviews() async throws -> [ReviewTask] - func fetchWeekReviews() async throws -> [ReviewTask] - func updateReviewStatus(id: String, status: ReviewTaskStatus) async throws -> ReviewTask - func generateReviews() async throws -> [ReviewTask] -} - -// MARK: - Request Models - -struct UpdateReviewRequest: Codable { - let status: ReviewTaskStatus -} - -// MARK: - Review Service Implementation - -final class ReviewService: ReviewServiceProtocol { - private let apiClient: APIClientProtocol - - init(apiClient: APIClientProtocol) { - self.apiClient = apiClient - } - - func fetchReviews() async throws -> [ReviewTask] { - try await apiClient.request(.reviews) - } - - func fetchTodayReviews() async throws -> [ReviewTask] { - try await apiClient.request(.reviewsToday) - } - - func fetchTomorrowReviews() async throws -> [ReviewTask] { - try await apiClient.request(.reviewsTomorrow) - } - - func fetchWeekReviews() async throws -> [ReviewTask] { - try await apiClient.request(.reviewsWeek) - } - - func updateReviewStatus(id: String, status: ReviewTaskStatus) async throws -> ReviewTask { - try await apiClient.request(.updateReview(id, UpdateReviewRequest(status: status))) - } - - func generateReviews() async throws -> [ReviewTask] { - try await apiClient.request(.generateReviews) - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/Entities/LearningRecordEntity.swift b/AIStudyApp/AIStudyApp/Core/Storage/Entities/LearningRecordEntity.swift deleted file mode 100644 index 56b5d50..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/Entities/LearningRecordEntity.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -struct LearningRecordEntity: Codable, Identifiable { - var id: UUID - var lessonTitle: String - var durationMinutes: Int - var masteryScore: Int - var weakPoints: [String] - var completedAt: Date - var createdAt: Date - - init(lessonTitle: String = "", durationMinutes: Int = 0, masteryScore: Int = 0, weakPoints: [String] = [], completedAt: Date = Date()) { - self.id = UUID() - self.lessonTitle = lessonTitle - self.durationMinutes = durationMinutes - self.masteryScore = masteryScore - self.weakPoints = weakPoints - self.completedAt = completedAt - self.createdAt = Date() - } - - static let weekActivitySeed: [(Int, Int)] = [ - (-6, 25), (-5, 55), (-4, 80), (-3, 35), (-2, 70), (-1, 45), (0, 15) - ] - - static func seedData() -> [LearningRecordEntity] { - weekActivitySeed.map { offset, minutes in - let date = Calendar.current.date(byAdding: .day, value: offset, to: Date())! - return LearningRecordEntity( - lessonTitle: "学习记录", - durationMinutes: minutes, - masteryScore: Int.random(in: 60...95), - completedAt: date - ) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/Entities/ReviewTaskEntity.swift b/AIStudyApp/AIStudyApp/Core/Storage/Entities/ReviewTaskEntity.swift deleted file mode 100644 index ccf9ddd..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/Entities/ReviewTaskEntity.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation - -struct ReviewTaskEntity: Codable, Identifiable { - var id: UUID - var lessonId: String - var reviewType: String - var scheduledAt: Date - var completedAt: Date? - var status: String - var createdAt: Date - - init(lessonId: String = "", reviewType: String = "recall", scheduledAt: Date = Date(), completedAt: Date? = nil, status: String = "pending") { - self.id = UUID() - self.lessonId = lessonId - self.reviewType = reviewType - self.scheduledAt = scheduledAt - self.completedAt = completedAt - self.status = status - self.createdAt = Date() - } - - var statusEnum: ReviewTaskEntityStatus { - get { ReviewTaskEntityStatus(rawValue: status) ?? .pending } - set { status = newValue.rawValue } - } - - var reviewTypeEnum: ReviewTaskEntityType { - get { ReviewTaskEntityType(rawValue: reviewType) ?? .recall } - set { reviewType = newValue.rawValue } - } - - var isToday: Bool { Calendar.current.isDateInToday(scheduledAt) } - var isTomorrow: Bool { Calendar.current.isDateInTomorrow(scheduledAt) } - var isThisWeek: Bool { - guard let weekLater = Calendar.current.date(byAdding: .day, value: 7, to: Date()) else { return false } - return scheduledAt > Date() && scheduledAt <= weekLater - } - - static func seedData() -> [ReviewTaskEntity] { - let today = Date() - let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)! - let day3 = Calendar.current.date(byAdding: .day, value: 3, to: today)! - let day5 = Calendar.current.date(byAdding: .day, value: 5, to: today)! - - let items: [(String, String, Date)] = [ - ("注意力机制核心概念", "recall", today), - ("Transformer 结构费曼解释", "feynman", today), - ("反向传播数学推导", "spacedRepetition", tomorrow), - ("CNN vs RNN 对比", "recall", tomorrow), - ("损失函数选择指南", "weakPoint", day3), - ("优化器对比总结", "spacedRepetition", day5), - ] - return items.map { lessonId, type, date in - ReviewTaskEntity(lessonId: lessonId, reviewType: type, scheduledAt: date) - } - } -} - -enum ReviewTaskEntityStatus: String, CaseIterable, Codable { - case pending, completed, skipped, overdue -} - -enum ReviewTaskEntityType: String, CaseIterable, Codable { - case spacedRepetition, feynman, recall, weakPoint -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/Entities/StudyTaskEntity.swift b/AIStudyApp/AIStudyApp/Core/Storage/Entities/StudyTaskEntity.swift deleted file mode 100644 index 0707303..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/Entities/StudyTaskEntity.swift +++ /dev/null @@ -1,47 +0,0 @@ -import SwiftUI - -struct StudyTaskEntity: Codable, Identifiable { - var id: UUID - var title: String - var taskType: String - var colorName: String - var minutes: Int - var isDone: Bool - var createdAt: Date - var sortOrder: Int - - init(title: String, taskType: String, colorName: String, minutes: Int, isDone: Bool = false, sortOrder: Int = 0) { - self.id = UUID() - self.title = title - self.taskType = taskType - self.colorName = colorName - self.minutes = minutes - self.isDone = isDone - self.createdAt = Date() - self.sortOrder = sortOrder - } - - var color: Color { - switch colorName { - case "purple": return .zxPurple - case "orange": return .zxOrange - case "teal": return .zxTeal - case "accent": return .zxAccent - case "yellow": return .zxYellow - default: return .zxAccent - } - } - - static func seedData() -> [StudyTaskEntity] { - let items: [(String, String, String, Int, Int)] = [ - ("机器学习 - 回忆测试", "回忆测试", "purple", 10, 0), - ("高数 - 间隔复习 8 题", "间隔复习", "orange", 15, 1), - ("英语词汇 - 25 个待复习", "词汇复习", "teal", 8, 2), - ("注意力机制 - 费曼解释", "费曼练习", "accent", 12, 3), - ("产品设计 - 薄弱点复习", "薄弱点", "yellow", 10, 4), - ] - return items.map { title, type, color, min, order in - StudyTaskEntity(title: title, taskType: type, colorName: color, minutes: min, sortOrder: order) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/KeychainStore.swift b/AIStudyApp/AIStudyApp/Core/Storage/KeychainStore.swift deleted file mode 100644 index de0baad..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/KeychainStore.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation -import Security - -enum KeychainError: Error { - case saveFailed(OSStatus) - case loadFailed(OSStatus) - case deleteFailed(OSStatus) - case itemNotFound - case invalidData -} - -final class KeychainStore { - private let service: String - - init(service: String = Bundle.main.bundleIdentifier ?? "com.zx.keystore") { - self.service = service - } - - func save(key: String, data: Data) throws { - try delete(key: key) - - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key, - kSecValueData as String: data, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - ] - - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecSuccess else { - throw KeychainError.saveFailed(status) - } - } - - func load(key: String) throws -> Data? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key, - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne, - ] - - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) - - switch status { - case errSecSuccess: - guard let data = result as? Data else { - throw KeychainError.invalidData - } - return data - case errSecItemNotFound: - return nil - default: - throw KeychainError.loadFailed(status) - } - } - - func delete(key: String) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key, - ] - - let status = SecItemDelete(query as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - throw KeychainError.deleteFailed(status) - } - } - - func clearAll(keys: [String]) throws { - for key in keys { - try? delete(key: key) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/PersistenceController.swift b/AIStudyApp/AIStudyApp/Core/Storage/PersistenceController.swift deleted file mode 100644 index 9a3ba1e..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/PersistenceController.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -@MainActor -final class PersistenceController { - static let shared = PersistenceController() - - private let cache = FileCache(suite: "app_persistence") - private var seeded = false - - private init() {} - - // MARK: - Study Tasks - - func loadTasks() -> [StudyTaskEntity] { - if !seeded { seedIfNeeded() } - return (try? cache.load([StudyTaskEntity].self, forKey: "study_tasks")) ?? [] - } - - func saveTasks(_ tasks: [StudyTaskEntity]) { - try? cache.save(tasks, forKey: "study_tasks") - } - - // MARK: - Review Tasks - - func loadReviewTasks() -> [ReviewTaskEntity] { - if !seeded { seedIfNeeded() } - return (try? cache.load([ReviewTaskEntity].self, forKey: "review_tasks")) ?? [] - } - - func saveReviewTasks(_ tasks: [ReviewTaskEntity]) { - try? cache.save(tasks, forKey: "review_tasks") - } - - // MARK: - Learning Records - - func loadRecords() -> [LearningRecordEntity] { - if !seeded { seedIfNeeded() } - return (try? cache.load([LearningRecordEntity].self, forKey: "learning_records")) ?? [] - } - - func saveRecords(_ records: [LearningRecordEntity]) { - try? cache.save(records, forKey: "learning_records") - } - - // MARK: - Seed - - func seedIfNeeded() { - guard !seeded else { return } - seeded = true - if (try? cache.load([StudyTaskEntity].self, forKey: "study_tasks")) == nil { - try? cache.save(StudyTaskEntity.seedData(), forKey: "study_tasks") - } - if (try? cache.load([ReviewTaskEntity].self, forKey: "review_tasks")) == nil { - try? cache.save(ReviewTaskEntity.seedData(), forKey: "review_tasks") - } - if (try? cache.load([LearningRecordEntity].self, forKey: "learning_records")) == nil { - try? cache.save(LearningRecordEntity.seedData(), forKey: "learning_records") - } - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Storage/TokenStore.swift b/AIStudyApp/AIStudyApp/Core/Storage/TokenStore.swift deleted file mode 100644 index 097ae5c..0000000 --- a/AIStudyApp/AIStudyApp/Core/Storage/TokenStore.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation - -protocol TokenStoreProtocol { - func saveAccessToken(_ token: String) throws - func getAccessToken() throws -> String? - func saveRefreshToken(_ token: String) throws - func getRefreshToken() throws -> String? - func clearAll() throws -} - -final class TokenStore: TokenStoreProtocol { - private let keychain: KeychainStore - private let accessTokenKey = "zx.accessToken" - private let refreshTokenKey = "zx.refreshToken" - - init(keychain: KeychainStore = KeychainStore()) { - self.keychain = keychain - } - - func saveAccessToken(_ token: String) throws { - guard let data = token.data(using: .utf8) else { - throw KeychainError.invalidData - } - try keychain.save(key: accessTokenKey, data: data) - } - - func getAccessToken() throws -> String? { - guard let data = try keychain.load(key: accessTokenKey) else { - return nil - } - return String(data: data, encoding: .utf8) - } - - func saveRefreshToken(_ token: String) throws { - guard let data = token.data(using: .utf8) else { - throw KeychainError.invalidData - } - try keychain.save(key: refreshTokenKey, data: data) - } - - func getRefreshToken() throws -> String? { - guard let data = try keychain.load(key: refreshTokenKey) else { - return nil - } - return String(data: data, encoding: .utf8) - } - - func clearAll() throws { - try keychain.clearAll(keys: [accessTokenKey, refreshTokenKey]) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift deleted file mode 100644 index 5b8dbf8..0000000 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift +++ /dev/null @@ -1,36 +0,0 @@ -import SwiftUI - -// MARK: - AI Chat Page - -struct AIChatPage: View { - @StateObject private var vm = AIChatViewModel() - - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "AI 对话", subtitle: "学习助手") {} - ScrollViewReader { proxy in ScrollView { VStack(spacing: 16) { - ForEach(vm.messages) { m in - HStack(alignment: .top, spacing: 8) { - if m.role == .ai { - Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple).frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle()) - } - Text(m.content).font(.system(size: 14)).foregroundColor(m.role == .user ? .white : Color.zxF007).padding(12).background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004)).clipShape(RoundedRectangle(cornerRadius: 16)) - if m.role == .user { Circle().frame(width: 28, height: 28).foregroundColor(Color.zxPurpleBG(0.2)).overlay(Text("我").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple)) } - } - .frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading) - } - if vm.isSending { - HStack { - Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple).frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle()) - ZXTypingIndicator() - Spacer() - } - } - }.padding(.horizontal, 20).padding(.bottom, 100).id("bottom") }.scrollIndicators(.hidden) - .onChange(of: vm.messages.count) { withAnimation { proxy.scrollTo("bottom") } } } - ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() }) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPage.swift deleted file mode 100644 index ab4cf53..0000000 --- a/AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPage.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -// MARK: - AI Feedback Page - -struct AIFeedbackPageView: View { - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "AI 反馈", subtitle: "今日思考 · 过拟合", trailing: { - ZXIconBtn(icon: "bookmark", size: 36) {} - }) - 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.zxF007).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)) - } - } - - VStack(alignment: .leading, spacing: 10) { - HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill").foregroundColor(Color.zxYellow); Text("需要完善").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0) } - ForEach([("缺少对「方差」和「偏差」权衡的说明", "过拟合本质是高方差问题,可以提到偏差-方差权衡"),("未提及正则化、Dropout 等解决方案", "完整答案通常要说明\"如何解决\"")], id: \.0) { p, d in - VStack(alignment: .leading, spacing: 4) { Text(p).font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text(d).font(.system(size: 12)).foregroundColor(Color.zxF05).lineSpacing(4) } - .padding(14).frame(maxWidth: .infinity, alignment: .leading).background(Color(hex: "#F59E0B", opacity: 0.07)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "#F59E0B", opacity: 0.18), lineWidth: 1)) - } - } - - VStack(alignment: .leading, spacing: 8) { - Text("✨ 参考答案要点").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxAccent) - Text("过拟合是模型复杂度过高导致的高方差问题。偏差-方差权衡是核心概念。解决方法包括:正则化、Dropout、数据增强、早停等。").font(.system(size: 13)).foregroundColor(Color.zxF007).lineSpacing(6) - }.padding(16).background(Color(hex: "#7C6EFA", opacity: 0.07)).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(hex: "#7C6EFA", opacity: 0.2), 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(.bottom, 80) }.scrollIndicators(.hidden) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift index 0e90391..676f52d 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIHomeView.swift @@ -1,11 +1,15 @@ // -// AIHomeView.swift - Page 6: AI Home +// AIHomeView.swift - Page 6: AI Home + API status indicator // import SwiftUI struct AIHomeView: View { @State private var text = "" + @State private var serverStatus: ServerStatus = .checking + @State private var knowledgeCount = 0 + + enum ServerStatus { case checking, online, offline } var body: some View { ZStack { @@ -14,14 +18,32 @@ struct AIHomeView: View { .frame(width:200,height:200).offset(x:80,y:-80).allowsHitTesting(false) VStack(spacing:0){ - // Header HStack(alignment:.bottom){ VStack(alignment:.leading,spacing:2){ Text("今天").font(.system(size:12,weight:.medium)).foregroundColor(Color.zxF04) Text("AI 学习助手").font(.system(size:20,weight:.heavy)).foregroundColor(Color.zxF0).tracking(-0.4) } Spacer() - ZXIconBtn(icon:"arrow.clockwise",size:36){} + + // API 状态指示器 + HStack(spacing: 4) { + Circle() + .fill(serverStatus == .online ? Color.zxGreen + : serverStatus == .checking ? Color.zxYellow + : Color.zxRed) + .frame(width: 6, height: 6) + Text(serverStatus == .online ? "API \(knowledgeCount)" + : serverStatus == .checking ? "检测中…" + : "离线") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(serverStatus == .online ? Color.zxGreen + : serverStatus == .checking ? Color.zxYellow + : Color.zxF03) + } + .padding(.horizontal, 8).padding(.vertical, 4) + .background(Color.zxFill005).clipShape(Capsule()) + + ZXIconBtn(icon:"arrow.clockwise",size:36){ Task { await checkServer() } } } .padding(.horizontal,20).padding(.top,ZXSpacing.statusBarH+16).padding(.bottom,12) @@ -39,6 +61,20 @@ struct AIHomeView: View { inputBar } } + .task { await checkServer() } + } + + private func checkServer() async { + serverStatus = .checking + do { + let resp: KnowledgeBaseListResponse = try await APIClient.shared.request("/knowledge-bases") + let count = resp.data?.count ?? 0 + knowledgeCount = count + serverStatus = .online + } catch { + serverStatus = .offline + print("[API] 服务器检测失败: \(error.localizedDescription)") + } } private var thinkingCard: some View { @@ -127,4 +163,48 @@ struct AIHomeView: View { } } -// ZXQuickAction, ZXAIInteractionRow → Shared/Components/ +struct ZXQuickAction: View { + let emoji: String + let label: String + + var body: some View { + VStack(spacing:6){ + Text(emoji).font(.system(size:22)) + Text(label).font(.system(size:10,weight:.medium)).foregroundColor(Color.zxF03) + .multilineTextAlignment(.center).lineSpacing(2) + } + .frame(width:72,height:72) + .background(Color.zxFill005).clipShape(RoundedRectangle(cornerRadius:16)) + } +} + +struct ZXAIInteractionRow: View { + let tag: String + let bg: Color + let fg: Color + let emoji: String + let title: String + let time: String + let score: Int + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing:12){ + Text(emoji).font(.system(size:16)) + .frame(width:36,height:36).background(bg).clipShape(RoundedRectangle(cornerRadius:10)) + VStack(alignment:.leading,spacing:4){ + HStack{ + Text(tag).font(.system(size:10,weight:.bold)).foregroundColor(fg) + Text(time).font(.system(size:10)).foregroundColor(Color.zxF04) + } + Text(title).font(.system(size:13,weight:.medium)).foregroundColor(Color.zxF0) + }.frame(maxWidth:.infinity,alignment:.leading) + Text("\(score)").font(.system(size:12,weight:.heavy)).foregroundColor(Color.zxYellow) + .frame(width:28,height:28).background(Color.zxYellowBG(0.1)).clipShape(RoundedRectangle(cornerRadius:8)) + } + .padding(.horizontal,14).padding(.vertical,12) + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)) + } + } +} diff --git a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift index 50a0064..f16a519 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/DailyThinkingPage.swift @@ -1,52 +1,26 @@ -// -// DailyThinkingPage.swift - Page 14: Daily Thinking -// - import SwiftUI struct DailyThinkingPage: View { - @State private var answer = "" - @State private var submitted = false - + @State private var answer = ""; @State private var submitted = false var body: some View { ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "今日思考", subtitle: "过拟合", onBack: nil, trailing: { - ZXIconBtn(icon: "bookmark", size: 36) {} - }) - ScrollView { VStack(spacing: 16) { - VStack(alignment: .leading, spacing: 12) { - HStack { - Image(systemName: "sparkles").foregroundColor(Color.zxAccent) - Text("解释\"注意力机制\"在 Transformer 中的作用").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) - } - Text("AI会从三个方面评估你的回答:核心概念理解 · 理论深度 · 实际应用能力").font(.system(size: 12)).foregroundColor(Color.zxF04) - } - .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(.bottom, 120) } - .scrollIndicators(.hidden) - } - } - .navigationBarHidden(true) + ScrollView { VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 12) { + HStack { Image(systemName:"sparkles").foregroundColor(Color.zxAccent); Text("解释\"注意力机制\"在 Transformer 中的作用").font(.system(size:15,weight:.bold)).foregroundColor(Color.zxF0) } + Text("AI会从三个方面评估你的回答:核心概念理解 · 理论深度 · 实际应用能力").font(.system(size:12)).foregroundColor(Color.zxF04) + }.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) + }.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar) } } - -// Extracted pages: RecallTestPage, WeakPointsPage, AIFeedbackPageView, AIChatPage -// Shared components: ZXBackHeader, ZXOutlineBtn → Shared/Components/ +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)} } diff --git a/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift b/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift deleted file mode 100644 index 57dfef9..0000000 --- a/AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -// MARK: - Recall Test Page - -struct RecallTestPage: View { - @State private var input = "" - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "回忆测试", subtitle: "机器学习 · 偏差-方差权衡") {} - 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(.bottom, 80) }.scrollIndicators(.hidden) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/ViewModels/AIChatViewModel.swift b/AIStudyApp/AIStudyApp/Features/AI/ViewModels/AIChatViewModel.swift deleted file mode 100644 index 7996b63..0000000 --- a/AIStudyApp/AIStudyApp/Features/AI/ViewModels/AIChatViewModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -import SwiftUI -import Combine - -struct ChatMessage: Identifiable { - let id = UUID() - let role: ChatRole - let content: String -} - -enum ChatRole { - case user, ai -} - -@MainActor -final class AIChatViewModel: ObservableObject { - @Published var messages: [ChatMessage] = [ - ChatMessage(role: .ai, content: "你好!我是你的 AI 学习助手。我可以帮你解答学习问题、分析薄弱点、制定复习计划。请告诉我你想学习什么?") - ] - @Published var inputText = "" - @Published var isSending = false - - var canSend: Bool { !inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending } - - func send() { - let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines) - guard !text.isEmpty, !isSending else { return } - messages.append(ChatMessage(role: .user, content: text)) - inputText = "" - isSending = true - Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - messages.append(ChatMessage(role: .ai, content: "好的,我理解你的问题。建议你从基础概念开始,逐步深入理解。需要我帮你制定具体的学习计划吗?")) - isSending = false - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift b/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift deleted file mode 100644 index 9df47cb..0000000 --- a/AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -// MARK: - Weak Points Page - -struct WeakPointsPage: View { - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "薄弱知识点", subtitle: "共 23 个待巩固") {} - 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(.bottom, 80) }.scrollIndicators(.hidden) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index b267461..711fb70 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -1,7 +1,3 @@ -// -// AnalysisHomeView.swift - Page 9: Analysis Home -// - import SwiftUI struct AnalysisHomeView: View { @@ -10,20 +6,12 @@ struct AnalysisHomeView: View { Color.zxBg0.ignoresSafeArea() VStack(spacing: 0) { HStack { - Text("学习分析") - .font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5) + Text("学习分析").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5) Spacer() - HStack(spacing: 4) { - Text("近 7 天").font(.system(size: 12)).foregroundColor(Color.zxF05) - Image(systemName: "chevron.down").font(.system(size: 10)).foregroundColor(Color.zxF04) - } - .padding(.horizontal, 12).padding(.vertical, 6) - .background(Color.zxFill005) - .clipShape(Capsule()) - .overlay(Capsule().stroke(Color.zxBorder008, lineWidth: 1)) + HStack(spacing: 4) { Text("近 7 天").font(.system(size: 12)).foregroundColor(Color.zxF05); Image(systemName: "chevron.down").font(.system(size: 10)).foregroundColor(Color.zxF04) } + .padding(.horizontal, 12).padding(.vertical, 6).background(Color.zxFill005).clipShape(Capsule()).overlay(Capsule().stroke(Color.zxBorder008, lineWidth: 1)) } .padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 12) - ScrollView { VStack(spacing: 16) { HStack(spacing: 12) { @@ -32,41 +20,46 @@ struct AnalysisHomeView: View { ZXStatBadge(icon: "exclamationmark.triangle.fill", label: "待巩固", value: "23", trend: "-5", color: Color.zxYellow) ZXStatBadge(icon: "chart.line.uptrend.xyaxis", label: "连续天", value: "14", trend: "🔥", color: Color.zxGreen) } - VStack(alignment: .leading, spacing: 16) { - HStack { - Text("掌握度趋势").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0) - Spacer() - Text("↑ +8% 本周").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxGreen) - } + HStack { Text("掌握度趋势").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0); Spacer(); Text("↑ +8% 本周").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxGreen) } ZXChartView() - } - .padding(16) - .background(Color.zxFill004) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 20)) - + }.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) - } + 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: "高") ZXWeakRow(score: 55, topic: "词根 spect- 相关词汇", lib: "英语词汇", priority: "中") } - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) + }.padding(.horizontal, 20).padding(.bottom, 120) + }.scrollIndicators(.hidden) } } } } -// ZXStatBadge, ZXWeakRow, ZXChartView → Shared/Components/ +struct ZXStatBadge: View { let icon: String; let label: String; let value: String; let trend: String; let color: Color + var body: some View { + VStack(spacing: 3) { + Image(systemName: icon).font(.system(size: 14)).foregroundColor(color) + Text(value).font(.system(size: 16, weight: .heavy)).foregroundColor(Color.zxF0) + Text(label).font(.system(size: 9)).foregroundColor(Color.zxF04).multilineTextAlignment(.center) + }.frame(maxWidth: .infinity).frame(height: 72).padding(.vertical, 4).background(color.opacity(0.06)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)) + } +} + +struct ZXChartView: View { + let data: [(String, CGFloat)] = [("一", 0.62), ("二", 0.65), ("三", 0.71), ("四", 0.68), ("五", 0.75), ("六", 0.79), ("今", 0.78)] + var body: some View { + VStack(spacing: 0) { + GeometryReader { g in + ZStack(alignment: .topLeading) { + Path { path in let w = g.size.width / 7 + for (i, d) in data.enumerated() { let x = w * CGFloat(i) + w / 2; let y = (1 - d.1) * g.size.height + if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } } + }.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) + } + }.frame(height: 100) + HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } } + } + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/LoginViewModel.swift b/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/LoginViewModel.swift deleted file mode 100644 index 023a986..0000000 --- a/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/LoginViewModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI -import Combine - -@MainActor -final class LoginViewModel: ObservableObject { - @Published var isLoading = false - @Published var errorMessage: String? - - let appSession: AppSession - - init(appSession: AppSession) { - self.appSession = appSession - } - - func loginWithApple() async { - isLoading = true - errorMessage = nil - await appSession.loginWithApple() - if let error = appSession.authError { - errorMessage = error - } - isLoading = false - } - - var isAuthenticated: Bool { - appSession.isAuthenticated - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/WaitlistViewModel.swift b/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/WaitlistViewModel.swift deleted file mode 100644 index c5fad25..0000000 --- a/AIStudyApp/AIStudyApp/Features/Auth/ViewModels/WaitlistViewModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI -import Combine - -@MainActor -final class WaitlistViewModel: ObservableObject { - @Published var email = "" - @Published var name = "" - @Published var isSubmitting = false - @Published var showSuccess = false - @Published var errorMessage: String? - - var canSubmit: Bool { - !email.trimmingCharacters(in: .whitespaces).isEmpty && !isSubmitting - } - - func submit() async { - guard canSubmit else { return } - isSubmitting = true - errorMessage = nil - - let entry = WaitlistEntry( - email: email.trimmingCharacters(in: .whitespaces), - name: name.trimmingCharacters(in: .whitespaces).isEmpty ? nil : name.trimmingCharacters(in: .whitespaces) - ) - - // TODO: Replace with actual API call when backend is ready - // let _: EmptyResponse = try await apiClient.request(.waitlist(entry)) - try? await Task.sleep(nanoseconds: 1_200_000_000) - - showSuccess = true - isSubmitting = false - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Auth/Views/LoginView.swift b/AIStudyApp/AIStudyApp/Features/Auth/Views/LoginView.swift deleted file mode 100644 index e150b86..0000000 --- a/AIStudyApp/AIStudyApp/Features/Auth/Views/LoginView.swift +++ /dev/null @@ -1,136 +0,0 @@ -import SwiftUI - -struct LoginView: View { - @StateObject private var viewModel: LoginViewModel - let onLoginSuccess: (Bool) -> Void - - init(appSession: AppSession, onLoginSuccess: @escaping (Bool) -> Void) { - _viewModel = StateObject(wrappedValue: LoginViewModel(appSession: appSession)) - self.onLoginSuccess = onLoginSuccess - } - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - - Circle() - .fill(RadialGradient( - colors: [Color(hex: "#7C6EFA", opacity: 0.08), .clear], - center: .top, - startRadius: 0, - endRadius: 200 - )) - .frame(width: 200, height: 200) - .offset(y: -80) - .allowsHitTesting(false) - - VStack(spacing: 0) { - Spacer() - - VStack(spacing: 24) { - // Logo - RoundedRectangle(cornerRadius: 28) - .fill(LinearGradient( - colors: [Color(hex: "#7C6EFA"), Color(hex: "#A78BFA"), Color(hex: "#F97316")], - startPoint: .topLeading, - endPoint: .bottomTrailing - )) - .frame(width: 80, height: 80) - .overlay( - Image(systemName: "brain.head.profile") - .font(.system(size: 36)) - .foregroundColor(.white.opacity(0.8)) - ) - .shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 32) - - // Brand - VStack(spacing: 8) { - Text("知习") - .font(.system(size: 32, weight: .heavy)) - .tracking(-1) - .foregroundStyle( - LinearGradient( - colors: [Color(hex: "#A78BFA"), Color(hex: "#F0F0FF"), Color(hex: "#F97316")], - startPoint: .leading, - endPoint: .trailing - ) - ) - Text("更懂你,更会学。") - .font(.system(size: 14)) - .foregroundColor(Color.zxF05) - } - - // Value prop - Text("用 AI 把知识库、主动回忆和间隔复习连接起来,\n从\"看过\"走向\"真正学会\"。") - .font(.system(size: 13)) - .foregroundColor(Color.zxF04) - .multilineTextAlignment(.center) - .lineSpacing(4) - .padding(.horizontal, 20) - - // Error - if let error = viewModel.errorMessage { - Text(error) - .font(.system(size: 12)) - .foregroundColor(Color.zxRed) - .padding(.horizontal, 20) - .multilineTextAlignment(.center) - } - - // Apple Sign In Button - Button { - Task { - await viewModel.loginWithApple() - if viewModel.isAuthenticated { - let needsOnboarding = viewModel.appSession.needsOnboarding - onLoginSuccess(needsOnboarding) - } - } - } label: { - HStack(spacing: 10) { - Image(systemName: "apple.logo") - .font(.system(size: 20)) - Text("使用 Apple 继续") - .font(.system(size: 16, weight: .semibold)) - } - .foregroundColor(.black) - .frame(maxWidth: .infinity) - .frame(height: 52) - .background(Color.white) - .clipShape(RoundedRectangle(cornerRadius: 18)) - } - .disabled(viewModel.isLoading) - .padding(.horizontal, 20) - - // Loading - if viewModel.isLoading { - ProgressView() - .tint(Color.zxF05) - } - - #if DEBUG - Button { - onLoginSuccess(true) - } label: { - Text("跳过,进入演示模式") - .font(.system(size: 13)) - .foregroundColor(Color.zxF035) - } - .padding(.top, 8) - #endif - } - - Spacer() - - // Footer - VStack(spacing: 8) { - Text("登录即代表你同意《用户服务协议》和《隐私政策》") - .font(.system(size: 11)) - .foregroundColor(Color.zxF02) - } - .padding(.bottom, 40) - } - } - } -} - diff --git a/AIStudyApp/AIStudyApp/Features/Auth/Views/WaitlistView.swift b/AIStudyApp/AIStudyApp/Features/Auth/Views/WaitlistView.swift deleted file mode 100644 index efd4dbc..0000000 --- a/AIStudyApp/AIStudyApp/Features/Auth/Views/WaitlistView.swift +++ /dev/null @@ -1,134 +0,0 @@ -import SwiftUI - -struct WaitlistView: View { - @StateObject private var vm = WaitlistViewModel() - @Environment(\.dismiss) private var dismiss - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - - ScrollView { - VStack(spacing: 28) { - headerSection - if vm.showSuccess { - successSection - } else { - formSection - } - } - .padding(.horizontal, 20) - .padding(.bottom, 60) - } - .scrollIndicators(.hidden) - } - } - - // MARK: - Header - - private var headerSection: some View { - VStack(spacing: 12) { - Spacer().frame(height: ZXSpacing.statusBarH + 40) - Image(systemName: "envelope.badge.person.crop") - .font(.system(size: 48)) - .foregroundColor(Color.zxPurple) - Text("加入内测名单") - .font(.system(size: 24, weight: .heavy)) - .foregroundColor(Color.zxF0) - .tracking(-0.5) - Text("知习正在邀请首批用户参与内测。\n留下联系方式,我们会在开放后第一时间通知你。") - .font(.system(size: 14)) - .foregroundColor(Color.zxF04) - .multilineTextAlignment(.center) - .lineSpacing(4) - } - } - - // MARK: - Form - - private var formSection: some View { - VStack(spacing: 16) { - VStack(alignment: .leading, spacing: 6) { - Text("邮箱").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - TextField("your@email.com", text: $vm.email) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - .autocapitalization(.none) - .font(.system(size: 15)) - .foregroundColor(Color.zxF0) - .padding(14) - .background(Color.zxFill005) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxBorder008, lineWidth: 1)) - } - - VStack(alignment: .leading, spacing: 6) { - Text("称呼(选填)").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - TextField("怎么称呼你", text: $vm.name) - .textContentType(.name) - .font(.system(size: 15)) - .foregroundColor(Color.zxF0) - .padding(14) - .background(Color.zxFill005) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxBorder008, lineWidth: 1)) - } - - if let error = vm.errorMessage { - Text(error) - .font(.system(size: 12)) - .foregroundColor(.red) - .padding(.top, 4) - } - - Button { - Task { await vm.submit() } - } label: { - HStack(spacing: 8) { - if vm.isSubmitting { - ProgressView().tint(.white) - } - Text(vm.isSubmitting ? "提交中…" : "加入名单") - .font(.system(size: 16, weight: .bold)) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 52) - .background(vm.canSubmit ? ZXGradient.ctaButton : LinearGradient(colors: [Color.zxFill006, Color.zxFill006], startPoint: .leading, endPoint: .trailing)) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - .disabled(!vm.canSubmit) - } - } - - // MARK: - Success - - private var successSection: some View { - VStack(spacing: 20) { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 64)) - .foregroundColor(Color.zxGreen) - Text("已加入名单") - .font(.system(size: 20, weight: .bold)) - .foregroundColor(Color.zxF0) - Text("我们会通过邮件通知你最新进展。\n感谢你的关注!") - .font(.system(size: 14)) - .foregroundColor(Color.zxF04) - .multilineTextAlignment(.center) - .lineSpacing(4) - - Button { - dismiss() - } label: { - Text("返回") - .font(.system(size: 16, weight: .bold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 52) - .background(ZXGradient.ctaButton) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - .padding(.top, 12) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackView.swift b/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackView.swift deleted file mode 100644 index 31b1726..0000000 --- a/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackView.swift +++ /dev/null @@ -1,138 +0,0 @@ -import SwiftUI - -// MARK: - Feedback Page - -struct FeedbackView: View { - @StateObject private var vm = FeedbackViewModel() - @Environment(\.dismiss) private var dismiss - - var body: some View { - ZStack { - Color.zxBg0.ignoresSafeArea() - - VStack(spacing: 0) { - ZXBackHeader(title: "反馈", subtitle: nil) {} - - ScrollView { - VStack(spacing: 20) { - // 分类选择 - VStack(alignment: .leading, spacing: 8) { - Text("反馈类型") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.zxF035) - - HStack(spacing: 8) { - ForEach(FeedbackCategory.allCases) { cat in - let sel = vm.selectedCategory == cat - Button { - vm.selectedCategory = cat - } label: { - VStack(spacing: 4) { - Image(systemName: cat.icon) - .font(.system(size: 16)) - Text(cat.rawValue) - .font(.system(size: 11, weight: sel ? .semibold : .regular)) - } - .foregroundColor(sel ? Color.zxPurple : Color.zxF05) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background(sel ? Color.zxPurpleBG(0.12) : Color.zxFill003) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(sel ? Color.zxPurple.opacity(0.3) : Color.zxBorder006, lineWidth: 1) - ) - } - } - } - } - - // 内容输入 - VStack(alignment: .leading, spacing: 8) { - Text("详细描述") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.zxF035) - - ZStack(alignment: .topLeading) { - if vm.content.isEmpty { - Text("请描述你遇到的问题或建议…") - .font(.system(size: 14)) - .foregroundColor(Color.zxF03) - .padding(.horizontal, 14) - .padding(.vertical, 14) - } - TextEditor(text: $vm.content) - .font(.system(size: 14)) - .foregroundColor(Color.zxF0) - .tint(Color.zxPurple) - .frame(minHeight: 160) - .scrollContentBackground(.hidden) - .padding(10) - } - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(Color.zxBorder008, lineWidth: 1) - ) - } - - // 错误提示 - if let error = vm.errorMessage { - ZXErrorBanner(message: error) { - vm.errorMessage = nil - } - } - - // 提交按钮 - Button { - vm.submit() - } label: { - HStack(spacing: 8) { - if vm.isSubmitting { - ProgressView() - .tint(.white) - } - Text(vm.isSubmitting ? "提交中…" : "提交反馈") - .font(.system(size: 14, weight: .bold)) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 52) - .background( - vm.isValid && !vm.isSubmitting - ? AnyView(ZXGradient.ctaPurple) - : AnyView(Color.zxFill005) - ) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - .disabled(!vm.isValid || vm.isSubmitting) - } - .padding(.horizontal, 20) - .padding(.top, 20) - .padding(.bottom, 80) - } - .scrollIndicators(.hidden) - } - } - .navigationBarHidden(true) - .alert("反馈已提交", isPresented: $vm.showSuccess) { - Button("好的") { dismiss() } - } message: { - Text("感谢你的反馈,我们会尽快处理。") - } - } -} - -// MARK: - Category Icon - -private extension FeedbackCategory { - var icon: String { - switch self { - case .bug: return "ladybug.fill" - case .feature: return "lightbulb.fill" - case .content: return "text.badge.checkmark" - case .other: return "ellipsis.bubble.fill" - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackViewModel.swift b/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackViewModel.swift deleted file mode 100644 index 751d52d..0000000 --- a/AIStudyApp/AIStudyApp/Features/Feedback/FeedbackViewModel.swift +++ /dev/null @@ -1,48 +0,0 @@ -import SwiftUI -import Combine - -// MARK: - Feedback View Model - -@MainActor -final class FeedbackViewModel: ObservableObject { - @Published var selectedCategory: FeedbackCategory = .feature - @Published var content = "" - @Published var isSubmitting = false - @Published var errorMessage: String? - @Published var showSuccess = false - - private let feedbackService: FeedbackServiceProtocol? - - init(feedbackService: FeedbackServiceProtocol? = nil) { - self.feedbackService = feedbackService - } - - var isValid: Bool { - !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } - - func submit() { - guard isValid else { return } - isSubmitting = true - errorMessage = nil - - Task { - do { - if let service = feedbackService { - _ = try await service.submit(SubmitFeedbackRequest( - category: selectedCategory.rawValue, - content: content - )) - } else { - // 未注入 service 时模拟提交延迟 - try await Task.sleep(nanoseconds: 800_000_000) - } - isSubmitting = false - showSuccess = true - } catch { - isSubmitting = false - errorMessage = error.localizedDescription - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/AddKnowledgePage.swift b/AIStudyApp/AIStudyApp/Features/Library/AddKnowledgePage.swift deleted file mode 100644 index 0ca7ab2..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/AddKnowledgePage.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -// MARK: - Add Knowledge Page - -struct AddKnowledgePage: View { - @State private var title = ""; @State private var content = "" - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "添加知识点", subtitle: "机器学习") {} - 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) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/CreateLibraryPage.swift b/AIStudyApp/AIStudyApp/Features/Library/CreateLibraryPage.swift deleted file mode 100644 index bcec91f..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/CreateLibraryPage.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -// MARK: - Create Library Page - -struct CreateLibraryPage: View { - @State private var name = ""; @State private var desc = "" - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "创建知识库", subtitle: nil) {} - 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)) } - }.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/EditKnowledgePage.swift b/AIStudyApp/AIStudyApp/Features/Library/EditKnowledgePage.swift deleted file mode 100644 index 84defe7..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/EditKnowledgePage.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -// MARK: - Edit Knowledge Page - -struct EditKnowledgePage: View { - @State private var title = "偏差-方差权衡"; @State private var content = "偏差衡量模型的预测与真实值之间的差异..." - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "编辑知识点", subtitle: nil) {} - 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) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/ImportPage.swift b/AIStudyApp/AIStudyApp/Features/Library/ImportPage.swift deleted file mode 100644 index e2b7fe3..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/ImportPage.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -// MARK: - Import Page - -struct ImportPage: View { - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "导入资料", subtitle: nil) {} - ScrollView { VStack(spacing: 12) { - ZXImportOption(icon: "camera.fill", title: "拍照导入", desc: "拍下书本或笔记,AI 自动识别") - 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) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/KnowledgeDetailPage.swift b/AIStudyApp/AIStudyApp/Features/Library/KnowledgeDetailPage.swift deleted file mode 100644 index 008f667..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/KnowledgeDetailPage.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -// MARK: - Knowledge Detail Page - -struct KnowledgeDetailPage: View { - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "知识点详情", subtitle: "机器学习") { ZXIconBtn(icon: "pencil", size: 36) {} } - 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)) } } - }.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) - } - }.navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibraryDetailPage.swift b/AIStudyApp/AIStudyApp/Features/Library/LibraryDetailPage.swift deleted file mode 100644 index 89b5ea0..0000000 --- a/AIStudyApp/AIStudyApp/Features/Library/LibraryDetailPage.swift +++ /dev/null @@ -1,21 +0,0 @@ -import SwiftUI - -// MARK: - Library Detail Page - -struct LibraryDetailPage: View { - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea() - VStack(spacing: 0) { - ZXBackHeader(title: "机器学习", subtitle: "47 个知识点 · 掌握 72%") { - HStack(spacing: 8) { ZXIconBtn(icon: "magnifyingglass", size: 36) {}; ZXIconBtn(icon: "plus", size: 36, branded: true) {} } - } - 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) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift new file mode 100644 index 0000000..dea71b1 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -0,0 +1,81 @@ +import SwiftUI + +struct CreateLibraryPage: View { + @State private var name = ""; @State private var desc = "" + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + 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)) } + }.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) } + }.navigationBarHidden(true)} +} + +struct LibraryDetailPage: View { + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + 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)} +} +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()) } + .padding(14).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1)) } +} + +struct AddKnowledgePage: View { + @State private var title = ""; @State private var content = "" + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + 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)} +} + +struct KnowledgeDetailPage: View { + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + 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)) } } + }.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) } + }.navigationBarHidden(true)} +} +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()) } +} + +struct ImportPage: View { + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + ScrollView { VStack(spacing: 12) { + ZXImportOption(icon: "camera.fill", title: "拍照导入", desc: "拍下书本或笔记,AI 自动识别") + 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)} +} +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) } +} + +struct EditKnowledgePage: View { + @State private var title = "偏差-方差权衡"; @State private var content = "偏差衡量模型的预测与真实值之间的差异..." + var body: some View { + ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { + 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)} +} diff --git a/AIStudyApp/AIStudyApp/Features/Onboarding/GoalSetupPage.swift b/AIStudyApp/AIStudyApp/Features/Onboarding/GoalSetupPage.swift deleted file mode 100644 index b251cb2..0000000 --- a/AIStudyApp/AIStudyApp/Features/Onboarding/GoalSetupPage.swift +++ /dev/null @@ -1,85 +0,0 @@ -import SwiftUI - -// MARK: - Goal Setup Page - -struct GoalSetupPage: View { - let onComplete: (Bool) -> Void - @State private var selectedGoal = "" - let goals = [("🧑‍🎓","备考考试","公考、考研、考证等"),("💼","职业技能","编程、设计、产品等"),("📚","通识学习","扩充知识面"),("🎯","自定义","设定自己的目标")] - @State private var selectedMethod = "" - let methods = ["间隔回忆","费曼技巧","AI 分析"] - @State private var dailyMins = "30 分钟" - let times = ["15 分钟","30 分钟","1 小时","不限制"] - var body: some View { - ZStack { ZXGradient.page.ignoresSafeArea() - VStack(spacing: 0) { Spacer() - Text("设定你的学习目标").font(.system(size: 24, weight: .heavy)).tracking(-0.5).foregroundColor(Color.zxF0).padding(.bottom, 24) - ScrollView { VStack(spacing: 16) { - VStack(alignment: .leading, spacing: 10) { - Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - 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.zxF02, 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) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - .foregroundColor(.primary) - } - } - VStack(alignment: .leading, spacing: 10) { - Text("学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - HStack(spacing: 8) { - ForEach(methods, id: \.self) { m in let sel = selectedMethod == m - Button { selectedMethod = m } label: { - Text(m).font(.system(size: 13)).fontWeight(sel ? .semibold : .regular) - .foregroundColor(sel ? Color.zxPurple : Color.zxF05) - .padding(.horizontal, 16).padding(.vertical, 10) - .background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - .foregroundColor(.primary) - } - } - } - VStack(alignment: .leading, spacing: 10) { - Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - 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) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(sel ? Color(hex: "#7C6EFA", opacity: 0.25) : Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - .foregroundColor(.primary) - } - } - } - } }.padding(.horizontal, 20) - Button { onComplete(true) } label: { - Text("开始学习").font(.system(size: 16, weight: .bold)).foregroundColor(.white) - .frame(maxWidth: .infinity).frame(height: 56) - .background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)) - .shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) - }.padding(.top, 24).padding(.bottom, 32).padding(.horizontal, 20) - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Onboarding/OnboardingPage.swift b/AIStudyApp/AIStudyApp/Features/Onboarding/OnboardingPage.swift deleted file mode 100644 index 74e1c93..0000000 --- a/AIStudyApp/AIStudyApp/Features/Onboarding/OnboardingPage.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -// MARK: - Onboarding Page - -struct OnboardingPage: View { - let onContinue: () -> Void - @State private var step = 0 - let titles = ["输入知识", "主动输出", "AI 分析", "掌握知识"] - let descs = ["从任何地方收集并导入学习资料,构建你的专属知识库。", "通过间隔回忆和费曼解释法,将知识转化为长期记忆。", "AI 自动定位薄弱知识点,给出针对性的学习建议。", "系统性掌握每一个知识点,建立牢固的知识体系。"] - let icons = ["square.and.arrow.down", "brain.head.profile", "sparkle.magnifyingglass", "chart.line.uptrend.xyaxis"] - var body: some View { - ZStack { ZXGradient.page.ignoresSafeArea() - VStack(spacing: 0) { Spacer() - HStack(spacing: 6) { - ForEach(0..<4, id: \.self) { i in - RoundedRectangle(cornerRadius: 2) - .fill(i == step ? AnyShapeStyle(ZXGradient.brand) : AnyShapeStyle(Color.zxFill01)) - .frame(width: i == step ? 24 : 8, height: 4) - } - } - VStack(spacing: 12) { - Text(titles[step]).font(.system(size: 24, weight: .heavy)).tracking(-0.5) - Text(descs[step]).font(.system(size: 14)).foregroundColor(Color.zxF04).lineSpacing(4).multilineTextAlignment(.center) - }.padding(.top, 32).padding(.bottom, 40) - Button { - if step < 3 { withAnimation { step += 1 } } else { onContinue() } - } label: { - Text(step < 3 ? "下一步" : "开始使用").font(.system(size: 16, weight: .bold)).foregroundColor(.white) - .frame(maxWidth: .infinity).frame(height: 56) - .background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)) - .shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) - } - Button("跳过") { onContinue() }.font(.system(size: 12)).foregroundColor(Color.zxF03).padding(.top, 12).padding(.bottom, 32) - }.padding(.horizontal, 20) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Onboarding/SplashPage.swift b/AIStudyApp/AIStudyApp/Features/Onboarding/SplashPage.swift deleted file mode 100644 index 18a66e7..0000000 --- a/AIStudyApp/AIStudyApp/Features/Onboarding/SplashPage.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -// MARK: - Splash Page - -struct SplashPage: View { - let onFinish: () -> Void - var body: some View { - ZStack { - ZXGradient.splash.ignoresSafeArea() - Circle().fill(RadialGradient(colors: [Color(hex: "#7C6EFA", opacity: 0.25), .clear], center: .center, startRadius: 0, endRadius: 140)).frame(width: 280, height: 280).offset(y: -60).allowsHitTesting(false) - Circle().fill(RadialGradient(colors: [Color(hex: "#F97316", opacity: 0.15), .clear], center: .center, startRadius: 0, endRadius: 100)).frame(width: 200, height: 200).offset(y: 180).allowsHitTesting(false) - VStack(spacing: 0) { - RoundedRectangle(cornerRadius: 28) - .fill(LinearGradient(colors: [Color(hex: "#7C6EFA"), Color(hex: "#A78BFA"), Color(hex: "#F97316")], startPoint: .topLeading, endPoint: .bottomTrailing)) - .frame(width: 96, height: 96) - .overlay(Image(systemName: "brain.head.profile").font(.system(size: 44)).foregroundColor(.white.opacity(0.8))) - .shadow(color: Color(hex: "#7C6EFA", opacity: 0.5), radius: 40) - .padding(.bottom, 24) - Text("知习") - .font(.system(size: 36, weight: .heavy)).tracking(-1) - .foregroundStyle(LinearGradient(colors: [Color(hex: "#A78BFA"), Color(hex: "#F0F0FF"), Color(hex: "#F97316")], startPoint: .leading, endPoint: .trailing)) - Text("Z H I X I").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF04).tracking(3).padding(.top, 6) - Text("AI-first 系统化学习").font(.system(size: 14)).foregroundColor(Color.zxF0045).tracking(0.5).padding(.top, 24) - } - VStack { Spacer() - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 2).fill(Color.zxFill01).frame(width: 40, height: 3) - RoundedRectangle(cornerRadius: 2).fill(LinearGradient(colors: [.zxPurple, Color.zxOrange], startPoint: .leading, endPoint: .trailing)).frame(width: 24, height: 3) - } - .padding(.bottom, 80) - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Onboarding/WelcomePage.swift b/AIStudyApp/AIStudyApp/Features/Onboarding/WelcomePage.swift deleted file mode 100644 index 1bdf27a..0000000 --- a/AIStudyApp/AIStudyApp/Features/Onboarding/WelcomePage.swift +++ /dev/null @@ -1,42 +0,0 @@ -import SwiftUI - -// MARK: - Welcome Page - -struct WelcomePage: View { - let onContinue: () -> Void; let onSkip: () -> Void - @State private var showWaitlist = false - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - Circle().fill(RadialGradient(colors: [Color(hex: "#7C6EFA", opacity: 0.12), .clear], center: .topTrailing, startRadius: 0, endRadius: 260)).frame(width: 260, height: 260).offset(x: 80, y: -120).allowsHitTesting(false) - VStack { Spacer() - VStack(spacing: 14) { - HStack(spacing: 6) { Image(systemName: "sparkles").font(.system(size: 12)); Text("AI 驱动").font(.system(size: 12, weight: .semibold)) } - .foregroundColor(Color.zxAccent).padding(.horizontal, 12).padding(.vertical, 6).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(Capsule()) - Text("用 AI 重新定义\n你的学习方式").font(.system(size: 32, weight: .heavy)).tracking(-0.8).lineSpacing(4) - VStack(spacing: 10) { FeatureRow(icon: "🧠", title: "主动回忆", desc: "基于间隔重复的智能复习"); FeatureRow(icon: "🎤", title: "费曼解释", desc: "用自己的话讲出来"); FeatureRow(icon: "📊", title: "AI 分析", desc: "发现知识薄弱点") } - } - VStack(spacing: 12) { - Button { onContinue() } label: { - Text("开始使用").font(.system(size: 16, weight: .bold)).foregroundColor(.white) - .frame(maxWidth: .infinity).frame(height: 56) - .background(ZXGradient.ctaButton).clipShape(RoundedRectangle(cornerRadius: 18)) - .shadow(color: Color(hex: "#7C6EFA", opacity: 0.4), radius: 20) - } - Button { showWaitlist = true } label: { - Text("申请内测资格").font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxAccent) - } - Button { onSkip() } label: { - Text("已有账号?立即登录").font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxF007) - }.padding(.bottom, 32) - } - }.padding(.horizontal, 20) - } - .sheet(isPresented: $showWaitlist) { - WaitlistView() - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift index 18d851b..95240ee 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift @@ -1,15 +1,6 @@ -// -// ProfileView.swift - Page 10: Profile -// - import SwiftUI struct ProfileView: View { - @StateObject private var colorSchemeManager = ColorSchemeManager.shared - @StateObject private var languageManager = LanguageManager.shared - @State private var showAppearancePicker = false - @State private var showLanguagePicker = false - var body: some View { ZStack { ZXGradient.page.ignoresSafeArea() @@ -20,76 +11,18 @@ struct ProfileView: View { Spacer() ZXIconBtn(icon: "bell", size: 36) {} ZXIconBtn(icon: "gearshape", size: 36) {} - } - .padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4) + }.padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4) profileCard VStack(spacing: 0) { - NavigationLink(destination: LearningGoalSettingsView()) { - ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标") - } - .foregroundColor(.primary) - NavigationLink(destination: ReviewReminderSettingsView()) { - ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置") - } - .foregroundColor(.primary) - NavigationLink(destination: LearningReportView()) { - ZXProfileMenuRow(emoji: "📊", title: "学习报告", desc: "周报 · 月报 · 成就") - } - .foregroundColor(.primary) - NavigationLink(destination: LearningMethodPreferencesView()) { - ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔") - } - .foregroundColor(.primary) - NavigationLink(destination: DataSyncSettingsView()) { - ZXProfileMenuRow(emoji: "☁️", title: "数据同步与备份", desc: "云端同步设置") - } - .foregroundColor(.primary) - NavigationLink(destination: FeedbackView()) { - ZXProfileMenuRow(emoji: "💬", title: "反馈", desc: "问题报告 · 功能建议") - } - .foregroundColor(.primary) - } - .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) - // 设置 - VStack(spacing: 0) { - Button { - showLanguagePicker = true - } label: { - ZXProfileMenuRow(emoji: "🌐", title: "语言", desc: languageManager.current.displayName) - } - .buttonStyle(.plain) - .confirmationDialog("语言", isPresented: $showLanguagePicker) { - ForEach(LanguageManager.shared.supported) { lang in - Button(lang.displayName) { - languageManager.current = lang - } - } - Button("取消", role: .cancel) {} - } - Button { - showAppearancePicker = true - } label: { - ZXProfileMenuRow(emoji: "🌓", title: "外观", desc: colorSchemeManager.current.displayName) - } - .buttonStyle(.plain) - .confirmationDialog("外观", isPresented: $showAppearancePicker) { - ForEach(AppColorScheme.allCases) { scheme in - Button(scheme.displayName) { - colorSchemeManager.current = scheme - } - } - Button("取消", role: .cancel) {} - } - } - .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) - + ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标") + ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置") + ZXProfileMenuRow(emoji: "📊", title: "学习报告", desc: "周报 · 月报 · 成就") + ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔") + ZXProfileMenuRow(emoji: "☁️", title: "数据同步与备份", desc: "云端同步设置") + }.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) achievementsSection.padding(.bottom, 120) - } - .padding(.horizontal, 20) - } - .scrollIndicators(.hidden) + }.padding(.horizontal, 20) + }.scrollIndicators(.hidden) } } private var profileCard: some View { @@ -100,8 +33,7 @@ struct ProfileView: View { 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)) + }.padding(20).background(ZXGradient.profileCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) } private var achievementsSection: some View { VStack(alignment: .leading, spacing: 12) { @@ -110,4 +42,12 @@ struct ProfileView: View { } } } -// ZXProfileStat, ZXProfileMenuRow, ZXAchievementBadge → Shared/Components/ +struct ZXProfileStat: View { let v: String; let l: String; let c: Color; var body: some View { VStack(spacing: 2) { Text(v).font(.system(size: 18, weight: .bold)).foregroundColor(c); Text(l).font(.system(size: 11)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) } + init(value: String, label: String, color: Color) { self.v = value; self.l = label; self.c = color } +} +struct ZXProfileMenuRow: View { let emoji: String; let title: String; let desc: String + var body: some View { HStack(spacing: 12) { Text(emoji).font(.system(size: 20)).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14) } +} +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/SettingsPages.swift b/AIStudyApp/AIStudyApp/Features/Profile/SettingsPages.swift deleted file mode 100644 index 804883b..0000000 --- a/AIStudyApp/AIStudyApp/Features/Profile/SettingsPages.swift +++ /dev/null @@ -1,275 +0,0 @@ -import SwiftUI - -// MARK: - Learning Goal Settings - -struct LearningGoalSettingsView: View { - @State private var selectedGoal = "" - @State private var dailyMins = "30 分钟" - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: 20) { - ZXBackHeader(title: "学习目标设置") - VStack(alignment: .leading, spacing: 10) { - Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - ForEach([("🧑‍🎓", "备考考试"), ("💼", "职业技能"), ("📚", "通识学习"), ("🎯", "自定义")], id: \.1) { emoji, title in - let sel = selectedGoal == title - Button { - selectedGoal = title - } label: { - HStack(spacing: 12) { - Text(emoji).font(.system(size: 22)).frame(width: 44, height: 44) - .background(sel ? Color.zxPurpleBG(0.15) : Color.zxFill005) - .clipShape(RoundedRectangle(cornerRadius: 12)) - Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0) - Spacer() - Circle().stroke(sel ? Color.zxPurple : Color.zxF02, lineWidth: 2) - .frame(width: 22, height: 22) - .overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } } - } - .padding(14) - .background(sel ? Color.zxPurpleBG(0.08) : Color.zxFill003) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(sel ? Color.zxPurpleBG(0.25) : Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - .foregroundColor(.primary) - } - } - VStack(alignment: .leading, spacing: 10) { - Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5) - HStack(spacing: 8) { - ForEach(["15 分钟", "30 分钟", "1 小时", "不限制"], 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.zxPurpleBG(0.1) : Color.zxFill003) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(sel ? Color.zxPurpleBG(0.25) : Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - .foregroundColor(.primary) - } - } - } - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - .navigationBarHidden(true) - } -} - -// MARK: - Review Reminder Settings - -struct ReviewReminderSettingsView: View { - @State private var reminderEnabled = true - @State private var reminderTime = Date() - @State private var intervalDays = 1 - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: 20) { - ZXBackHeader(title: "复习提醒") - VStack(spacing: 0) { - Toggle(isOn: $reminderEnabled) { - VStack(alignment: .leading, spacing: 2) { - Text("开启复习提醒").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - Text("基于间隔重复算法,在最佳时间提醒你复习").font(.system(size: 12)).foregroundColor(Color.zxF04) - } - } - .tint(Color.zxPurple) - .padding(.horizontal, 16).padding(.vertical, 14) - Divider().background(Color.zxBorder006) - DatePicker("提醒时间", selection: $reminderTime, displayedComponents: .hourAndMinute) - .font(.system(size: 14)).foregroundColor(Color.zxF0) - .padding(.horizontal, 16).padding(.vertical, 14) - .tint(Color.zxPurple) - Divider().background(Color.zxBorder006) - HStack { - Text("间隔天数").font(.system(size: 14)).foregroundColor(Color.zxF0) - Spacer() - Stepper("每 \(intervalDays) 天", value: $intervalDays, in: 1...7) - .font(.system(size: 14)).foregroundColor(Color.zxF05) - } - .padding(.horizontal, 16).padding(.vertical, 14) - } - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - .navigationBarHidden(true) - } -} - -// MARK: - Learning Report - -struct LearningReportView: View { - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: 20) { - ZXBackHeader(title: "学习报告") - VStack(spacing: 12) { - ReportCard(period: "本周", studyDays: 5, totalMins: 320, newItems: 12, reviewed: 47) - ReportCard(period: "本月", studyDays: 18, totalMins: 1240, newItems: 47, reviewed: 186) - } - VStack(alignment: .leading, spacing: 8) { - Text("成就").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) - HStack(spacing: 8) { - ZXAchievementBadge(emoji: "🔥", label: "连续 14 天", color: Color.zxOrange) - ZXAchievementBadge(emoji: "🧠", label: "费曼达人", color: Color.zxPurple) - ZXAchievementBadge(emoji: "📚", label: "知识收藏家", color: Color.zxTeal) - ZXAchievementBadge(emoji: "⚡", label: "速学者", color: Color.zxYellow) - } - } - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - .navigationBarHidden(true) - } -} - -private struct ReportCard: View { - let period: String; let studyDays: Int; let totalMins: Int; let newItems: Int; let reviewed: Int - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text(period).font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxPurple) - HStack(spacing: 0) { - ReportStat(value: "\(studyDays)", label: "学习天") - ReportStat(value: "\(totalMins)", label: "分钟") - ReportStat(value: "\(newItems)", label: "新知识") - ReportStat(value: "\(reviewed)", label: "已复习") - } - } - .padding(16) - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - } -} - -private struct ReportStat: View { - let value: String; let label: String - var body: some View { - VStack(spacing: 2) { - Text(value).font(.system(size: 20, weight: .heavy)).foregroundColor(Color.zxF0) - Text(label).font(.system(size: 10)).foregroundColor(Color.zxF04) - } - .frame(maxWidth: .infinity) - } -} - -// MARK: - Learning Method Preferences - -struct LearningMethodPreferencesView: View { - @State private var selectedMethods: Set = ["间隔回忆", "费曼技巧"] - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: 20) { - ZXBackHeader(title: "学习方法偏好") - VStack(spacing: 0) { - ForEach([ - ("间隔回忆", "基于遗忘曲线,在最佳时机提醒你复习"), - ("费曼技巧", "用自己的语言重新解释知识,发现理解盲区"), - ("AI 分析", "AI 自动定位薄弱知识点,给出针对性建议") - ], id: \.0) { method, desc in - let sel = selectedMethods.contains(method) - Button { - if sel { selectedMethods.remove(method) } - else { selectedMethods.insert(method) } - } label: { - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 2) { - Text(method).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) - } - Spacer() - Image(systemName: sel ? "checkmark.circle.fill" : "circle") - .font(.system(size: 22)) - .foregroundColor(sel ? Color.zxPurple : Color.zxF02) - } - .padding(.horizontal, 16).padding(.vertical, 14) - } - .foregroundColor(.primary) - if method != "AI 分析" { - Divider().background(Color.zxBorder006) - } - } - } - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - .navigationBarHidden(true) - } -} - -// MARK: - Data Sync Settings - -struct DataSyncSettingsView: View { - @State private var iCloudEnabled = false - @State private var autoBackupEnabled = true - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: 20) { - ZXBackHeader(title: "数据同步与备份") - VStack(spacing: 0) { - Toggle(isOn: $iCloudEnabled) { - VStack(alignment: .leading, spacing: 2) { - Text("iCloud 同步").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - Text("在 Apple ID 关联的设备间同步学习数据").font(.system(size: 12)).foregroundColor(Color.zxF04) - } - } - .tint(Color.zxPurple) - .padding(.horizontal, 16).padding(.vertical, 14) - Divider().background(Color.zxBorder006) - Toggle(isOn: $autoBackupEnabled) { - VStack(alignment: .leading, spacing: 2) { - Text("自动备份").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - Text("每日自动备份学习记录和知识库").font(.system(size: 12)).foregroundColor(Color.zxF04) - } - } - .tint(Color.zxPurple) - .padding(.horizontal, 16).padding(.vertical, 14) - } - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - .navigationBarHidden(true) - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Review/ReviewPlanView.swift b/AIStudyApp/AIStudyApp/Features/Review/ReviewPlanView.swift deleted file mode 100644 index c703e3d..0000000 --- a/AIStudyApp/AIStudyApp/Features/Review/ReviewPlanView.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI - -struct ReviewPlanView: View { - @StateObject private var vm = ReviewPlanViewModel() - - var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - - VStack(spacing: 0) { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("复习计划") - .font(.system(size: 22, weight: .heavy)) - .foregroundColor(Color.zxF0) - .tracking(-0.5) - Text("\(vm.totalCount) 个待复习") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - } - Spacer() - } - .padding(.horizontal, 20) - .padding(.top, ZXSpacing.statusBarH + 16) - .padding(.bottom, 16) - - ScrollView { - VStack(spacing: 20) { - sectionView(title: "今天", icon: "sun.max.fill", tasks: vm.todayTasks, color: Color.zxOrange) - sectionView(title: "明天", icon: "sunrise.fill", tasks: vm.tomorrowTasks, color: Color.zxPurple) - sectionView(title: "本周", icon: "calendar", tasks: vm.weekTasks, color: Color.zxTeal) - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - } - .navigationBarHidden(true) - } - - func sectionView(title: String, icon: String, tasks: [ReviewTaskEntity], color: Color) -> some View { - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 8) { - Image(systemName: icon) - .font(.system(size: 14)) - .foregroundColor(color) - Text(title) - .font(.system(size: 15, weight: .bold)) - .foregroundColor(Color.zxF0) - Spacer() - Text("\(tasks.count) 项") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - } - - if tasks.isEmpty { - ZXEmptyView(icon: "checkmark.circle", title: "暂无复习任务", subtitle: "完成学习后 AI 会自动生成复习计划") - } else { - ForEach(tasks) { task in - ReviewTaskRow(task: task) { - vm.toggleTask(task) - } - } - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Review/ViewModels/ReviewPlanViewModel.swift b/AIStudyApp/AIStudyApp/Features/Review/ViewModels/ReviewPlanViewModel.swift deleted file mode 100644 index d70733a..0000000 --- a/AIStudyApp/AIStudyApp/Features/Review/ViewModels/ReviewPlanViewModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -import SwiftUI -import Combine - -@MainActor -final class ReviewPlanViewModel: ObservableObject { - private let persistence = PersistenceController.shared - - @Published var todayTasks: [ReviewTaskEntity] = [] - @Published var tomorrowTasks: [ReviewTaskEntity] = [] - @Published var weekTasks: [ReviewTaskEntity] = [] - - var totalCount: Int { todayTasks.count + tomorrowTasks.count + weekTasks.count } - - init() { - persistence.seedIfNeeded() - fetchAll() - } - - func toggleTask(_ task: ReviewTaskEntity) { - let all = persistence.loadReviewTasks() - guard let i = all.firstIndex(where: { $0.id == task.id }) else { return } - var updated = all - let newStatus: ReviewTaskEntityStatus = task.statusEnum == .completed ? .pending : .completed - updated[i].statusEnum = newStatus - updated[i].completedAt = newStatus == .completed ? Date() : nil - persistence.saveReviewTasks(updated) - fetchAll() - } - - private func fetchAll() { - let all = persistence.loadReviewTasks() - todayTasks = all.filter(\.isToday) - tomorrowTasks = all.filter(\.isTomorrow) - weekTasks = all.filter { $0.isThisWeek && !$0.isToday && !$0.isTomorrow } - } -} diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift index 5db02a4..cf1ff04 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -1,187 +1,44 @@ import SwiftUI struct StudyHomeView: View { - @StateObject private var vm = StudyHomeViewModel() + @State private var ts: [ZXSTask] = [ + .init(t: "机器学习 - 回忆测试", tp: "回忆测试", c: Color.zxPurple, m: 10, d: true), + .init(t: "高数 - 间隔复习 8 题", tp: "间隔复习", c: Color.zxOrange, m: 15, d: true), + .init(t: "英语词汇 - 25 个待复习", tp: "词汇复习", c: Color.zxTeal, m: 8, d: false), + .init(t: "注意力机制 - 费曼解释", tp: "费曼练习", c: Color.zxAccent, m: 12, d: false), + .init(t: "产品设计 - 薄弱点复习", tp: "薄弱点", c: Color.zxYellow, m: 10, d: false), + ] + private let wb: [CGFloat] = [0.3, 0.7, 1.0, 0.4, 0.9, 0.6, 0.2] + private let dl = ["一","二","三","四","五","六","日"] var body: some View { - ZStack { - ZXGradient.page.ignoresSafeArea() - ScrollView { - VStack(spacing: 16) { - headerRow - progressCard - taskSection - weeklyActivitySection - } - .padding(.horizontal, 20) - .padding(.bottom, 120) - } - .scrollIndicators(.hidden) - } - } - - // MARK: - Header - - private var headerRow: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("周四,1月16日") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.zxF04) - Text("学习工作台") - .font(.system(size: 20, weight: .heavy)) - .foregroundColor(Color.zxF0) - .tracking(-0.4) - } - Spacer() - HStack(spacing: 4) { - Image(systemName: "flame.fill") - .font(.system(size: 14)) - .foregroundColor(Color.zxOrange) - Text("14 天连续") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.zxOrange) - } - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.zxOrangeBG(0.1)) - .clipShape(Capsule()) - .overlay(Capsule().stroke(Color(hex: "#F97316", opacity: 0.2), lineWidth: 1)) - } - .padding(.horizontal, 20) - .padding(.top, ZXSpacing.statusBarH + 16) - .padding(.bottom, 4) - } - - // MARK: - Progress Card - - private var progressCard: some View { - let pct = vm.progress - return VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("今日进度") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(Color.zxF05) - HStack(alignment: .lastTextBaseline, spacing: 6) { - Text("\(vm.doneCount)") - .font(.system(size: 26, weight: .black)) - .foregroundColor(Color.zxF0) - Text("/ \(vm.totalCount)") - Text("个任务") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxF04) - } - } - Spacer() - ZStack { - Circle() - .trim(from: 0, to: pct) - .stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .frame(width: 64, height: 64) - Text("\(Int(pct * 100))%") - .font(.system(size: 14, weight: .heavy)) - .foregroundColor(Color.zxPurple) - } - } - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 3) - .fill(Color.zxFill008) - .frame(height: 6) - RoundedRectangle(cornerRadius: 3) - .fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)) - .frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6) - } - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("\(vm.doneMinutes) 分钟") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.zxF0) - Text("已学") - .font(.system(size: 10)) - .foregroundColor(Color.zxF04) - } - Spacer() - VStack(spacing: 2) { - Text("\(vm.remainingMinutes) 分钟") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.zxF0) - Text("剩余") - .font(.system(size: 10)) - .foregroundColor(Color.zxF04) - } - Spacer() - VStack(alignment: .trailing, spacing: 2) { - Text("+5 点") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.zxF0) - Text("掌握") - .font(.system(size: 10)) - .foregroundColor(Color.zxF04) - } - } - } - .padding(16) - .background(ZXGradient.progressCard) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - - // MARK: - Task Section - - private var taskSection: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("今日任务") - .font(.system(size: 15, weight: .bold)) - .foregroundColor(Color.zxF0) - Spacer() - HStack(spacing: 4) { - Image(systemName: "calendar") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - Text("AI 自动排期") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - } - } - ForEach(vm.tasks) { task in - ZXSTaskRow(task: task) { vm.toggleTask(task) } - .transition(.opacity.combined(with: .offset(y: 8))) - } - } - } - - // MARK: - Weekly Activity - - private var weeklyActivitySection: some View { - 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: vm.weekActivity[i] * 0.9 + 0.1)) - .frame(height: vm.weekActivity[i] * 60) - Text(vm.dayLabels[i]) - .font(.system(size: 10, weight: i == 2 ? .bold : .regular)) - .foregroundColor(i == 2 ? Color.zxPurple : Color.zxF03) - } - .frame(maxWidth: .infinity) - } - } - HStack { - Text("总计 \(vm.todayTotalMinutes / 60) 小时 \(vm.todayTotalMinutes % 60) 分钟") - .font(.system(size: 11)) - .foregroundColor(Color.zxF03) - Spacer() - Text("日均 \(max(1, vm.todayTotalMinutes)) 分钟") - .font(.system(size: 11)) - .foregroundColor(Color.zxF03) - } - } + ZStack { ZXGradient.page.ignoresSafeArea() + ScrollView { VStack(spacing: 16) { + HStack { VStack(alignment: .leading, spacing: 2) { Text("周四,1月16日").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxF04); Text("学习工作台").font(.system(size: 20, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.4) }; Spacer() + HStack(spacing: 4) { Image(systemName: "flame.fill").font(.system(size: 14)).foregroundColor(Color.zxOrange); Text("14 天连续").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxOrange) }.padding(.horizontal, 12).padding(.vertical, 6).background(Color.zxOrangeBG(0.1)).clipShape(Capsule()).overlay(Capsule().stroke(Color(hex: "#F97316", opacity: 0.2), lineWidth: 1)) } + .padding(.horizontal, 20).padding(.top, 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() } } } + 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) } } + .padding(.bottom, 120) } + .padding(.horizontal, 20) } + .scrollIndicators(.hidden) } } + private var pc: some View { let dn = ts.filter(\.d).count; let pct = CGFloat(dn) / 5 + return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer() + ZStack { Circle().trim(from: 0, to: pct).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 8, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 64, height: 64); Text("\(Int(pct * 100))%").font(.system(size: 14, weight: .heavy)).foregroundColor(Color.zxPurple) } } + ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.zxFill008).frame(height: 6); RoundedRectangle(cornerRadius: 3).fill(LinearGradient(colors: [Color.zxPurple, Color.zxAccent], startPoint: .leading, endPoint: .trailing)).frame(width: max(6, pct * (UIScreen.main.bounds.width - 72)), height: 6) } + HStack { VStack(alignment: .leading, spacing: 2) { Text("\(dn * 12) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("已学").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(spacing: 2) { Text("\((5 - dn) * 11) 分钟").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("剩余").font(.system(size: 10)).foregroundColor(Color.zxF04) }; Spacer(); VStack(alignment: .trailing, spacing: 2) { Text("+5 点").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0); Text("掌握").font(.system(size: 10)).foregroundColor(Color.zxF04) } } } + .padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) } +} + +struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let c: Color; let m: Int; var d: Bool } +struct 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) + 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) } } diff --git a/AIStudyApp/AIStudyApp/Features/Study/ViewModels/StudyHomeViewModel.swift b/AIStudyApp/AIStudyApp/Features/Study/ViewModels/StudyHomeViewModel.swift deleted file mode 100644 index eca2ab8..0000000 --- a/AIStudyApp/AIStudyApp/Features/Study/ViewModels/StudyHomeViewModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI -import Combine - -@MainActor -final class StudyHomeViewModel: ObservableObject { - private let persistence = PersistenceController.shared - - @Published var tasks: [StudyTaskEntity] = [] - @Published var records: [LearningRecordEntity] = [] - - let dayLabels = ["一", "二", "三", "四", "五", "六", "日"] - - var weekActivity: [CGFloat] { - let calendar = Calendar.current - var mins: [Int: Int] = [:] - for r in records where calendar.isDate(r.completedAt, equalTo: Date(), toGranularity: .weekOfYear) { - let wd = calendar.component(.weekday, from: r.completedAt) - let idx = (wd + 5) % 7 - mins[idx, default: 0] += r.durationMinutes - } - let maxMins = max(mins.values.max() ?? 1, 1) - return (0..<7).map { CGFloat(mins[$0] ?? 0) / CGFloat(maxMins) } - } - - var doneCount: Int { tasks.filter(\.isDone).count } - var totalCount: Int { tasks.count } - var progress: CGFloat { totalCount > 0 ? CGFloat(doneCount) / CGFloat(totalCount) : 0 } - var doneMinutes: Int { tasks.filter(\.isDone).reduce(0) { $0 + $1.minutes } } - var remainingMinutes: Int { tasks.filter { !$0.isDone }.reduce(0) { $0 + $1.minutes } } - var todayTotalMinutes: Int { records.filter { Calendar.current.isDateInToday($0.completedAt) }.reduce(0) { $0 + $1.durationMinutes } } - - init() { - persistence.seedIfNeeded() - tasks = persistence.loadTasks() - records = persistence.loadRecords() - } - - func toggleTask(_ task: StudyTaskEntity) { - guard let i = tasks.firstIndex(where: { $0.id == task.id }) else { return } - tasks[i].isDone.toggle() - persistence.saveTasks(tasks) - } - - func recordSession(lessonTitle: String, durationMinutes: Int, masteryScore: Int, weakPoints: [String]) { - let record = LearningRecordEntity( - lessonTitle: lessonTitle, - durationMinutes: durationMinutes, - masteryScore: masteryScore, - weakPoints: weakPoints - ) - records.append(record) - persistence.saveRecords(records) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/FeatureRow.swift b/AIStudyApp/AIStudyApp/Shared/Components/FeatureRow.swift deleted file mode 100644 index 32ea5a4..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/FeatureRow.swift +++ /dev/null @@ -1,21 +0,0 @@ -import SwiftUI - -// MARK: - Feature Row - -struct FeatureRow: View { - let icon: String; let title: String; let desc: String - var body: some View { - HStack(spacing: 14) { - Text(icon).font(.system(size: 20)).frame(width: 40, height: 40) - .background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12)) - VStack(alignment: .leading, spacing: 2) { - Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) - } - } - .padding(.horizontal, 16).padding(.vertical, 14) - .background(Color.zxFill003) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ReviewTaskRow.swift b/AIStudyApp/AIStudyApp/Shared/Components/ReviewTaskRow.swift deleted file mode 100644 index e2c9482..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ReviewTaskRow.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftUI - -struct ReviewTaskRow: View { - let task: ReviewTaskEntity - let onToggle: () -> Void - - var body: some View { - HStack(spacing: 12) { - Button(action: onToggle) { - Image(systemName: task.statusEnum == .completed ? "checkmark.circle.fill" : "circle") - .font(.system(size: 20)) - .foregroundColor(task.statusEnum == .completed ? Color.zxGreen : Color.zxF02) - } - - VStack(alignment: .leading, spacing: 4) { - Text(task.lessonId) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(task.statusEnum == .completed ? Color.zxF04 : Color.zxF0) - - HStack(spacing: 8) { - reviewTypeTag(task.reviewTypeEnum) - Text("第 1 次复习") - .font(.system(size: 10)) - .foregroundColor(Color.zxF035) - } - } - - Spacer() - - if task.statusEnum == .pending { - 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(Color.zxFill003) - .overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(Color.zxBorder006, lineWidth: 1) - ) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .opacity(task.statusEnum == .completed ? 0.6 : 1) - .accessibilityLabel("复习任务:\(task.lessonId),\(reviewTypeLabel(task.reviewTypeEnum))") - .accessibilityHint(task.statusEnum == .completed ? "已完成" : "双击开始复习") - } - - private func reviewTypeLabel(_ type: ReviewTaskEntityType) -> String { - switch type { - case .spacedRepetition: return "间隔重复" - case .feynman: return "费曼技巧" - case .recall: return "主动回忆" - case .weakPoint: return "薄弱点巩固" - } - } - - func reviewTypeTag(_ type: ReviewTaskEntityType) -> some View { - let config: (String, Color) = { - switch type { - case .spacedRepetition: return ("间隔重复", Color.zxPurple) - case .feynman: return ("费曼", Color.zxAccent) - case .recall: return ("回忆", Color.zxOrange) - case .weakPoint: return ("薄弱", Color.zxYellow) - } - }() - return Text(config.0) - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(config.1) - .padding(.horizontal, 6) - .padding(.vertical, 1) - .background(config.1.opacity(0.12)) - .clipShape(Capsule()) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInputBar.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInputBar.swift deleted file mode 100644 index ebe97a3..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInputBar.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI - -// MARK: - AI Input Bar - -struct ZXAIInputBar: View { - @Binding var text: String - let onSend: () -> Void - var body: some View { - HStack(spacing: 10) { - Image(systemName: "sparkles").font(.system(size: 16)).foregroundColor(Color.zxPurple) - TextField("问 AI 任何学习问题…", text: $text).font(.system(size: 14)).tint(Color.zxPurple) - .accessibilityLabel("AI 对话输入") - Spacer() - Image(systemName: "mic.fill").font(.system(size: 18)).foregroundColor(Color.zxF03) - .accessibilityHidden(true) - Button(action: onSend) { - Image(systemName: "arrow.up").font(.system(size: 14, weight: .bold)).foregroundColor(.white) - .frame(width: 30, height: 30).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 9)) - } - .accessibilityLabel("发送消息") - } - .padding(.horizontal, 14).padding(.vertical, 10) - .background(.ultraThinMaterial).background(Color.zxFill004) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder008, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 20)) - .padding(.horizontal, 20).padding(.bottom, 34) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInteractionRow.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInteractionRow.swift deleted file mode 100644 index ff67cda..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXAIInteractionRow.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -// MARK: - AI Interaction Row - -struct ZXAIInteractionRow: View { - let tag: String; let bg: Color; let fg: Color; let emoji: String - let title: String; let time: String; let score: Int; let action: () -> Void - var body: some View { - Button(action: action) { - HStack(spacing: 12) { - Text(emoji).font(.system(size: 18)).frame(width: 40, height: 40) - .background(bg).clipShape(RoundedRectangle(cornerRadius: 12)) - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 8) { - Text(tag).font(.system(size: 10, weight: .bold)).foregroundColor(fg).tracking(0.3) - Text(time).font(.system(size: 10)).foregroundColor(Color.zxF03) - } - Text(title).font(.system(size: 13, weight: .semibold)) - .foregroundColor(Color.zxF007).lineLimit(1) - } - Spacer() - ZXScoreBox( - score: score, - bg: score >= 80 ? Color.zxGreenBG(0.15) : score >= 60 ? Color.zxOrangeBG(0.15) : Color.zxRedBG(0.15), - fg: score >= 80 ? Color.zxGreen : score >= 60 ? Color.zxOrange : Color.zxRed - ) - } - .padding(.horizontal, 14).padding(.vertical, 12) - .background(Color.zxFill003) - .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXAchievementBadge.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXAchievementBadge.swift deleted file mode 100644 index 6b5e781..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXAchievementBadge.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -// MARK: - Achievement Badge - -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/Shared/Components/ZXBackHeader.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXBackHeader.swift deleted file mode 100644 index 404ee63..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXBackHeader.swift +++ /dev/null @@ -1,32 +0,0 @@ -import SwiftUI - -// MARK: - Back Header - -struct ZXBackHeader: View { - let title: String; let subtitle: String?; var onBack: (() -> Void)? - @ViewBuilder var trailing: () -> T - - init(title: String, subtitle: String? = nil, onBack: (() -> Void)? = nil, @ViewBuilder trailing: @escaping () -> T = { EmptyView() }) { - self.title = title - self.subtitle = subtitle - self.onBack = onBack - self.trailing = trailing - } - @Environment(\.dismiss) private var dismiss - var body: some View { - HStack { - Button { (onBack ?? { dismiss() })() } label: { - Image(systemName: "chevron.left").font(.system(size: 18)).foregroundColor(Color.zxF007) - .frame(width: 36, height: 36).background(Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 10)) - .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1)) - } - VStack(spacing: 1) { - Text(title).font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) - if let s = subtitle { Text(s).font(.system(size: 11)).foregroundColor(Color.zxF03) } - }.frame(maxWidth: .infinity) - trailing() - } - .padding(.horizontal, 16).padding(.top, ZXSpacing.statusBarH + 8).padding(.bottom, 12) - .background(Color.zxBg0) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXCardRow.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXCardRow.swift deleted file mode 100644 index 660821f..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXCardRow.swift +++ /dev/null @@ -1,24 +0,0 @@ -import SwiftUI - -// MARK: - Card Row - -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()) - } - .padding(14) - .background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)) - .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXChartView.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXChartView.swift deleted file mode 100644 index c861b50..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXChartView.swift +++ /dev/null @@ -1,36 +0,0 @@ -import SwiftUI - -// MARK: - Chart View - -struct ZXChartView: View { - let data: [(String, CGFloat)] = [ - ("一", 0.62), ("二", 0.65), ("三", 0.71), ("四", 0.68), - ("五", 0.75), ("六", 0.79), ("今", 0.78) - ] - var body: some View { - VStack(spacing: 0) { - GeometryReader { g in - ZStack(alignment: .topLeading) { - Path { path in - let w = g.size.width / 7 - for (i, d) in data.enumerated() { - let x = w * CGFloat(i) + w / 2 - let y = (1 - d.1) * g.size.height - if i == 0 { path.move(to: CGPoint(x: x, y: y)) } - else { path.addLine(to: CGPoint(x: x, y: y)) } - } - } - .stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) - } - } - .frame(height: 100) - HStack(spacing: 0) { - ForEach(data, id: \.0) { d in - Text(d.0).font(.system(size: 9)) - .foregroundColor(Color.zxF035) - .frame(maxWidth: .infinity) - } - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXChip.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXChip.swift deleted file mode 100644 index a14be5b..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXChip.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI - -// MARK: - Chip - -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()) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXEmptyView.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXEmptyView.swift deleted file mode 100644 index c53b864..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXEmptyView.swift +++ /dev/null @@ -1,60 +0,0 @@ -import SwiftUI - -// MARK: - Empty State - -struct ZXEmptyView: View { - let icon: String - let title: String - let subtitle: String? - let actionLabel: String? - let action: (() -> Void)? - - init( - icon: String = "tray", - title: String, - subtitle: String? = nil, - actionLabel: String? = nil, - action: (() -> Void)? = nil - ) { - self.icon = icon - self.title = title - self.subtitle = subtitle - self.actionLabel = actionLabel - self.action = action - } - - var body: some View { - VStack(spacing: 12) { - Image(systemName: icon) - .font(.system(size: 36)) - .foregroundColor(Color.zxF03) - - Text(title) - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color.zxF04) - - if let subtitle = subtitle { - Text(subtitle) - .font(.system(size: 12)) - .foregroundColor(Color.zxF03) - .multilineTextAlignment(.center) - } - - if let actionLabel = actionLabel, let action = action { - Button(action: action) { - Text(actionLabel) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(Color.zxPurple) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(Color.zxPurpleBG(0.1)) - .clipShape(Capsule()) - .overlay(Capsule().stroke(Color.zxPurple.opacity(0.3), lineWidth: 1)) - } - .padding(.top, 4) - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 48) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXErrorView.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXErrorView.swift deleted file mode 100644 index e90ee14..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXErrorView.swift +++ /dev/null @@ -1,74 +0,0 @@ -import SwiftUI - -// MARK: - Error Banner - -struct ZXErrorView: View { - let message: String - let onRetry: (() -> Void)? - - init(message: String, onRetry: (() -> Void)? = nil) { - self.message = message - self.onRetry = onRetry - } - - var body: some View { - VStack(spacing: 12) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 28)) - .foregroundColor(Color.zxYellow) - - Text(message) - .font(.system(size: 13)) - .foregroundColor(Color.zxF04) - .multilineTextAlignment(.center) - - if let onRetry = onRetry { - Button(action: onRetry) { - HStack(spacing: 6) { - Image(systemName: "arrow.clockwise") - .font(.system(size: 14)) - Text("重试") - .font(.system(size: 14, weight: .semibold)) - } - .foregroundColor(Color.zxPurple) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(Color.zxPurpleBG(0.1)) - .clipShape(Capsule()) - .overlay(Capsule().stroke(Color.zxPurple.opacity(0.3), lineWidth: 1)) - } - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 40) - } -} - -// MARK: - Inline Error Banner (compact, for use inside scroll views) - -struct ZXErrorBanner: View { - let message: String - let onDismiss: () -> Void - - var body: some View { - HStack(spacing: 10) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 14)) - .foregroundColor(Color.zxYellow) - Text(message) - .font(.system(size: 13)) - .foregroundColor(Color.zxF0) - Spacer() - Button(action: onDismiss) { - Image(systemName: "xmark") - .font(.system(size: 12)) - .foregroundColor(Color.zxF04) - } - } - .padding(.horizontal, 14) - .padding(.vertical, 12) - .background(Color.zxYellowBG(0.12)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxYellow.opacity(0.25), lineWidth: 1)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXIconBtn.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXIconBtn.swift deleted file mode 100644 index bbd03b0..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXIconBtn.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -// MARK: - Icon Button - -struct ZXIconBtn: View { - let icon: String; let size: CGFloat; var branded = false; var label: String?; let action: () -> Void - var body: some View { - Button(action: action) { - Image(systemName: icon).font(.system(size: size * 0.44)).frame(width: size, height: size) - } - .foregroundColor(branded ? .white : Color.zxF05) - .background(branded ? AnyView(ZXGradient.brand) : AnyView(Color.zxFill005)) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .overlay { if !branded { RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1) } } - .accessibilityLabel(label ?? icon.replacingOccurrences(of: ".", with: " ")) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXImportOption.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXImportOption.swift deleted file mode 100644 index d9a9c1f..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXImportOption.swift +++ /dev/null @@ -1,26 +0,0 @@ -import SwiftUI - -// MARK: - Import Option - -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) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXLoadingView.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXLoadingView.swift deleted file mode 100644 index 4181d6f..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXLoadingView.swift +++ /dev/null @@ -1,44 +0,0 @@ -import SwiftUI - -// MARK: - Loading Shimmer - -struct ZXLoadingView: View { - var body: some View { - VStack(spacing: 16) { - ProgressView() - .tint(Color.zxPurple) - Text("加载中…") - .font(.system(size: 13)) - .foregroundColor(Color.zxF04) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.zxBg0) - } -} - -// MARK: - Card Placeholder (skeleton shimmer) - -struct ZXCardPlaceholder: View { - var body: some View { - RoundedRectangle(cornerRadius: 14) - .fill(Color.zxFill004) - .overlay( - RoundedRectangle(cornerRadius: 14) - .stroke(Color.zxBorder006, lineWidth: 1) - ) - .frame(height: 72) - } -} - -// MARK: - Shimmer List - -struct ZXShimmerList: View { - let count: Int - var body: some View { - VStack(spacing: 12) { - ForEach(0.. Void - var body: some View { - Button(action: { withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) { action() } }) { - HStack(spacing: 12) { - Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle") - .font(.system(size: 20)) - .foregroundColor(task.isDone ? Color.zxGreen : Color.zxF02) - VStack(alignment: .leading, spacing: 4) { - Text(task.title) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(task.isDone ? Color.zxF04 : Color.zxF0) - .strikethrough(task.isDone) - HStack(spacing: 8) { - Text(task.taskType) - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(task.color) - .padding(.horizontal, 6).padding(.vertical, 1) - .background(task.color.opacity(0.12)).clipShape(Capsule()) - Text("约 \(task.minutes) 分钟") - .font(.system(size: 10)) - .foregroundColor(Color.zxF035) - } - } - Spacer() - if !task.isDone { - 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.isDone ? Color.zxFill003 : Color.zxFill005) - .overlay(RoundedRectangle(cornerRadius: 14).stroke( - task.isDone ? Color.zxFill005 : Color.zxBorder008, lineWidth: 1 - )) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .opacity(task.isDone ? 0.6 : 1) - } - .foregroundColor(.primary) - .accessibilityLabel("\(task.title),\(task.isDone ? "已完成" : "未完成")") - .accessibilityHint(task.isDone ? "双击取消完成" : "双击标记完成") - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXScoreBox.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXScoreBox.swift deleted file mode 100644 index bfa3f94..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXScoreBox.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI - -// MARK: - Score Box - -struct ZXScoreBox: View { - let score: Int; let bg: Color; let fg: Color - var body: some View { - Text("\(score)").font(.system(size: 12, weight: .heavy)).foregroundColor(fg) - .frame(width: 36, height: 36).background(bg).clipShape(RoundedRectangle(cornerRadius: 10)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXShimmerModifier.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXShimmerModifier.swift deleted file mode 100644 index a8c49ae..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXShimmerModifier.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -struct ZXShimmerModifier: ViewModifier { - @State private var phase: CGFloat = -1 - - let color: Color - let highlightColor: Color - - init(color: Color = Color.zxFill005, highlightColor: Color = Color.zxFill006) { - self.color = color - self.highlightColor = highlightColor - } - - func body(content: Content) -> some View { - content - .overlay { - GeometryReader { geo in - LinearGradient( - colors: [color, highlightColor, color], - startPoint: .leading, - endPoint: .trailing - ) - .frame(width: geo.size.width * 2) - .offset(x: phase * geo.size.width) - .animation( - .linear(duration: 1.5).repeatForever(autoreverses: false), - value: phase - ) - } - } - .clipped() - .onAppear { phase = 1 } - } -} - -extension View { - func shimmer(color: Color = Color.zxFill005, highlightColor: Color = Color.zxFill006) -> some View { - modifier(ZXShimmerModifier(color: color, highlightColor: highlightColor)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXStatBadge.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXStatBadge.swift deleted file mode 100644 index 7793d1d..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXStatBadge.swift +++ /dev/null @@ -1,18 +0,0 @@ -import SwiftUI - -// MARK: - Stat Badge - -struct ZXStatBadge: View { - let icon: String; let label: String; let value: String; let trend: String; let color: Color - var body: some View { - VStack(spacing: 3) { - Image(systemName: icon).font(.system(size: 14)).foregroundColor(color) - Text(value).font(.system(size: 16, weight: .heavy)).foregroundColor(Color.zxF0) - Text(label).font(.system(size: 9)).foregroundColor(Color.zxF04).multilineTextAlignment(.center) - } - .frame(maxWidth: .infinity).frame(height: 72).padding(.vertical, 4) - .background(color.opacity(0.06)) - .overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.15), lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 14)) - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXTabBar.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXTabBar.swift deleted file mode 100644 index 386be71..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXTabBar.swift +++ /dev/null @@ -1,45 +0,0 @@ -import SwiftUI - -// MARK: - Tab Bar - -struct ZXTabBar: View { - @Binding var active: String - private let tabs = [ - ("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(tabs, id: \.0) { item in - let on = item.0 == active - Button { withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { 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.zxF035) - } - Text(item.1) - .font(.system(size: 10, weight: on ? .semibold : .regular)) - .foregroundColor(on ? Color.zxPurple : Color.zxF035) - } - } - .accessibilityLabel("\(item.1)标签") - .accessibilityAddTraits(on ? .isSelected : []) - .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) - } - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXTypingIndicator.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXTypingIndicator.swift deleted file mode 100644 index 66ea3e1..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXTypingIndicator.swift +++ /dev/null @@ -1,26 +0,0 @@ -import SwiftUI - -struct ZXTypingIndicator: View { - @State private var phase = 0 - - var body: some View { - HStack(spacing: 4) { - ForEach(0..<3) { i in - Circle() - .fill(Color.zxPurple) - .frame(width: 8, height: 8) - .scaleEffect(phase == i ? 1 : 0.5) - .animation(.easeInOut(duration: 0.35).repeatForever(autoreverses: true), value: phase) - } - } - .padding(.horizontal, 14) - .padding(.vertical, 10) - .background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .onAppear { - Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in - phase = (phase + 1) % 3 - } - } - } -} diff --git a/AIStudyApp/AIStudyApp/Shared/Components/ZXWeakRow.swift b/AIStudyApp/AIStudyApp/Shared/Components/ZXWeakRow.swift deleted file mode 100644 index 801dac8..0000000 --- a/AIStudyApp/AIStudyApp/Shared/Components/ZXWeakRow.swift +++ /dev/null @@ -1,27 +0,0 @@ -import SwiftUI - -// MARK: - Weak Point Row - -struct ZXWeakRow: View { - let score: Int; let topic: String; let lib: String; let priority: String - var body: some View { - HStack(spacing: 12) { - Text("\(score)").font(.system(size: 13, weight: .heavy)).foregroundColor(Color.zxYellow) - .frame(width: 40, height: 40) - .background(Color.zxYellowBG(0.15)).clipShape(RoundedRectangle(cornerRadius: 12)) - VStack(alignment: .leading, spacing: 2) { - Text(topic).font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF0) - Text(lib).font(.system(size: 11)).foregroundColor(Color.zxF04) - }.frame(maxWidth: .infinity, alignment: .leading) - Text("\(priority)优先").font(.system(size: 11, weight: .bold)) - .foregroundColor(priority == "高" ? Color.zxRed : Color.zxYellow) - .padding(.horizontal, 8).padding(.vertical, 3) - .background((priority == "高" ? Color.zxRedBG(0.15) : Color.zxYellowBG(0.15))) - .clipShape(Capsule()) - } - .padding(.horizontal, 16).padding(.vertical, 12) - .background(Color.zxYellowBG(0.06)) - .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "#F59E0B", opacity: 0.15), lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: 14)) - } -}