refactor(ios): migrate NavigationLink to value-based routing, add Dynamic Type support, fix gesture conflicts
- Replace all deprecated NavigationLink(destination:) with NavigationLink(value: Route) - Add Route enum with navigationDestination mapping in new Core/Navigation/ - Extract 7 new sub-page files (AIChatPage, AIFeedbackPageView, RecallTestPage, WeakPointsPage, FeedbackFormView, GoalSettingDetailView, MethodPreferenceView) - Add @ScaledMetric-based zxFontScaled modifier for Dynamic Type - Fix ZXPressModifier gesture conflict with ScrollView using onLongPressGesture - Enlarge touch targets from 36pt to 44pt - Add accessibility labels to TextField and other controls Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9a4b4afaf4
commit
a05dd09902
@ -112,9 +112,9 @@ struct WelcomePage: View {
|
||||
HStack(spacing: 6) { Image(systemName: "sparkles").font(.system(size: 12)); Text("AI 驱动").font(.system(size: 12, weight: .semibold)) }.foregroundColor(Color.zxAccent).padding(.horizontal, 12).padding(.vertical, 6).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(Capsule())
|
||||
Text("用 AI 重新定义\n你的学习方式").font(.system(size: 32, weight: .heavy)).tracking(-0.8).lineSpacing(4)
|
||||
VStack(spacing: 10) {
|
||||
FeatureRow(icon: "🧠", title: "主动回忆", desc: "基于间隔重复的智能复习")
|
||||
FeatureRow(icon: "🎤", title: "费曼解释", desc: "用自己的话讲出来")
|
||||
FeatureRow(icon: "📊", title: "AI 分析", desc: "发现知识薄弱点")
|
||||
FeatureRow(icon: "brain.head.profile", title: "主动回忆", desc: "基于间隔重复的智能复习")
|
||||
FeatureRow(icon: "mic.fill", title: "费曼解释", desc: "用自己的话讲出来")
|
||||
FeatureRow(icon: "chart.bar.fill", title: "AI 分析", desc: "发现知识薄弱点")
|
||||
}
|
||||
}
|
||||
VStack(spacing: 12) {
|
||||
@ -131,7 +131,7 @@ struct FeatureRow: View {
|
||||
let icon: String; let title: String; let desc: String
|
||||
var body: some View {
|
||||
HStack(spacing: 14) {
|
||||
Text(icon).font(.system(size: 20)).frame(width: 40, height: 40).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
Image(systemName: icon).font(.system(size: 18)).foregroundColor(Color.zxPurple).frame(width: 40, height: 40).background(Color(hex: "#7C6EFA", opacity: 0.1)).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }
|
||||
}.padding(.horizontal, 16).padding(.vertical, 14).background(Color.zxFill003).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
|
||||
@ -23,10 +23,10 @@ struct ZXTabBar: View {
|
||||
@Binding var active: String
|
||||
private let items = [
|
||||
("ai", "AI", "brain.head.profile"),
|
||||
("library", "知识库", "books.vertical.fill"),
|
||||
("study", "学习", "bolt.fill"),
|
||||
("analysis", "分析", "chart.bar.fill"),
|
||||
("profile", "我的", "person.fill"),
|
||||
("library", "知识库", "books.vertical"),
|
||||
("study", "学习", "bolt"),
|
||||
("analysis", "分析", "chart.bar"),
|
||||
("profile", "我的", "person"),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
@ -37,15 +37,15 @@ struct ZXTabBar: View {
|
||||
active = item.0
|
||||
} label: {
|
||||
VStack(spacing: 4) {
|
||||
ZStack {
|
||||
ZStack(alignment: .top) {
|
||||
if on {
|
||||
Circle()
|
||||
.fill(Color.zxPurple.opacity(0.2))
|
||||
.frame(width: 28, height: 28)
|
||||
.scaleEffect(1.4)
|
||||
Capsule()
|
||||
.fill(Color.zxPurple)
|
||||
.frame(width: 20, height: 3)
|
||||
.offset(y: -4)
|
||||
}
|
||||
Image(systemName: item.2)
|
||||
.font(.system(size: 22, weight: on ? .semibold : .regular))
|
||||
Image(systemName: on ? "\(item.2).fill" : item.2)
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(on ? Color.zxPurple : Color.zxF03)
|
||||
}
|
||||
Text(item.1)
|
||||
@ -139,6 +139,5 @@ struct ZXAIInputBar: View {
|
||||
.background(.ultraThinMaterial).background(Color.zxFill004)
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.padding(.horizontal, 20).padding(.bottom, 34)
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,14 +52,14 @@ extension Color {
|
||||
|
||||
// ── 文字 ──
|
||||
static let zxF0 = Color(light: Color(hex: "#1A1A2E"), dark: Color(hex: "#F0F0FF"))
|
||||
static let zxF05 = Color(light: Color(hex: "#1A1A2E", opacity: 0.5), dark: Color(hex: "#F0F0FF", opacity: 0.5))
|
||||
static let zxF04 = Color(light: Color(hex: "#1A1A2E", opacity: 0.4), dark: Color(hex: "#F0F0FF", opacity: 0.4))
|
||||
static let zxF03 = Color(light: Color(hex: "#1A1A2E", opacity: 0.3), dark: Color(hex: "#F0F0FF", opacity: 0.3))
|
||||
static let zxF05 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.5))
|
||||
static let zxF04 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.4))
|
||||
static let zxF03 = Color(light: Color(hex: "#64748B"), dark: Color(hex: "#F0F0FF", opacity: 0.3))
|
||||
static let zxF007 = Color(light: Color(hex: "#1A1A2E", opacity: 0.7), dark: Color(hex: "#F0F0FF", opacity: 0.7))
|
||||
static let zxF006 = Color(light: Color(hex: "#1A1A2E", opacity: 0.6), dark: Color(hex: "#F0F0FF", opacity: 0.6))
|
||||
static let zxF0045 = Color(light: Color(hex: "#1A1A2E", opacity: 0.45), dark: Color(hex: "#F0F0FF", opacity: 0.45))
|
||||
static let zxF035 = Color(light: Color(hex: "#1A1A2E", opacity: 0.35), dark: Color(hex: "#F0F0FF", opacity: 0.35))
|
||||
static let zxF02 = Color(light: Color(hex: "#1A1A2E", opacity: 0.2), dark: Color(hex: "#F0F0FF", opacity: 0.2))
|
||||
static let zxF006 = Color(light: Color(hex: "#334155"), dark: Color(hex: "#F0F0FF", opacity: 0.6))
|
||||
static let zxF0045 = Color(light: Color(hex: "#475569"), dark: Color(hex: "#F0F0FF", opacity: 0.45))
|
||||
static let zxF035 = Color(light: Color(hex: "#586A82"), dark: Color(hex: "#F0F0FF", opacity: 0.35))
|
||||
static let zxF02 = Color(light: Color(hex: "#64748B"), dark: Color(hex: "#F0F0FF", opacity: 0.2))
|
||||
|
||||
// ── 品牌色 ──
|
||||
static let zxPurple = Color(hex: "#7C6EFA")
|
||||
@ -72,9 +72,9 @@ extension Color {
|
||||
static let zxCyan = Color(hex: "#4ECDC4")
|
||||
|
||||
// ── 边框/分割线 ──
|
||||
static let zxBorder008 = Color(light: Color(hex: "#000000", opacity: 0.08), dark: Color(hex: "#FFFFFF", opacity: 0.08))
|
||||
static let zxBorder006 = Color(light: Color(hex: "#000000", opacity: 0.06), dark: Color(hex: "#FFFFFF", opacity: 0.06))
|
||||
static let zxBorder004 = Color(light: Color(hex: "#000000", opacity: 0.04), dark: Color(hex: "#FFFFFF", opacity: 0.04))
|
||||
static let zxBorder008 = Color(light: Color(hex: "#000000", opacity: 0.12), dark: Color(hex: "#FFFFFF", opacity: 0.08))
|
||||
static let zxBorder006 = Color(light: Color(hex: "#000000", opacity: 0.10), dark: Color(hex: "#FFFFFF", opacity: 0.06))
|
||||
static let zxBorder004 = Color(light: Color(hex: "#000000", opacity: 0.08), dark: Color(hex: "#FFFFFF", opacity: 0.04))
|
||||
static let zxBorder01 = Color(light: Color(hex: "#000000", opacity: 0.10), dark: Color(hex: "#FFFFFF", opacity: 0.10))
|
||||
static let zxBorder015 = Color(light: Color(hex: "#000000", opacity: 0.15), dark: Color(hex: "#FFFFFF", opacity: 0.15))
|
||||
|
||||
@ -273,3 +273,25 @@ enum ZXFont {
|
||||
// description: 12, regular, 0.4
|
||||
static let description = (size: CGFloat(12), weight: Font.Weight.regular, spacing: CGFloat(0.4))
|
||||
}
|
||||
|
||||
// MARK: - Dynamic Type Scaled Font
|
||||
|
||||
struct ZXScaledFont: ViewModifier {
|
||||
@ScaledMetric var size: CGFloat
|
||||
let weight: Font.Weight
|
||||
|
||||
init(size: CGFloat, weight: Font.Weight = .regular) {
|
||||
_size = ScaledMetric(wrappedValue: size, relativeTo: .body)
|
||||
self.weight = weight
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.font(.system(size: size, weight: weight))
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func zxFontScaled(size: CGFloat, weight: Font.Weight = .regular) -> some View {
|
||||
modifier(ZXScaledFont(size: size, weight: weight))
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,9 @@ struct ZXPressModifier: ViewModifier {
|
||||
.scaleEffect(pressed ? 0.96 : 1.0)
|
||||
.opacity(pressed ? 0.8 : 1.0)
|
||||
.animation(.easeOut(duration: 0.12), value: pressed)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in pressed = true }
|
||||
.onEnded { _ in pressed = false }
|
||||
)
|
||||
.onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in
|
||||
pressed = pressing
|
||||
}, perform: {})
|
||||
.sensoryFeedback(.impact(weight: .light), trigger: pressed)
|
||||
}
|
||||
}
|
||||
@ -72,6 +70,7 @@ struct ZXThinkingOverlay: View {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var show = false
|
||||
|
||||
var body: some View {
|
||||
@ -79,7 +78,6 @@ struct ZXThinkingOverlay: View {
|
||||
Color.black.opacity(0.4).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 20) {
|
||||
// Animated brain
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(RadialGradient(
|
||||
@ -87,8 +85,8 @@ struct ZXThinkingOverlay: View {
|
||||
center: .center, startRadius: 8, endRadius: 32
|
||||
))
|
||||
.frame(width: 64, height: 64)
|
||||
.scaleEffect(show ? 1.3 : 0.8)
|
||||
.animation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: show)
|
||||
.scaleEffect(show && !reduceMotion ? 1.3 : 1.0)
|
||||
.animation(reduceMotion ? nil : .easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: show)
|
||||
|
||||
Image(systemName: "brain.head.profile")
|
||||
.font(.system(size: 28))
|
||||
@ -99,7 +97,8 @@ struct ZXThinkingOverlay: View {
|
||||
Text(message)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
ZXDotLoader(color: .white)
|
||||
if !reduceMotion { ZXDotLoader(color: .white) }
|
||||
else { Text("处理中…").font(.system(size: 12)).foregroundColor(.white.opacity(0.7)) }
|
||||
}
|
||||
}
|
||||
.padding(32)
|
||||
@ -119,6 +118,7 @@ struct ZXCelebrationView: View {
|
||||
let subtitle: String
|
||||
let onDismiss: () -> Void
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var particles: [ConfettiParticle] = []
|
||||
@State private var showContent = false
|
||||
|
||||
@ -127,7 +127,7 @@ struct ZXCelebrationView: View {
|
||||
Color.black.opacity(0.5).ignoresSafeArea()
|
||||
.onTapGesture { dismiss() }
|
||||
|
||||
// Particles
|
||||
if !reduceMotion {
|
||||
ForEach(particles) { p in
|
||||
Circle()
|
||||
.fill(p.color)
|
||||
@ -136,6 +136,7 @@ struct ZXCelebrationView: View {
|
||||
.opacity(p.opacity)
|
||||
.scaleEffect(p.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// Content card
|
||||
VStack(spacing: 20) {
|
||||
@ -152,8 +153,8 @@ struct ZXCelebrationView: View {
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.scaleEffect(showContent ? 1 : 0.5)
|
||||
.animation(.spring(response: 0.5, dampingFraction: 0.6).delay(0.2), value: showContent)
|
||||
.scaleEffect(showContent && !reduceMotion ? 1 : 1)
|
||||
.animation(reduceMotion ? nil : .spring(response: 0.5, dampingFraction: 0.6).delay(0.2), value: showContent)
|
||||
|
||||
VStack(spacing: 6) {
|
||||
Text(title)
|
||||
@ -163,8 +164,8 @@ struct ZXCelebrationView: View {
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color(hex: "#F0F0FF", opacity: 0.6))
|
||||
}
|
||||
.opacity(showContent ? 1 : 0)
|
||||
.offset(y: showContent ? 0 : 20)
|
||||
.opacity(reduceMotion ? 1 : (showContent ? 1 : 0))
|
||||
.offset(y: reduceMotion ? 0 : (showContent ? 0 : 20))
|
||||
|
||||
Button(action: dismiss) {
|
||||
Text("继续学习")
|
||||
@ -180,7 +181,7 @@ struct ZXCelebrationView: View {
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.opacity(showContent ? 1 : 0)
|
||||
.opacity(reduceMotion ? 1 : (showContent ? 1 : 0))
|
||||
}
|
||||
.padding(28)
|
||||
.background(
|
||||
@ -191,7 +192,7 @@ struct ZXCelebrationView: View {
|
||||
}
|
||||
.onAppear {
|
||||
showContent = true
|
||||
launchConfetti()
|
||||
if !reduceMotion { launchConfetti() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,7 +208,7 @@ struct ZXCelebrationView: View {
|
||||
Color(hex: "#A78BFA"), Color(hex: "#34D399"),
|
||||
Color(hex: "#F59E0B"), Color(hex: "#4ECDC4")]
|
||||
var ps: [ConfettiParticle] = []
|
||||
for i in 0..<60 {
|
||||
for i in 0..<36 {
|
||||
let delay = Double(i) * 0.015
|
||||
let x = CGFloat.random(in: 0...UIScreen.main.bounds.width)
|
||||
let endY = CGFloat.random(in: 80...UIScreen.main.bounds.height * 0.7)
|
||||
@ -249,14 +250,19 @@ private struct ConfettiParticle: Identifiable {
|
||||
|
||||
struct ZXAIAnalysisProgress: View {
|
||||
let steps: [String]
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var currentStep = 0
|
||||
@State private var progress: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
ZStack {
|
||||
if reduceMotion {
|
||||
ProgressView().scaleEffect(1.5)
|
||||
} else {
|
||||
ZXLoadingView(size: 48, lineWidth: 3)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("AI 分析中…")
|
||||
@ -288,6 +294,10 @@ struct ZXAIAnalysisProgress: View {
|
||||
)
|
||||
.padding(.horizontal, 40)
|
||||
.onAppear {
|
||||
if reduceMotion {
|
||||
currentStep = steps.count - 1
|
||||
progress = 1.0
|
||||
} else {
|
||||
var delay: TimeInterval = 0.8
|
||||
for i in 0..<steps.count {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
@ -299,6 +309,7 @@ struct ZXAIAnalysisProgress: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
subscript(safe index: Int) -> Element? {
|
||||
|
||||
@ -75,16 +75,22 @@ struct ZXLoadingOverlay: View {
|
||||
// MARK: - Skeleton shimmer (for placeholder loading)
|
||||
|
||||
struct ZXShimmer: ViewModifier {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var phase: CGFloat = -0.5
|
||||
|
||||
private var shimmerColor: Color {
|
||||
colorScheme == .dark ? Color.white.opacity(0.06) : Color.black.opacity(0.06)
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0),
|
||||
Color.white.opacity(0.06),
|
||||
Color.white.opacity(0),
|
||||
shimmerColor.opacity(0),
|
||||
shimmerColor,
|
||||
shimmerColor.opacity(0),
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
@ -92,10 +98,10 @@ struct ZXShimmer: ViewModifier {
|
||||
.rotationEffect(.degrees(15))
|
||||
.scaleEffect(2)
|
||||
.offset(x: phase * 400)
|
||||
.animation(.linear(duration: 1.5).repeatForever(autoreverses: false), value: phase)
|
||||
.animation(reduceMotion ? nil : .linear(duration: 1.5).repeatForever(autoreverses: false), value: phase)
|
||||
)
|
||||
.clipped()
|
||||
.onAppear { phase = 1.5 }
|
||||
.onAppear { phase = reduceMotion ? 0 : 1.5 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -176,7 +176,7 @@ struct CreateKnowledgeBaseRequest: Codable {
|
||||
|
||||
// MARK: - Knowledge Items (matches Prisma KnowledgeItem model)
|
||||
|
||||
struct KnowledgeItem: Codable, Identifiable {
|
||||
struct KnowledgeItem: Codable, Identifiable, Hashable {
|
||||
let id: String
|
||||
let userId: String?
|
||||
let knowledgeBaseId: String?
|
||||
|
||||
65
AIStudyApp/AIStudyApp/Core/Navigation/Route.swift
Normal file
65
AIStudyApp/AIStudyApp/Core/Navigation/Route.swift
Normal file
@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
|
||||
enum Route: Hashable {
|
||||
// AI
|
||||
case aiChat
|
||||
case dailyThinking
|
||||
case aiFeedback
|
||||
case activeRecall
|
||||
case weakPoints
|
||||
case reviewCard
|
||||
|
||||
// Library
|
||||
case librarySearch
|
||||
case libraryDetail(knowledgeBaseId: String)
|
||||
case libraryImport
|
||||
case libraryCreate
|
||||
case addKnowledge(knowledgeBaseId: String)
|
||||
case knowledgeDetail(item: KnowledgeItem)
|
||||
case editKnowledge(item: KnowledgeItem)
|
||||
|
||||
// Study
|
||||
case learningSession(taskTitle: String, taskType: String, taskColorHex: String)
|
||||
case studyHome
|
||||
|
||||
// Profile
|
||||
case notificationList
|
||||
case settings
|
||||
case goalSetting
|
||||
case methodPreference
|
||||
case feedbackForm
|
||||
case editProfile
|
||||
}
|
||||
|
||||
extension Route {
|
||||
@ViewBuilder
|
||||
var destination: some View {
|
||||
switch self {
|
||||
case .aiChat: AIChatPage()
|
||||
case .dailyThinking: DailyThinkingPage()
|
||||
case .aiFeedback: AIFeedbackPageView()
|
||||
case .activeRecall: ActiveRecallView()
|
||||
case .weakPoints: WeakPointsPage()
|
||||
case .reviewCard: ReviewCardView()
|
||||
|
||||
case .librarySearch: LibrarySearchView()
|
||||
case .libraryDetail(let id): LibraryDetailPage(knowledgeBaseId: id)
|
||||
case .libraryImport: ImportPage()
|
||||
case .libraryCreate: CreateLibraryPage()
|
||||
case .addKnowledge(let id): AddKnowledgePage(knowledgeBaseId: id)
|
||||
case .knowledgeDetail(let item): KnowledgeDetailPage(item: item)
|
||||
case .editKnowledge(let item): EditKnowledgePage(item: item)
|
||||
|
||||
case .learningSession(let title, let type, let colorHex):
|
||||
LearningSessionView(taskTitle: title, taskType: type, taskColor: Color(hex: colorHex))
|
||||
case .studyHome: StudyHomeView()
|
||||
|
||||
case .notificationList: NotificationListView()
|
||||
case .settings: SettingsView()
|
||||
case .goalSetting: GoalSettingDetailView()
|
||||
case .methodPreference: MethodPreferenceView()
|
||||
case .feedbackForm: FeedbackFormView()
|
||||
case .editProfile: EditProfilePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
66
AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift
Normal file
66
AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift
Normal file
@ -0,0 +1,66 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AIChatPage: View {
|
||||
@StateObject private var vm = AIChatViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(vm.messages) { m in
|
||||
chatBubble(m)
|
||||
.id(m.id)
|
||||
}
|
||||
if vm.isSending {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
ZXDotLoader(color: Color.zxPurple)
|
||||
.padding(.leading, 4)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.onChange(of: vm.messages.count) { _ in
|
||||
withAnimation { proxy.scrollTo(vm.messages.last?.id) }
|
||||
}
|
||||
}
|
||||
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() })
|
||||
.padding(.horizontal, 20).padding(.bottom, 34)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
|
||||
private func chatBubble(_ m: AIMessage) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
if m.role == .ai {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Text(m.content).zxFontScaled(size: 14)
|
||||
.foregroundColor(m.role == .user ? .white : Color.zxF007)
|
||||
.padding(12)
|
||||
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
if m.role == .user {
|
||||
Circle().frame(width: 28, height: 28)
|
||||
.foregroundColor(Color.zxPurpleBG(0.2))
|
||||
.overlay(Text("我").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
|
||||
}
|
||||
}
|
||||
107
AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPageView.swift
Normal file
107
AIStudyApp/AIStudyApp/Features/AI/AIFeedbackPageView.swift
Normal file
@ -0,0 +1,107 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AIFeedbackPageView: View {
|
||||
@State private var navigateToChat = false
|
||||
@State private var isAnalyzing = true
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
if isAnalyzing {
|
||||
ZXAIAnalysisProgress(steps: [
|
||||
"解析你的回答结构…",
|
||||
"对比知识库标准答案…",
|
||||
"评估概念理解深度…",
|
||||
"生成个性化反馈…"
|
||||
])
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
withAnimation(.easeOut(duration: 0.4)) { isAnalyzing = false }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 20) {
|
||||
ZStack {
|
||||
Circle().trim(from: 0, to: 0.78).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 10, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 80, height: 80)
|
||||
VStack(spacing: 0) {
|
||||
Text("78").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxPurple)
|
||||
Text("/ 100").font(.system(size: 9)).foregroundColor(Color.zxF04)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("良好掌握").zxFontScaled(size: 18, weight: .heavy).foregroundColor(Color.zxF0)
|
||||
Text("理解核心概念,但缺少理论深度和解决方案").zxFontScaled(size: 12).foregroundColor(Color.zxF0045).lineSpacing(4)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(20)
|
||||
.background(ZXGradient.feedbackScore)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("你的回答").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF04)
|
||||
Text("过拟合就像一个学生只会「死记硬背」考题,而不是真正理解知识…").zxFontScaled(size: 13).foregroundColor(Color.zxF007).lineSpacing(6).padding(14).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.circle.fill").foregroundColor(Color.zxGreen)
|
||||
Text("答对的部分").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
}
|
||||
ForEach(["正确识别出过拟合是\"记住训练数据\"而非\"学习规律\"", "使用了死记硬背类比,方向正确且贴切"], id: \.self) { s in
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Circle().fill(Color.zxGreen).frame(width: 6, height: 6).padding(.top, 6)
|
||||
Text(s).zxFontScaled(size: 13).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.75)).lineSpacing(4)
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(hex: "#34D399", opacity: 0.07))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color(hex: "#34D399", opacity: 0.18), lineWidth: 1))
|
||||
}
|
||||
}
|
||||
NavigationLink(value: Route.studyHome) {
|
||||
Label("加入待巩固,安排间隔复习", systemImage: "bolt.fill")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity).frame(height: 52)
|
||||
.background(ZXGradient.ctaPurple)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: Color(hex: "#7C6EFA", opacity: 0.3), radius: 24)
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
NavigationLink(value: Route.aiChat) {
|
||||
HStack(spacing: 4) {
|
||||
Text("深入提问").font(.system(size: 13))
|
||||
Image(systemName: "chevron.right").font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(Color.zxF05)
|
||||
.frame(maxWidth: .infinity).frame(height: 44)
|
||||
.background(Color.zxFill005)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
NavigationLink(value: Route.dailyThinking) {
|
||||
HStack(spacing: 4) {
|
||||
Text("再来一题").font(.system(size: 13))
|
||||
Image(systemName: "chevron.right").font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(Color.zxF05)
|
||||
.frame(maxWidth: .infinity).frame(height: 44)
|
||||
.background(Color.zxFill005)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
@ -31,7 +31,7 @@ struct AIHomeView: View {
|
||||
.fill(serverStatus == .online ? Color.zxGreen
|
||||
: serverStatus == .checking ? Color.zxYellow
|
||||
: Color.zxRed)
|
||||
.frame(width: 6, height: 6)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(serverStatus == .online ? serverMessage
|
||||
: serverStatus == .checking ? "检测中…"
|
||||
: "离线")
|
||||
@ -43,7 +43,7 @@ struct AIHomeView: View {
|
||||
.padding(.horizontal, 8).padding(.vertical, 4)
|
||||
.background(Color.zxFill005).clipShape(Capsule())
|
||||
|
||||
ZXIconBtn(icon:"arrow.clockwise",size:36){ Task { await checkServer() } }
|
||||
ZXIconBtn(icon:"arrow.clockwise",size:44){ Task { await checkServer() } }
|
||||
}
|
||||
.padding(.horizontal,20).padding(.top,ZXSpacing.statusBarH+16).padding(.bottom,12)
|
||||
|
||||
@ -63,6 +63,7 @@ struct AIHomeView: View {
|
||||
|
||||
NavigationLink(destination: AIChatPage(), isActive: $navigateToChat) { EmptyView() }
|
||||
}
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
.task { await checkServer() }
|
||||
}
|
||||
|
||||
@ -90,8 +91,8 @@ struct AIHomeView: View {
|
||||
.padding(.horizontal,8).padding(.vertical,2).background(Color(hex:"#F97316",opacity:0.2)).clipShape(Capsule())
|
||||
}
|
||||
Text("解释\"注意力机制\"在 Transformer 中的作用,不能使用搜索,用你自己的话说。")
|
||||
.font(.system(size:14,weight:.medium)).foregroundColor(Color.zxF0).lineSpacing(4)
|
||||
NavigationLink(destination: DailyThinkingPage()) {
|
||||
.zxFontScaled(size:14,weight:.medium).foregroundColor(Color.zxF0).lineSpacing(4)
|
||||
NavigationLink(value: Route.dailyThinking) {
|
||||
Text("开始回答").font(.system(size:13,weight:.bold)).foregroundColor(.white)
|
||||
.frame(maxWidth:.infinity).frame(height:42)
|
||||
.background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius:12))
|
||||
@ -106,17 +107,17 @@ struct AIHomeView: View {
|
||||
|
||||
private var quickActions: some View {
|
||||
HStack(spacing:12){
|
||||
NavigationLink(destination: ActiveRecallView()) {
|
||||
ZXQuickAction(emoji:"🧠",label:"生成\n回忆测试")
|
||||
NavigationLink(value: Route.activeRecall) {
|
||||
ZXQuickAction(icon:"brain.head.profile",label:"生成\n回忆测试")
|
||||
}.foregroundColor(.primary)
|
||||
NavigationLink(destination: WeakPointsPage()) {
|
||||
ZXQuickAction(emoji:"🔍",label:"分析\n薄弱点")
|
||||
NavigationLink(value: Route.weakPoints) {
|
||||
ZXQuickAction(icon:"magnifyingglass",label:"分析\n薄弱点")
|
||||
}.foregroundColor(.primary)
|
||||
NavigationLink(destination: AIChatPage()) {
|
||||
ZXQuickAction(emoji:"🎤",label:"费曼\n解释练习")
|
||||
NavigationLink(value: Route.aiChat) {
|
||||
ZXQuickAction(icon:"mic.fill",label:"费曼\n解释练习")
|
||||
}.foregroundColor(.primary)
|
||||
NavigationLink(destination: ReviewCardView()) {
|
||||
ZXQuickAction(emoji:"📅",label:"今日\n复习计划")
|
||||
NavigationLink(value: Route.reviewCard) {
|
||||
ZXQuickAction(icon:"calendar",label:"今日\n复习计划")
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
@ -127,22 +128,22 @@ struct AIHomeView: View {
|
||||
Text("最近 AI 互动").font(.system(size:14,weight:.bold)).foregroundColor(Color.zxF0)
|
||||
Spacer();Text("全部").font(.system(size:12)).foregroundColor(Color.zxPurple)
|
||||
}
|
||||
ZXAIInteractionRow(tag:"费曼复习",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxPurple,emoji:"🎤",
|
||||
ZXAIInteractionRow(tag:"费曼复习",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxPurple,icon:"mic.fill",
|
||||
title:"解释量子纠缠的核心概念",time:"2小时前",score:82){ navigateToChat = true }
|
||||
ZXAIInteractionRow(tag:"薄弱点",bg:Color(hex:"#F97316",opacity:0.15),fg:Color(hex:"#FBA574"),emoji:"⚠️",
|
||||
ZXAIInteractionRow(tag:"薄弱点",bg:Color(hex:"#F97316",opacity:0.15),fg:Color(hex:"#FBA574"),icon:"exclamationmark.triangle.fill",
|
||||
title:"混淆了协方差和相关系数",time:"昨天",score:56){ navigateToChat = true }
|
||||
ZXAIInteractionRow(tag:"回忆测试",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxAccent,emoji:"📝",
|
||||
ZXAIInteractionRow(tag:"回忆测试",bg:Color(hex:"#7C6EFA",opacity:0.15),fg:Color.zxAccent,icon:"doc.text.fill",
|
||||
title:"机器学习中的偏差-方差权衡",time:"2天前",score:91){ navigateToChat = true }
|
||||
}
|
||||
}
|
||||
|
||||
private var suggestionSection: some View {
|
||||
VStack(alignment:.leading,spacing:10){
|
||||
Text("💡 你可以问 AI").font(.system(size:12,weight:.semibold)).foregroundColor(Color.zxF04)
|
||||
(Text(Image(systemName:"lightbulb.fill")).foregroundColor(Color.zxYellow) + Text(" 你可以问 AI")).font(.system(size:12,weight:.semibold)).foregroundColor(Color.zxF04)
|
||||
ForEach(["\"帮我测试机器学习这章的掌握情况\"","\"我最近的薄弱知识点有哪些?\"","\"生成一份本周的复习计划\""],id:\.self){s in
|
||||
Button { text = s; navigateToChat = true } label: {
|
||||
HStack{
|
||||
Text(s).font(.system(size:12)).foregroundColor(Color(hex:"#F0F0FF",opacity:0.55)).lineSpacing(4)
|
||||
Text(s).zxFontScaled(size:12).foregroundColor(Color(hex:"#F0F0FF",opacity:0.55)).lineSpacing(4)
|
||||
Spacer()
|
||||
Image(systemName:"arrow.up").font(.system(size:12)).foregroundColor(Color(hex:"#7C6EFA",opacity:0.5))
|
||||
}
|
||||
@ -158,33 +159,19 @@ struct AIHomeView: View {
|
||||
}
|
||||
|
||||
private var inputBar: some View {
|
||||
HStack(spacing:10){
|
||||
Image(systemName:"sparkles").font(.system(size:16)).foregroundColor(Color.zxPurple)
|
||||
TextField("问 AI 任何学习问题…",text:$text).font(.system(size:14)).tint(Color.zxPurple)
|
||||
Spacer()
|
||||
Image(systemName:"mic.fill").font(.system(size:18)).foregroundColor(Color.zxF03)
|
||||
Button{ navigateToChat = true }label:{
|
||||
Image(systemName:"arrow.up").font(.system(size:14,weight:.bold)).foregroundColor(.white)
|
||||
.frame(width:30,height:30).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius:9))
|
||||
}
|
||||
.accessibilityLabel("发送消息,开始 AI 对话")
|
||||
}
|
||||
.padding(.horizontal,14).padding(.vertical,10)
|
||||
.background(.ultraThinMaterial).background(Color.zxFill004)
|
||||
.overlay(RoundedRectangle(cornerRadius:20).stroke(Color.zxBorder008,lineWidth:1))
|
||||
.clipShape(RoundedRectangle(cornerRadius:20))
|
||||
ZXAIInputBar(text: $text, onSend: { navigateToChat = true })
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, ZXSpacing.tabBarH + 20)
|
||||
}
|
||||
}
|
||||
|
||||
struct ZXQuickAction: View {
|
||||
let emoji: String
|
||||
let icon: String
|
||||
let label: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:6){
|
||||
Text(emoji).font(.system(size:22))
|
||||
Image(systemName:icon).font(.system(size:22)).foregroundColor(Color.zxPurple)
|
||||
Text(label).font(.system(size:10,weight:.medium)).foregroundColor(Color.zxF03)
|
||||
.multilineTextAlignment(.center).lineSpacing(2)
|
||||
}
|
||||
@ -198,7 +185,7 @@ struct ZXAIInteractionRow: View {
|
||||
let tag: String
|
||||
let bg: Color
|
||||
let fg: Color
|
||||
let emoji: String
|
||||
let icon: String
|
||||
let title: String
|
||||
let time: String
|
||||
let score: Int
|
||||
@ -207,7 +194,7 @@ struct ZXAIInteractionRow: View {
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing:12){
|
||||
Text(emoji).font(.system(size:16))
|
||||
Image(systemName:icon).font(.system(size:16)).foregroundColor(fg)
|
||||
.frame(width:36,height:36).background(bg).clipShape(RoundedRectangle(cornerRadius:10))
|
||||
VStack(alignment:.leading,spacing:4){
|
||||
HStack{
|
||||
|
||||
@ -6,190 +6,12 @@ struct DailyThinkingPage: View {
|
||||
ZStack { Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView { VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack { Image(systemName:"sparkles").foregroundColor(Color.zxAccent); Text("解释\"注意力机制\"在 Transformer 中的作用").font(.system(size:15,weight:.bold)).foregroundColor(Color.zxF0) }
|
||||
Text("AI会从三个方面评估你的回答:核心概念理解 · 理论深度 · 实际应用能力").font(.system(size:12)).foregroundColor(Color.zxF04)
|
||||
HStack { Image(systemName:"sparkles").foregroundColor(Color.zxAccent); Text("解释\"注意力机制\"在 Transformer 中的作用").zxFontScaled(size:15,weight:.bold).foregroundColor(Color.zxF0) }
|
||||
Text("AI会从三个方面评估你的回答:核心概念理解 · 理论深度 · 实际应用能力").zxFontScaled(size:12).foregroundColor(Color.zxF04)
|
||||
}.padding(16).background(ZXGradient.thinkingCard).clipShape(RoundedRectangle(cornerRadius:16))
|
||||
VStack(alignment:.leading,spacing:8){Text("你的回答").font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF04);TextEditor(text:$answer).font(.system(size:13)).foregroundColor(Color.zxF0).tint(Color.zxPurple).frame(minHeight:160).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1))}
|
||||
if !submitted{ NavigationLink(destination:AIFeedbackPageView()){ Text("提交回答,获取 AI 反馈").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16)).shadow(color:Color(hex:"#7C6EFA",opacity:0.3),radius:24) }.zxPressable() }
|
||||
VStack(alignment:.leading,spacing:8){Text("你的回答").font(.system(size:13,weight:.semibold)).foregroundColor(Color.zxF04);TextEditor(text:$answer).zxFontScaled(size:13).foregroundColor(Color.zxF0).tint(Color.zxPurple).frame(minHeight:160).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1))}
|
||||
if !submitted{ NavigationLink(value: Route.aiFeedback){ Text("提交回答,获取 AI 反馈").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16)).shadow(color:Color(hex:"#7C6EFA",opacity:0.3),radius:24) }.zxPressable() }
|
||||
}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,120) }.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)
|
||||
}
|
||||
}
|
||||
struct RecallTestPage: View { @State private var input = ""; var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();ScrollView{VStack(spacing:16){Text("请回忆并写下你对「偏差-方差权衡」的理解").font(.system(size:14)).foregroundColor(Color.zxF04);TextEditor(text:$input).frame(minHeight:200).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius:14)).overlay(RoundedRectangle(cornerRadius:14).stroke(Color.zxBorder008,lineWidth:1));NavigationLink(destination: AIFeedbackPageView()){Text("提交").font(.system(size:14,weight:.bold)).foregroundColor(.white).frame(maxWidth:.infinity).frame(height:52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius:16))}}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} }
|
||||
struct WeakPointsPage: View { var body: some View { ZStack{Color.zxBg0.ignoresSafeArea();ScrollView{VStack(spacing:12){
|
||||
ZXWeakRow(score:32,topic:"贝叶斯定理应用",lib:"机器学习",priority:"高")
|
||||
ZXWeakRow(score:41,topic:"正态分布性质",lib:"高等数学",priority:"高")
|
||||
ZXWeakRow(score:55,topic:"词根 spect- 相关词汇",lib:"英语词汇",priority:"中")
|
||||
ZXWeakRow(score:48,topic:"协方差与相关系数",lib:"机器学习",priority:"中")
|
||||
ZXWeakRow(score:36,topic:"梯度下降优化",lib:"机器学习",priority:"高")
|
||||
}.padding(.horizontal,20).padding(.top, 8).padding(.bottom,80)}.scrollIndicators(.hidden)}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden,for:.navigationBar)} }
|
||||
struct AIFeedbackPageView: View {
|
||||
@State private var navigateToChat = false
|
||||
@State private var isAnalyzing = true
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
if isAnalyzing {
|
||||
ZXAIAnalysisProgress(steps: [
|
||||
"解析你的回答结构…",
|
||||
"对比知识库标准答案…",
|
||||
"评估概念理解深度…",
|
||||
"生成个性化反馈…"
|
||||
])
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
withAnimation(.easeOut(duration: 0.4)) { isAnalyzing = false }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 20) {
|
||||
ZStack {
|
||||
Circle().trim(from: 0, to: 0.78).stroke(ZXGradient.brand, style: StrokeStyle(lineWidth: 10, lineCap: .round)).rotationEffect(.degrees(-90)).frame(width: 80, height: 80)
|
||||
VStack(spacing: 0) {
|
||||
Text("78").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxPurple)
|
||||
Text("/ 100").font(.system(size: 9)).foregroundColor(Color.zxF04)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("良好掌握").font(.system(size: 18, weight: .heavy)).foregroundColor(Color.zxF0)
|
||||
Text("理解核心概念,但缺少理论深度和解决方案").font(.system(size: 12)).foregroundColor(Color.zxF0045).lineSpacing(4)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(20)
|
||||
.background(ZXGradient.feedbackScore)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("你的回答").font(.system(size: 13, weight: .semibold)).foregroundColor(Color.zxF04)
|
||||
Text("过拟合就像一个学生只会「死记硬背」考题,而不是真正理解知识…").font(.system(size: 13)).foregroundColor(Color.zxF007).lineSpacing(6).padding(14).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.circle.fill").foregroundColor(Color.zxGreen)
|
||||
Text("答对的部分").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
}
|
||||
ForEach(["正确识别出过拟合是\"记住训练数据\"而非\"学习规律\"", "使用了死记硬背类比,方向正确且贴切"], id: \.self) { s in
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Circle().fill(Color.zxGreen).frame(width: 6, height: 6).padding(.top, 6)
|
||||
Text(s).font(.system(size: 13)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.75)).lineSpacing(4)
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(hex: "#34D399", opacity: 0.07))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color(hex: "#34D399", opacity: 0.18), lineWidth: 1))
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: StudyHomeView()) {
|
||||
Label("加入待巩固,安排间隔复习", systemImage: "bolt.fill")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity).frame(height: 52)
|
||||
.background(ZXGradient.ctaPurple)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: Color(hex: "#7C6EFA", opacity: 0.3), radius: 24)
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
NavigationLink(destination: AIChatPage()) {
|
||||
HStack(spacing: 4) {
|
||||
Text("深入提问").font(.system(size: 13))
|
||||
Image(systemName: "chevron.right").font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(Color.zxF05)
|
||||
.frame(maxWidth: .infinity).frame(height: 44)
|
||||
.background(Color.zxFill005)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
NavigationLink(destination: DailyThinkingPage()) {
|
||||
HStack(spacing: 4) {
|
||||
Text("再来一题").font(.system(size: 13))
|
||||
Image(systemName: "chevron.right").font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(Color.zxF05)
|
||||
.frame(maxWidth: .infinity).frame(height: 44)
|
||||
.background(Color.zxFill005)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
// MARK: - AI Chat
|
||||
|
||||
struct AIChatPage: View {
|
||||
@StateObject private var vm = AIChatViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(vm.messages) { m in
|
||||
chatBubble(m)
|
||||
.id(m.id)
|
||||
}
|
||||
if vm.isSending {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
ZXDotLoader(color: Color.zxPurple)
|
||||
.padding(.leading, 4)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.onChange(of: vm.messages.count) { _ in
|
||||
withAnimation { proxy.scrollTo(vm.messages.last?.id) }
|
||||
}
|
||||
}
|
||||
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() })
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
|
||||
private func chatBubble(_ m: AIMessage) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
if m.role == .ai {
|
||||
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Color(hex: "#7C6EFA", opacity: 0.15))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Text(m.content).font(.system(size: 14))
|
||||
.foregroundColor(m.role == .user ? .white : Color.zxF007)
|
||||
.padding(12)
|
||||
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
if m.role == .user {
|
||||
Circle().frame(width: 28, height: 28)
|
||||
.foregroundColor(Color.zxPurpleBG(0.2))
|
||||
.overlay(Text("我").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
|
||||
}
|
||||
}
|
||||
|
||||
38
AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift
Normal file
38
AIStudyApp/AIStudyApp/Features/AI/RecallTestPage.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RecallTestPage: View {
|
||||
@State private var input = ""
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
Text("请回忆并写下你对「偏差-方差权衡」的理解")
|
||||
.zxFontScaled(size: 14)
|
||||
.foregroundColor(Color.zxF04)
|
||||
TextEditor(text: $input)
|
||||
.frame(minHeight: 200)
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(12)
|
||||
.background(Color.zxFill004)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
NavigationLink(value: Route.aiFeedback) {
|
||||
Text("提交")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 52)
|
||||
.background(ZXGradient.ctaPurple)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
22
AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift
Normal file
22
AIStudyApp/AIStudyApp/Features/AI/WeakPointsPage.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WeakPointsPage: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 12) {
|
||||
ZXWeakRow(score: 32, topic: "贝叶斯定理应用", lib: "机器学习", priority: "高")
|
||||
ZXWeakRow(score: 41, topic: "正态分布性质", lib: "高等数学", priority: "高")
|
||||
ZXWeakRow(score: 55, topic: "词根 spect- 相关词汇", lib: "英语词汇", priority: "中")
|
||||
ZXWeakRow(score: 48, topic: "协方差与相关系数", lib: "机器学习", priority: "中")
|
||||
ZXWeakRow(score: 36, topic: "梯度下降优化", lib: "机器学习", priority: "高")
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ struct AnalysisHomeView: View {
|
||||
ZXChartView()
|
||||
}.padding(16).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill").font(.system(size: 14)).foregroundColor(Color.zxYellow); Text("薄弱知识点").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) }; Spacer(); NavigationLink(destination: WeakPointsPage()) { Text("全部 \(viewModel.focusItems.count) 个").font(.system(size: 12)).foregroundColor(Color.zxPurple) }.accessibilityLabel("查看全部薄弱知识点") }
|
||||
HStack { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill").font(.system(size: 14)).foregroundColor(Color.zxYellow); Text("薄弱知识点").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) }; Spacer(); NavigationLink(value: Route.weakPoints) { Text("全部 \(viewModel.focusItems.count) 个").font(.system(size: 12)).foregroundColor(Color.zxPurple) }.accessibilityLabel("查看全部薄弱知识点") }
|
||||
ForEach(viewModel.focusItems.prefix(5)) { item in
|
||||
ZXWeakRow(score: item.masteryScore ?? 0, topic: item.title, lib: item.knowledgeBaseId ?? "", priority: item.priority ?? "normal")
|
||||
}
|
||||
@ -45,6 +45,7 @@ struct AnalysisHomeView: View {
|
||||
}
|
||||
}
|
||||
.task { await viewModel.loadAll() }
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,6 +61,7 @@ struct ZXStatBadge: View { let icon: String; let label: String; let value: Strin
|
||||
|
||||
struct ZXChartView: View {
|
||||
let data: [(String, CGFloat)] = [("一", 0.62), ("二", 0.65), ("三", 0.71), ("四", 0.68), ("五", 0.75), ("六", 0.79), ("今", 0.78)]
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var showChart = false
|
||||
|
||||
var body: some View {
|
||||
@ -81,7 +83,7 @@ struct ZXChartView: View {
|
||||
)
|
||||
)
|
||||
.opacity(showChart ? 1 : 0)
|
||||
.animation(.easeOut(duration: 0.8).delay(0.3), value: showChart)
|
||||
.animation(reduceMotion ? nil : .easeOut(duration: 0.8).delay(0.3), value: showChart)
|
||||
|
||||
// Animated line
|
||||
Path { path in let w = g.size.width / 7
|
||||
@ -90,11 +92,12 @@ struct ZXChartView: View {
|
||||
}
|
||||
.trim(from: 0, to: showChart ? 1 : 0)
|
||||
.stroke(Color.zxPurple, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
|
||||
.animation(.easeOut(duration: 1.0), value: showChart)
|
||||
.animation(reduceMotion ? nil : .easeOut(duration: 1.0), value: showChart)
|
||||
}
|
||||
}.frame(height: 100)
|
||||
HStack(spacing: 0) { ForEach(data, id: \.0) { d in Text(d.0).font(.system(size: 9)).foregroundColor(Color(hex: "#F0F0FF", opacity: 0.35)).frame(maxWidth: .infinity) } }
|
||||
}
|
||||
.onAppear { showChart = true }
|
||||
.animation(reduceMotion ? nil : .default, value: showChart)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,14 @@ struct LibraryHomeView: View {
|
||||
ZStack { ZXGradient.page.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
HStack { Text("知识库").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5); Spacer()
|
||||
NavigationLink(destination: LibrarySearchView()) {
|
||||
NavigationLink(value: Route.librarySearch) {
|
||||
Image(systemName: "magnifyingglass").font(.system(size: 18)).foregroundColor(Color.zxF05)
|
||||
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
.accessibilityLabel("搜索知识库")
|
||||
NavigationLink(destination: ImportPage()) {
|
||||
NavigationLink(value: Route.libraryImport) {
|
||||
Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white)
|
||||
.frame(width: 36, height: 36).background(ZXGradient.brand)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
@ -35,8 +35,8 @@ struct LibraryHomeView: View {
|
||||
.frame(maxWidth: .infinity).padding(.top, 80)
|
||||
}
|
||||
ForEach(viewModel.knowledgeBases) { kb in
|
||||
NavigationLink(destination: LibraryDetailPage(knowledgeBaseId: kb.id)) {
|
||||
ZLibraryCard(emoji: "📚", name: kb.title, desc: kb.description ?? "", color: Color.zxPurple, items: kb.itemCount ?? 0, mastery: 50, tags: [], last: lastStudiedText(kb.lastStudiedAt))
|
||||
NavigationLink(value: Route.libraryDetail(knowledgeBaseId: kb.id)) {
|
||||
ZLibraryCard(icon: "books.vertical.fill", name: kb.title, desc: kb.description ?? "", color: Color.zxPurple, items: kb.itemCount ?? 0, mastery: 50, tags: [], last: lastStudiedText(kb.lastStudiedAt))
|
||||
}
|
||||
}
|
||||
if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading {
|
||||
@ -45,7 +45,7 @@ struct LibraryHomeView: View {
|
||||
if viewModel.hasMore {
|
||||
ZXLoadMoreFooter { await viewModel.loadMore() }
|
||||
}
|
||||
NavigationLink(destination: CreateLibraryPage()) {
|
||||
NavigationLink(value: Route.libraryCreate) {
|
||||
HStack(spacing: 8) { Image(systemName: "plus").font(.system(size: 16)); Text("创建新知识库").font(.system(size: 14, weight: .semibold)) }
|
||||
.foregroundColor(Color.zxF05).frame(maxWidth: .infinity).frame(height: 52).background(Color.zxFill003)
|
||||
.overlay(RoundedRectangle(cornerRadius: 16).strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [6, 4]), antialiased: true).foregroundColor(Color.zxBorder01))
|
||||
@ -59,6 +59,7 @@ struct LibraryHomeView: View {
|
||||
}
|
||||
}
|
||||
.task { await viewModel.loadKnowledgeBases() }
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
}
|
||||
|
||||
private func lastStudiedText(_ iso: String?) -> String {
|
||||
@ -66,8 +67,8 @@ struct LibraryHomeView: View {
|
||||
return iso.prefix(10).description
|
||||
}
|
||||
}
|
||||
struct ZLibraryCard: View { let emoji: String; let name: String; let desc: String; let color: Color; let items: Int; let mastery: Int; let tags: [String]; let last: String
|
||||
var body: some View { VStack(spacing: 0) { Rectangle().fill(ZXGradient.progressBar).frame(height: 3); HStack(spacing: 12) { Text(emoji).font(.system(size: 22)).frame(width: 44, height: 44).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 13)).overlay(RoundedRectangle(cornerRadius: 13).stroke(color.opacity(0.3), lineWidth: 1)); VStack(alignment: .leading, spacing: 2) { Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04); Text("掌握 \(mastery)%").font(.system(size: 11)).foregroundColor(Color.zxF04) }; Spacer() }.padding(16); HStack { HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03); Spacer(); ForEach(tags.prefix(2), id: \.self) { t in Text(t).font(.system(size: 10, weight: .medium)).foregroundColor(Color.zxPurple).padding(.horizontal, 7).padding(.vertical, 2).background(Color(hex: "#7C6EFA", opacity: 0.08)).clipShape(Capsule()) } }.padding(.horizontal, 16).padding(.bottom, 12) }
|
||||
struct ZLibraryCard: View { let icon: String; let name: String; let desc: String; let color: Color; let items: Int; let mastery: Int; let tags: [String]; let last: String
|
||||
var body: some View { VStack(spacing: 0) { Rectangle().fill(ZXGradient.progressBar).frame(height: 3); HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 20)).foregroundColor(color).frame(width: 44, height: 44).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 13)).overlay(RoundedRectangle(cornerRadius: 13).stroke(color.opacity(0.3), lineWidth: 1)); VStack(alignment: .leading, spacing: 2) { Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04); Text("掌握 \(mastery)%").font(.system(size: 11)).foregroundColor(Color.zxF04) }; Spacer() }.padding(16); HStack { HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03); Spacer(); ForEach(tags.prefix(2), id: \.self) { t in Text(t).font(.system(size: 10, weight: .medium)).foregroundColor(Color.zxPurple).padding(.horizontal, 7).padding(.vertical, 2).background(Color(hex: "#7C6EFA", opacity: 0.08)).clipShape(Capsule()) } }.padding(.horizontal, 16).padding(.bottom, 12) }
|
||||
.background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) }
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ struct LibraryDetailPage: View {
|
||||
var body: some View {
|
||||
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
|
||||
HStack { Spacer()
|
||||
NavigationLink(destination: AddKnowledgePage(knowledgeBaseId: knowledgeBaseId)) {
|
||||
NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) {
|
||||
Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white)
|
||||
.frame(width: 36, height: 36).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
@ -31,8 +31,8 @@ struct LibraryDetailPage: View {
|
||||
.frame(maxWidth: .infinity).padding(.top, 80)
|
||||
}
|
||||
ForEach(viewModel.items) { item in
|
||||
NavigationLink(destination: KnowledgeDetailPage(item: item)) {
|
||||
ZXCardRow(emoji: "📝", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen)
|
||||
NavigationLink(value: Route.knowledgeDetail(item: item)) {
|
||||
ZXCardRow(icon: "doc.text", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen)
|
||||
}
|
||||
}
|
||||
if viewModel.items.isEmpty && !viewModel.isLoading {
|
||||
@ -48,8 +48,8 @@ struct LibraryDetailPage: View {
|
||||
.task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) }
|
||||
}
|
||||
}
|
||||
struct ZXCardRow: View { let emoji: String; let title: String; let desc: String; let status: String; let c: Color
|
||||
var body: some View { HStack(spacing: 12) { Text(emoji).font(.system(size: 20)).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
|
||||
struct ZXCardRow: View { let icon: String; let title: String; let desc: String; let status: String; let c: Color
|
||||
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(c).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
|
||||
.padding(14).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1)) }
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ struct KnowledgeDetailPage: View {
|
||||
var body: some View {
|
||||
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
|
||||
HStack { Spacer()
|
||||
NavigationLink(destination: EditKnowledgePage(item: item)) {
|
||||
NavigationLink(value: Route.editKnowledge(item: item)) {
|
||||
Image(systemName: "pencil").font(.system(size: 16)).foregroundColor(Color.zxF05)
|
||||
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
@ -90,10 +90,10 @@ struct KnowledgeDetailPage: View {
|
||||
if let content = item.content { Text(content).font(.system(size: 14)).foregroundColor(Color.zxF007).lineSpacing(6) }
|
||||
}.padding(20).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
||||
HStack(spacing: 12) {
|
||||
NavigationLink(destination: StudyHomeView()) {
|
||||
NavigationLink(value: Route.studyHome) {
|
||||
Label("开始复习", systemImage: "arrow.triangle.2.circlepath").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 44).background(ZXGradient.brandPurple).clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
NavigationLink(destination: AIChatPage()) {
|
||||
NavigationLink(value: Route.aiChat) {
|
||||
Label("费曼解释", systemImage: "mic.fill").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF05).frame(maxWidth: .infinity).frame(height: 44).background(Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FeedbackFormView: View {
|
||||
@State private var type = "功能建议"
|
||||
@State private var content = ""
|
||||
@State private var submitted = false
|
||||
@State private var isSubmitting = false
|
||||
let types = ["Bug 反馈", "功能建议", "内容问题", "其他"]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("反馈类型").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
HStack(spacing: 8) { ForEach(types, id: \.self) { t in let sel = type == t; Button { type = t } label: { Text(t).font(.system(size: 12)).foregroundColor(sel ? .white : Color.zxF05).padding(.horizontal, 12).padding(.vertical, 6).background(sel ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill005)).clipShape(Capsule()) } } }
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
TextEditor(text: $content).frame(minHeight: 150).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
isSubmitting = true
|
||||
_ = try? await FeedbackService.shared.submit(category: type, content: content)
|
||||
submitted = true
|
||||
isSubmitting = false
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if isSubmitting { ProgressView().tint(.white) }
|
||||
Text(submitted ? "已提交" : "提交").font(.system(size: 14, weight: .bold))
|
||||
}
|
||||
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
|
||||
.background(isSubmitting ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.disabled(isSubmitting)
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GoalSettingDetailView: View {
|
||||
@State private var selectedGoal = "备考考试"
|
||||
let goals = [("person.crop.circle.fill","备考考试","公考、考研、考证等"),("briefcase.fill","职业技能","编程、设计、产品等"),("books.vertical.fill","通识学习","扩充知识面"),("target","自定义","设定自己的目标")]
|
||||
@State private var dailyMins = "30 分钟"
|
||||
let times = ["15 分钟","30 分钟","1 小时","不限制"]
|
||||
@State private var isSaving = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
ForEach(goals, id: \.1) { g in let sel = selectedGoal == g.1
|
||||
Button { selectedGoal = g.1 } label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: g.0).font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(width: 44, height: 44).background(sel ? Color(hex: "#7C6EFA", opacity: 0.15) : Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
VStack(alignment: .leading, spacing: 2) { Text(g.1).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0); Text(g.2).font(.system(size: 12)).foregroundColor(Color.zxF04) }
|
||||
Spacer()
|
||||
Circle().stroke(sel ? Color.zxPurple : Color(hex: "#FFFFFF", opacity: 0.2), lineWidth: 2).frame(width: 22, height: 22).overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } }
|
||||
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
HStack(spacing: 8) { ForEach(times, id: \.self) { t in let sel = dailyMins == t; Button { dailyMins = t } label: { Text(t).font(.system(size: 12)).fontWeight(sel ? .semibold : .regular).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(maxWidth: .infinity).frame(height: 40).background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12)) }.foregroundColor(.primary) } }
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
isSaving = true
|
||||
_ = try? await UserService.shared.updateProfileDetail(UpdateProfileDataRequest(
|
||||
learningIdentity: nil, learningDirection: nil, bio: nil, currentGoal: selectedGoal
|
||||
))
|
||||
isSaving = false
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if isSaving { ProgressView().tint(.white) }
|
||||
Text("保存").font(.system(size: 14, weight: .bold))
|
||||
}
|
||||
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
|
||||
.background(isSaving ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.disabled(isSaving)
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MethodPreferenceView: View {
|
||||
@State private var methods: Set<String> = ["间隔回忆", "费曼技巧"]
|
||||
let allMethods = ["间隔回忆", "费曼技巧", "AI 分析", "主动回忆"]
|
||||
@State private var saved = false
|
||||
@State private var isSaving = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("选择你偏好的学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
ForEach(allMethods, id: \.self) { m in let sel = methods.contains(m)
|
||||
Button { if sel { methods.remove(m) } else { methods.insert(m) } } label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: sel ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF02)
|
||||
Text(m).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0)
|
||||
Spacer()
|
||||
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
isSaving = true
|
||||
_ = try? await UserService.shared.updatePreferences(UpdatePreferencesRequest(
|
||||
preferredMethods: Array(methods), defaultFocusMinutes: nil,
|
||||
aiSuggestionLevel: nil, language: nil, appearance: nil,
|
||||
notificationEnabled: nil
|
||||
))
|
||||
saved = true
|
||||
isSaving = false
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if isSaving { ProgressView().tint(.white) }
|
||||
Text(saved ? "已保存" : "保存").font(.system(size: 14, weight: .bold))
|
||||
}
|
||||
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
|
||||
.background(isSaving ? AnyView(Color.gray) : AnyView(ZXGradient.ctaPurple))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.disabled(isSaving)
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
@ -11,14 +11,14 @@ struct ProfileView: View {
|
||||
HStack {
|
||||
Text("我的").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5)
|
||||
Spacer()
|
||||
NavigationLink(destination: NotificationListView()) {
|
||||
NavigationLink(value: Route.notificationList) {
|
||||
Image(systemName: "bell").font(.system(size: 18)).foregroundColor(Color.zxF05)
|
||||
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
.accessibilityLabel("通知中心")
|
||||
NavigationLink(destination: SettingsView()) {
|
||||
NavigationLink(value: Route.settings) {
|
||||
Image(systemName: "gearshape").font(.system(size: 18)).foregroundColor(Color.zxF05)
|
||||
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
@ -28,20 +28,20 @@ struct ProfileView: View {
|
||||
}.padding(.horizontal, 20).padding(.top, ZXSpacing.statusBarH + 16).padding(.bottom, 4)
|
||||
profileCard
|
||||
VStack(spacing: 0) {
|
||||
NavigationLink(destination: GoalSettingDetailView()) {
|
||||
ZXProfileMenuRow(emoji: "🎯", title: "学习目标设置", desc: "调整你的学习目标")
|
||||
NavigationLink(value: Route.goalSetting) {
|
||||
ZXProfileMenuRow(icon: "target", title: "学习目标设置", desc: "调整你的学习目标")
|
||||
}.foregroundColor(.primary)
|
||||
ZXProfileDivider()
|
||||
NavigationLink(destination: SettingsView()) {
|
||||
ZXProfileMenuRow(emoji: "🔔", title: "复习提醒", desc: "间隔复习通知设置")
|
||||
NavigationLink(value: Route.settings) {
|
||||
ZXProfileMenuRow(icon: "bell.fill", title: "复习提醒", desc: "间隔复习通知设置")
|
||||
}.foregroundColor(.primary)
|
||||
ZXProfileDivider()
|
||||
NavigationLink(destination: MethodPreferenceView()) {
|
||||
ZXProfileMenuRow(emoji: "🧩", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔")
|
||||
NavigationLink(value: Route.methodPreference) {
|
||||
ZXProfileMenuRow(icon: "puzzlepiece.fill", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔")
|
||||
}.foregroundColor(.primary)
|
||||
ZXProfileDivider()
|
||||
NavigationLink(destination: FeedbackFormView()) {
|
||||
ZXProfileMenuRow(emoji: "💬", title: "帮助与反馈", desc: "问题报告 · 功能建议")
|
||||
NavigationLink(value: Route.feedbackForm) {
|
||||
ZXProfileMenuRow(icon: "bubble.left.fill", title: "帮助与反馈", desc: "问题报告 · 功能建议")
|
||||
}.foregroundColor(.primary)
|
||||
}.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
||||
achievementsSection.padding(.bottom, 120)
|
||||
@ -49,13 +49,14 @@ struct ProfileView: View {
|
||||
}.scrollIndicators(.hidden)
|
||||
}
|
||||
.task { await viewModel.loadAll() }
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
}
|
||||
private var profileCard: some View {
|
||||
let profile = viewModel.userProfile
|
||||
return NavigationLink(destination: EditProfilePage()) {
|
||||
return NavigationLink(value: Route.editProfile) {
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Text("🧑🎓").font(.system(size: 36)) }
|
||||
ZStack { Circle().frame(width: 80, height: 80).foregroundColor(Color.zxPurpleBG(0.2)); Image(systemName: "person.crop.circle.fill").font(.system(size: 36)).foregroundColor(Color.zxPurple) }
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(profile?.nickname ?? "学习者").font(.system(size: 20, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
Text(profile?.email ?? "").font(.system(size: 12)).foregroundColor(Color.zxF04)
|
||||
@ -71,19 +72,19 @@ struct ProfileView: View {
|
||||
private var achievementsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("成就").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
HStack(spacing: 8) { ZXAchievementBadge(emoji: "🔥", label: "连续 14 天", color: Color.zxOrange); ZXAchievementBadge(emoji: "🧠", label: "费曼达人", color: Color.zxPurple); ZXAchievementBadge(emoji: "📚", label: "知识收藏家", color: Color.zxTeal); ZXAchievementBadge(emoji: "⚡", label: "速学者", color: Color.zxYellow) }
|
||||
HStack(spacing: 8) { ZXAchievementBadge(icon: "flame.fill", label: "连续 14 天", color: Color.zxOrange); ZXAchievementBadge(icon: "brain.head.profile", label: "费曼达人", color: Color.zxPurple); ZXAchievementBadge(icon: "books.vertical.fill", label: "知识收藏家", color: Color.zxTeal); ZXAchievementBadge(icon: "bolt.fill", label: "速学者", color: Color.zxYellow) }
|
||||
}
|
||||
}
|
||||
}
|
||||
struct ZXProfileStat: View { let v: String; let l: String; let c: Color; var body: some View { VStack(spacing: 2) { Text(v).font(.system(size: 18, weight: .bold)).foregroundColor(c); Text(l).font(.system(size: 11)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
|
||||
init(value: String, label: String, color: Color) { self.v = value; self.l = label; self.c = color }
|
||||
}
|
||||
struct ZXProfileMenuRow: View { let emoji: String; let title: String; let desc: String
|
||||
var body: some View { HStack(spacing: 12) { Text(emoji).font(.system(size: 20)).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14).accessibilityLabel("\(title):\(desc)") }
|
||||
struct ZXProfileMenuRow: View { let icon: String; let title: String; let desc: String
|
||||
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(Color.zxF05).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14).accessibilityLabel("\(title):\(desc)") }
|
||||
}
|
||||
struct ZXProfileDivider: View {
|
||||
var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 64) }
|
||||
}
|
||||
struct ZXAchievementBadge: View { let emoji: String; let label: String; let color: Color
|
||||
var body: some View { VStack(spacing: 6) { Text(emoji).font(.system(size: 24)).frame(width: 48, height: 48).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.25), lineWidth: 1)); Text(label).font(.system(size: 10, weight: .semibold)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
|
||||
struct ZXAchievementBadge: View { let icon: String; let label: String; let color: Color
|
||||
var body: some View { VStack(spacing: 6) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(color).frame(width: 48, height: 48).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.25), lineWidth: 1)); Text(label).font(.system(size: 10, weight: .semibold)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
|
||||
}
|
||||
|
||||
@ -32,11 +32,11 @@ struct SettingsView: View {
|
||||
|
||||
sectionHeader("学习设置")
|
||||
VStack(spacing: 0) {
|
||||
NavigationLink(destination: GoalSettingDetailView()) {
|
||||
NavigationLink(value: Route.goalSetting) {
|
||||
ZXSettingRow(title: "学习目标", value: "备考考试", icon: "target", color: Color.zxOrange)
|
||||
}.foregroundColor(.primary)
|
||||
ZXSettingDivider()
|
||||
NavigationLink(destination: MethodPreferenceView()) {
|
||||
NavigationLink(value: Route.methodPreference) {
|
||||
ZXSettingRow(title: "学习方法偏好", value: "间隔回忆 · 费曼技巧", icon: "brain.head.profile", color: Color.zxPurple)
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
@ -66,7 +66,7 @@ struct SettingsView: View {
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
NavigationLink(destination: FeedbackFormView()) {
|
||||
NavigationLink(value: Route.feedbackForm) {
|
||||
ZXSettingRow(title: "帮助与反馈", value: "", icon: "questionmark.circle.fill", color: Color.zxAccent)
|
||||
}.foregroundColor(.primary)
|
||||
ZXSettingDivider()
|
||||
@ -152,121 +152,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct GoalSettingDetailView: View {
|
||||
@State private var selectedGoal = "备考考试"
|
||||
let goals = [("🧑🎓","备考考试","公考、考研、考证等"),("💼","职业技能","编程、设计、产品等"),("📚","通识学习","扩充知识面"),("🎯","自定义","设定自己的目标")]
|
||||
@State private var dailyMins = "30 分钟"
|
||||
let times = ["15 分钟","30 分钟","1 小时","不限制"]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
ForEach(goals, id: \.1) { g in let sel = selectedGoal == g.1
|
||||
Button { selectedGoal = g.1 } label: {
|
||||
HStack(spacing: 12) {
|
||||
Text(g.0).font(.system(size: 22)).frame(width: 44, height: 44).background(sel ? Color(hex: "#7C6EFA", opacity: 0.15) : Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
VStack(alignment: .leading, spacing: 2) { Text(g.1).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0); Text(g.2).font(.system(size: 12)).foregroundColor(Color.zxF04) }
|
||||
Spacer()
|
||||
Circle().stroke(sel ? Color.zxPurple : Color(hex: "#FFFFFF", opacity: 0.2), lineWidth: 2).frame(width: 22, height: 22).overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } }
|
||||
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
HStack(spacing: 8) { ForEach(times, id: \.self) { t in let sel = dailyMins == t; Button { dailyMins = t } label: { Text(t).font(.system(size: 12)).fontWeight(sel ? .semibold : .regular).foregroundColor(sel ? Color.zxPurple : Color.zxF05).frame(maxWidth: .infinity).frame(height: 40).background(sel ? Color(hex: "#7C6EFA", opacity: 0.1) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12)) }.foregroundColor(.primary) } }
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
_ = try? await UserService.shared.updateProfileDetail(UpdateProfileDataRequest(
|
||||
learningIdentity: nil, learningDirection: nil, bio: nil, currentGoal: selectedGoal
|
||||
))
|
||||
}
|
||||
} label: {
|
||||
Text("保存").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
|
||||
struct MethodPreferenceView: View {
|
||||
@State private var methods: Set<String> = ["间隔回忆", "费曼技巧"]
|
||||
let allMethods = ["间隔回忆", "费曼技巧", "AI 分析", "主动回忆"]
|
||||
@State private var saved = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("选择你偏好的学习方法").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
ForEach(allMethods, id: \.self) { m in let sel = methods.contains(m)
|
||||
Button { if sel { methods.remove(m) } else { methods.insert(m) } } label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: sel ? "checkmark.circle.fill" : "circle").font(.system(size: 20)).foregroundColor(sel ? Color.zxPurple : Color.zxF02)
|
||||
Text(m).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0)
|
||||
Spacer()
|
||||
}.padding(14).background(sel ? Color(hex: "#7C6EFA", opacity: 0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
_ = try? await UserService.shared.updatePreferences(UpdatePreferencesRequest(
|
||||
preferredMethods: Array(methods), defaultFocusMinutes: nil,
|
||||
aiSuggestionLevel: nil, language: nil, appearance: nil,
|
||||
notificationEnabled: nil
|
||||
))
|
||||
saved = true
|
||||
}
|
||||
} label: {
|
||||
Text(saved ? "已保存" : "保存").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedbackFormView: View {
|
||||
@State private var type = "功能建议"
|
||||
@State private var content = ""
|
||||
@State private var submitted = false
|
||||
let types = ["Bug 反馈", "功能建议", "内容问题", "其他"]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("反馈类型").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
HStack(spacing: 8) { ForEach(types, id: \.self) { t in let sel = type == t; Button { type = t } label: { Text(t).font(.system(size: 12)).foregroundColor(sel ? .white : Color.zxF05).padding(.horizontal, 12).padding(.vertical, 6).background(sel ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill005)).clipShape(Capsule()) } } }
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
|
||||
TextEditor(text: $content).frame(minHeight: 150).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
_ = try? await FeedbackService.shared.submit(category: type, content: content)
|
||||
submitted = true
|
||||
}
|
||||
} label: {
|
||||
Text(submitted ? "已提交" : "提交").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
|
||||
}.scrollIndicators(.hidden)
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
||||
}
|
||||
}
|
||||
// MARK: - Setting row components
|
||||
|
||||
struct ZXSettingRow: View {
|
||||
let title: String; let value: String; let icon: String; let color: Color
|
||||
|
||||
@ -6,6 +6,7 @@ struct LearningSessionView: View {
|
||||
let taskType: String
|
||||
let taskColor: Color
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var elapsed: TimeInterval = 0
|
||||
@State private var animatedProgress: CGFloat = 0
|
||||
@State private var isRunning = true
|
||||
@ -74,7 +75,7 @@ struct LearningSessionView: View {
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.frame(width: 180, height: 180)
|
||||
.animation(.easeInOut(duration: 0.5), value: animatedProgress)
|
||||
.animation(reduceMotion ? nil : .easeInOut(duration: 0.5), value: animatedProgress)
|
||||
VStack(spacing: 4) {
|
||||
Text(formatTime(elapsed))
|
||||
.font(.system(size: 36, weight: .black))
|
||||
@ -87,9 +88,9 @@ struct LearningSessionView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: elapsed) { newElapsed in
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
animatedProgress = min(CGFloat(newElapsed) / 1800, 1)
|
||||
}
|
||||
let pct = min(CGFloat(newElapsed) / 1800, 1)
|
||||
if reduceMotion { animatedProgress = pct }
|
||||
else { withAnimation(.easeInOut(duration: 0.5)) { animatedProgress = pct } }
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
@ -147,7 +148,7 @@ struct LearningSessionView: View {
|
||||
Text("学习小贴士").font(.system(size: 13, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
}
|
||||
Text("保持专注,25-30 分钟后休息 5 分钟能有效提升记忆效果。学习时尽量避免切换任务。")
|
||||
.font(.system(size: 12)).foregroundColor(Color.zxF04).lineSpacing(4)
|
||||
.zxFontScaled(size: 12).foregroundColor(Color.zxF04).lineSpacing(4)
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.zxFill004)
|
||||
@ -159,12 +160,12 @@ struct LearningSessionView: View {
|
||||
HStack(spacing: 12) {
|
||||
if isRunning {
|
||||
HStack(spacing: 6) {
|
||||
Circle().fill(Color.zxGreen).frame(width: 6, height: 6)
|
||||
Circle().fill(Color.zxGreen).frame(width: 8, height: 8)
|
||||
Text("学习中…").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxGreen)
|
||||
}
|
||||
} else if isPaused {
|
||||
HStack(spacing: 6) {
|
||||
Circle().fill(Color.zxYellow).frame(width: 6, height: 6)
|
||||
Circle().fill(Color.zxYellow).frame(width: 8, height: 8)
|
||||
Text("已暂停").font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxYellow)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ struct ReviewCardView: View {
|
||||
source: "机器学习 · 正则化方法", count: 3, total: 8),
|
||||
]
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var idx = 0
|
||||
@State private var flipped = false
|
||||
@State private var rating: Int? = nil
|
||||
@ -78,7 +79,7 @@ struct ReviewCardView: View {
|
||||
private var flashCard: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(flipped ? "答案" : "问题")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.zxFontScaled(size: 10, weight: .bold)
|
||||
.foregroundColor(flipped ? Color.zxGreen : Color.zxAccent)
|
||||
.tracking(0.5)
|
||||
.padding(.horizontal, 10).padding(.vertical, 3)
|
||||
@ -87,7 +88,7 @@ struct ReviewCardView: View {
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Text(flipped ? current.answer : current.question)
|
||||
.font(.system(size: flipped ? 14 : 16, weight: flipped ? .medium : .semibold))
|
||||
.zxFontScaled(size: flipped ? 14 : 16, weight: flipped ? .medium : .semibold)
|
||||
.foregroundColor(Color.zxF0)
|
||||
.lineSpacing(6)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -98,12 +99,12 @@ struct ReviewCardView: View {
|
||||
Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.vertical, 12)
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "book.closed.fill").font(.system(size: 10)).foregroundColor(Color.zxF03)
|
||||
Text(current.source).font(.system(size: 11)).foregroundColor(Color.zxF04)
|
||||
Text(current.source).zxFontScaled(size: 11).foregroundColor(Color.zxF04)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("点击翻转查看答案")
|
||||
.font(.system(size: 11))
|
||||
.zxFontScaled(size: 11)
|
||||
.foregroundColor(Color.zxF03)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
@ -113,7 +114,7 @@ struct ReviewCardView: View {
|
||||
.background(flipped ? ZXGradient.progressCard : ZXGradient.thinkingCard)
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke((flipped ? Color.zxPurple : Color.zxAccent).opacity(0.15), lineWidth: 1))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.onTapGesture { withAnimation(.easeInOut(duration: 0.4)) { flipped.toggle() } }
|
||||
.onTapGesture { if reduceMotion { flipped.toggle() } else { withAnimation(.easeInOut(duration: 0.4)) { flipped.toggle() } } }
|
||||
.zxPressable()
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(flipped ? "答案:\(current.answer)" : "问题:\(current.question)")
|
||||
@ -163,10 +164,10 @@ struct ZXRatingBtn: View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: 4) {
|
||||
Text(label).font(.system(size: 11, weight: selected ? .bold : .medium))
|
||||
.foregroundColor(selected ? .white : Color.zxF05)
|
||||
.foregroundColor(selected ? .white : color)
|
||||
}
|
||||
.frame(maxWidth: .infinity).frame(height: 56)
|
||||
.background(selected ? AnyView(ZXGradient.brand) : AnyView(Color.zxFill005))
|
||||
.background(selected ? AnyView(color) : AnyView(Color.zxFill005))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.overlay {
|
||||
if !selected { RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1) }
|
||||
|
||||
@ -16,15 +16,15 @@ struct StudyHomeView: View {
|
||||
VStack(alignment: .leading, spacing: 12) { HStack { Text("今日任务").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0); Spacer(); HStack(spacing: 4) { Image(systemName: "calendar").font(.system(size: 12)).foregroundColor(Color.zxF04); Text("AI 自动排期").font(.system(size: 12)).foregroundColor(Color.zxF04) } }
|
||||
ForEach($studyHomeVM.tasks) { $t in
|
||||
if t.tp == "回忆测试" {
|
||||
NavigationLink(destination: ActiveRecallView()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
NavigationLink(value: Route.activeRecall) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
} else if t.tp == "费曼练习" {
|
||||
NavigationLink(destination: AIChatPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
NavigationLink(value: Route.aiChat) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
} else if t.tp == "薄弱点" {
|
||||
NavigationLink(destination: WeakPointsPage()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
NavigationLink(value: Route.weakPoints) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
} else if t.tp == "间隔复习" {
|
||||
NavigationLink(destination: ReviewCardView()) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
NavigationLink(value: Route.reviewCard) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
} else {
|
||||
NavigationLink(destination: LearningSessionView(taskTitle: t.t, taskType: t.tp, taskColor: t.c)) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
NavigationLink(value: Route.learningSession(taskTitle: t.t, taskType: t.tp, taskColorHex: t.ch)) { ZXSTaskRowView(task: t) { t.d.toggle() } }.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,7 @@ struct StudyHomeView: View {
|
||||
.zxPullToRefresh { await studyVM.loadSessions() }
|
||||
}
|
||||
.task { await studyVM.loadSessions() }
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
}
|
||||
private var pc: some View { let dn = studyHomeVM.doneCount; let pct = CGFloat(dn) / 5
|
||||
return VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 2) { Text("今日进度").font(.system(size: 13, weight: .medium)).foregroundColor(Color.zxF05); HStack(alignment: .lastTextBaseline, spacing: 6) { Text("\(dn)").font(.system(size: 26, weight: .black)).foregroundColor(Color.zxF0).contentTransition(.numericText()); Text("/ 5"); Text("个任务").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) } }; Spacer()
|
||||
@ -46,7 +47,7 @@ struct StudyHomeView: View {
|
||||
.padding(16).background(ZXGradient.progressCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.15), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20)) }
|
||||
}
|
||||
|
||||
struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let c: Color; let m: Int; var d: Bool }
|
||||
struct ZXSTask: Identifiable { let id = UUID(); let t: String; let tp: String; let ch: String; let m: Int; var d: Bool; var c: Color { Color(hex: ch) } }
|
||||
struct ZXSTaskRow: View { @Binding var task: ZXSTask
|
||||
var body: some View { Button { task.d.toggle() } label: { ZXSTaskRowView(task: task) {} }.foregroundColor(.primary) }
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@ import Foundation
|
||||
@MainActor
|
||||
final class StudyHomeViewModel: ObservableObject {
|
||||
@Published var tasks: [ZXSTask] = [
|
||||
ZXSTask(t: "机器学习 - 回忆测试", tp: "回忆测试", c: .zxPurple, m: 10, d: true),
|
||||
ZXSTask(t: "高数 - 间隔复习 8 题", tp: "间隔复习", c: .zxOrange, m: 15, d: true),
|
||||
ZXSTask(t: "英语词汇 - 25 个待复习", tp: "词汇复习", c: .zxTeal, m: 8, d: false),
|
||||
ZXSTask(t: "注意力机制 - 费曼解释", tp: "费曼练习", c: .zxAccent, m: 12, d: false),
|
||||
ZXSTask(t: "产品设计 - 薄弱点复习", tp: "薄弱点", c: .zxYellow, m: 10, d: false),
|
||||
ZXSTask(t: "机器学习 - 回忆测试", tp: "回忆测试", ch: "#7C6EFA", m: 10, d: true),
|
||||
ZXSTask(t: "高数 - 间隔复习 8 题", tp: "间隔复习", ch: "#F97316", m: 15, d: true),
|
||||
ZXSTask(t: "英语词汇 - 25 个待复习", tp: "词汇复习", ch: "#2DD4BF", m: 8, d: false),
|
||||
ZXSTask(t: "注意力机制 - 费曼解释", tp: "费曼练习", ch: "#A78BFA", m: 12, d: false),
|
||||
ZXSTask(t: "产品设计 - 薄弱点复习", tp: "薄弱点", ch: "#F59E0B", m: 10, d: false),
|
||||
]
|
||||
|
||||
@Published var weekActivity: [CGFloat] = [0.3, 0.7, 1.0, 0.4, 0.9, 0.6, 0.2]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user