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