refactor: 重构架构,接入线上 API 并修复编译错误

- 删除旧 ViewModel/Repository/Storage 层,简化架构
- 新增网络层:APIClient、APIConfig、APIError
- 新增数据模型层:APIModels(含 20+ DTO)
- 新增服务层:APIService(含 8 个业务服务类)
- 各页面内联组件定义,消除跨文件依赖
- 修复 ZXQuickAction、ZXAIInteractionRow、ZXWeakRow 缺失
- 修复 ZXTabBtn 参数标签缺失
- 配置 ATS 例外允许 HTTP 请求
- API 地址指向线上服务器 81.70.187.179:3001
This commit is contained in:
WangDL 2026-05-11 17:27:56 +08:00
parent 7066200b7b
commit fb95c27340
99 changed files with 985 additions and 4359 deletions

View File

@ -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;

View File

@ -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) } } }
}

View File

@ -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
}
}

View File

@ -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)}
}

View File

@ -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() {}
}

View File

@ -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)
}
}

View File

@ -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() }
}

View File

@ -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()
}
}

View File

@ -1,117 +0,0 @@
//
// ZXStrings.swift
// AIStudyApp
//
// SwiftUI Text
// SwiftUI Text("") LocalizedStringKey
// ViewModelServiceAlert 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: "其他")
}

View File

@ -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?
}

View File

@ -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?
}

View File

@ -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
}

View File

@ -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 }
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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?
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -1,75 +1,78 @@
//
// APIClient.swift - HTTP
//
import Foundation
protocol APIClientProtocol {
func request<T: Decodable>(_ 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<T: Decodable>(_ 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<T: Decodable>(
_ 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<T: Decodable>(_ 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) }
}

View File

@ -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)")!
}
}

View File

@ -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"
}

View File

@ -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
}
}
}

View File

@ -1,52 +0,0 @@
import Foundation
// MARK: - File Cache Protocol
protocol FileCacheProtocol {
func load<T: Codable>(_ type: T.Type, forKey key: String) throws -> T?
func save<T: Codable>(_ 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<T: Codable>(_ 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<T: Codable>(_ 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")
}
}

View File

@ -1,70 +0,0 @@
import Foundation
// MARK: - Knowledge Repository
final class KnowledgeRepository {
let bases: BaseRepository<KnowledgeBase>
let paths: PathRepository
let lessons: LessonRepository
init(knowledgeService: KnowledgeServiceProtocol) {
self.bases = BaseRepository<KnowledgeBase>(
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<T: Codable>: Codable {
let value: T
let timestamp: Date
}

View File

@ -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<Item: Codable & Identifiable>: 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<T: Codable>: Codable {
let value: T
let timestamp: Date
}

View File

@ -1,47 +0,0 @@
import Foundation
// MARK: - Review Repository
final class ReviewRepository {
let today: BaseRepository<ReviewTask>
let tomorrow: BaseRepository<ReviewTask>
let week: BaseRepository<ReviewTask>
let all: BaseRepository<ReviewTask>
init(reviewService: ReviewServiceProtocol) {
self.today = BaseRepository<ReviewTask>(
cacheKey: "reviews_today",
ttl: 120
) {
try await reviewService.fetchTodayReviews()
}
self.tomorrow = BaseRepository<ReviewTask>(
cacheKey: "reviews_tomorrow",
ttl: 300
) {
try await reviewService.fetchTomorrowReviews()
}
self.week = BaseRepository<ReviewTask>(
cacheKey: "reviews_week",
ttl: 600
) {
try await reviewService.fetchWeekReviews()
}
self.all = BaseRepository<ReviewTask>(
cacheKey: "reviews_all",
ttl: 300
) {
try await reviewService.fetchReviews()
}
}
func clearAll() throws {
try today.clearCache()
try tomorrow.clearCache()
try week.clearCache()
try all.clearCache()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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<ASAuthorizationAppleIDCredential, Error>?
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
}
}
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}
}

View File

@ -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])
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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)} }

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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) } }
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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"
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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) }
}

View File

@ -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<String> = ["间隔回忆", "费曼技巧"]
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)
}
}

View File

@ -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)
}
}
}
}
}
}

View File

@ -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 }
}
}

View File

@ -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) }
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}

View File

@ -1,32 +0,0 @@
import SwiftUI
// MARK: - Back Header
struct ZXBackHeader<T: View>: 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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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: " "))
}
}

View File

@ -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)
}
}

View File

@ -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..<count, id: \.self) { _ in
ZXCardPlaceholder()
}
}
}
}

View File

@ -1,20 +0,0 @@
import SwiftUI
// MARK: - Outline Button
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))
}
}
}

View File

@ -1,22 +0,0 @@
import SwiftUI
// MARK: - Profile Menu Row
struct ZXProfileMenuRow: View {
let emoji: String; let title: String; let desc: String
var body: some View {
HStack(spacing: 12) {
Text(emoji).font(.system(size: 20)).frame(width: 36, height: 36)
.background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03)
}
Spacer()
Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03)
}
.padding(.horizontal, 16).padding(.vertical, 14)
.accessibilityLabel("\(title)\(desc)")
.accessibilityAddTraits(.isButton)
}
}

View File

@ -1,14 +0,0 @@
import SwiftUI
// MARK: - Profile Stat
struct ZXProfileStat: View {
let value: String; let label: String; let color: Color
var body: some View {
VStack(spacing: 2) {
Text(value).font(.system(size: 18, weight: .bold)).foregroundColor(color)
Text(label).font(.system(size: 11)).foregroundColor(Color.zxF04)
}
.frame(maxWidth: .infinity)
}
}

View File

@ -1,18 +0,0 @@
import SwiftUI
// MARK: - Quick Action
struct ZXQuickAction: View {
let emoji: String; let label: String
var body: some View {
VStack(spacing: 6) {
Text(emoji).font(.system(size: 20))
Text(label).font(.system(size: 10, weight: .semibold))
.foregroundColor(Color.zxF006).multilineTextAlignment(.center).lineSpacing(4)
}
.frame(maxWidth: .infinity).frame(height: 72)
.background(Color.zxFill003)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}

View File

@ -1,47 +0,0 @@
import SwiftUI
struct ZXSTaskRow: View {
let task: StudyTaskEntity; var action: () -> 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 ? "双击取消完成" : "双击标记完成")
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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))
}
}