WangDL 7066200b7b feat: MVVM 架构、全套 UI 页面、浅深色主题、本地持久化、等待名单、AI 动效
- 架构层:ViewModel/ObservableObject、Service/Repository、网络层 APIClient/APIEndpoint/APIError
- 设计系统:Color(light:dark:) 自适应 28 色 Token、ColorSchemeManager 深浅色切换
- 全页面:AI 对话/反馈/回忆/薄弱点、知识库 CRUD、学习工作台、复习计划、学习分析、个人中心/设置
- 登录与引导:Sign in with Apple、AppSession 状态管理、引导流程、演示模式
- 本地持久化:FileCache + PersistenceController(学习任务/复习任务/学习记录)
- 本地化:zh-Hans Localizable.strings ~120 条、ZXStrings 程序化引用、LanguageManager
- 组件库:ZXTabBar/ZXBackHeader/ZXSTaskRow/ZXChartView/ZXTypingIndicator 等 22 个共享组件
- 等待名单:WaitlistView 邮箱收集表单
- 动效:ZXTypingIndicator AI 打字动画、ZXShimmerModifier 骨架屏
- 测试:StudyHomeViewModel/AIChatViewModel/ReviewPlanViewModel/FileCache 共 28 条
- Dynamic Type 支持 + 范围限制

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:22:50 +08:00

276 lines
13 KiB
Swift

import SwiftUI
// MARK: - Learning Goal Settings
struct LearningGoalSettingsView: View {
@State private var selectedGoal = ""
@State private var dailyMins = "30 分钟"
var body: some View {
ZStack {
ZXGradient.page.ignoresSafeArea()
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ZXBackHeader(title: "学习目标设置")
VStack(alignment: .leading, spacing: 10) {
Text("学习目标").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5)
ForEach([("🧑‍🎓", "备考考试"), ("💼", "职业技能"), ("📚", "通识学习"), ("🎯", "自定义")], id: \.1) { emoji, title in
let sel = selectedGoal == title
Button {
selectedGoal = title
} label: {
HStack(spacing: 12) {
Text(emoji).font(.system(size: 22)).frame(width: 44, height: 44)
.background(sel ? Color.zxPurpleBG(0.15) : Color.zxFill005)
.clipShape(RoundedRectangle(cornerRadius: 12))
Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(sel ? Color.zxPurple : Color.zxF0)
Spacer()
Circle().stroke(sel ? Color.zxPurple : Color.zxF02, lineWidth: 2)
.frame(width: 22, height: 22)
.overlay { if sel { Circle().fill(Color.zxPurple).frame(width: 12, height: 12) } }
}
.padding(14)
.background(sel ? Color.zxPurpleBG(0.08) : Color.zxFill003)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(sel ? Color.zxPurpleBG(0.25) : Color.zxBorder006, lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.foregroundColor(.primary)
}
}
VStack(alignment: .leading, spacing: 10) {
Text("每日学习时间").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5)
HStack(spacing: 8) {
ForEach(["15 分钟", "30 分钟", "1 小时", "不限制"], id: \.self) { t in
let sel = dailyMins == t
Button {
dailyMins = t
} label: {
Text(t).font(.system(size: 12)).fontWeight(sel ? .semibold : .regular)
.foregroundColor(sel ? Color.zxPurple : Color.zxF05)
.frame(maxWidth: .infinity).frame(height: 40)
.background(sel ? Color.zxPurpleBG(0.1) : Color.zxFill003)
.overlay(RoundedRectangle(cornerRadius: 12).stroke(sel ? Color.zxPurpleBG(0.25) : Color.zxBorder006, lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.foregroundColor(.primary)
}
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
.navigationBarHidden(true)
}
}
// MARK: - Review Reminder Settings
struct ReviewReminderSettingsView: View {
@State private var reminderEnabled = true
@State private var reminderTime = Date()
@State private var intervalDays = 1
var body: some View {
ZStack {
ZXGradient.page.ignoresSafeArea()
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ZXBackHeader(title: "复习提醒")
VStack(spacing: 0) {
Toggle(isOn: $reminderEnabled) {
VStack(alignment: .leading, spacing: 2) {
Text("开启复习提醒").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
Text("基于间隔重复算法,在最佳时间提醒你复习").font(.system(size: 12)).foregroundColor(Color.zxF04)
}
}
.tint(Color.zxPurple)
.padding(.horizontal, 16).padding(.vertical, 14)
Divider().background(Color.zxBorder006)
DatePicker("提醒时间", selection: $reminderTime, displayedComponents: .hourAndMinute)
.font(.system(size: 14)).foregroundColor(Color.zxF0)
.padding(.horizontal, 16).padding(.vertical, 14)
.tint(Color.zxPurple)
Divider().background(Color.zxBorder006)
HStack {
Text("间隔天数").font(.system(size: 14)).foregroundColor(Color.zxF0)
Spacer()
Stepper("\(intervalDays)", value: $intervalDays, in: 1...7)
.font(.system(size: 14)).foregroundColor(Color.zxF05)
}
.padding(.horizontal, 16).padding(.vertical, 14)
}
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
}
.padding(.horizontal, 20)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
.navigationBarHidden(true)
}
}
// MARK: - Learning Report
struct LearningReportView: View {
var body: some View {
ZStack {
ZXGradient.page.ignoresSafeArea()
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ZXBackHeader(title: "学习报告")
VStack(spacing: 12) {
ReportCard(period: "本周", studyDays: 5, totalMins: 320, newItems: 12, reviewed: 47)
ReportCard(period: "本月", studyDays: 18, totalMins: 1240, newItems: 47, reviewed: 186)
}
VStack(alignment: .leading, spacing: 8) {
Text("成就").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0)
HStack(spacing: 8) {
ZXAchievementBadge(emoji: "🔥", label: "连续 14 天", color: Color.zxOrange)
ZXAchievementBadge(emoji: "🧠", label: "费曼达人", color: Color.zxPurple)
ZXAchievementBadge(emoji: "📚", label: "知识收藏家", color: Color.zxTeal)
ZXAchievementBadge(emoji: "", label: "速学者", color: Color.zxYellow)
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
.navigationBarHidden(true)
}
}
private struct ReportCard: View {
let period: String; let studyDays: Int; let totalMins: Int; let newItems: Int; let reviewed: Int
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(period).font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxPurple)
HStack(spacing: 0) {
ReportStat(value: "\(studyDays)", label: "学习天")
ReportStat(value: "\(totalMins)", label: "分钟")
ReportStat(value: "\(newItems)", label: "新知识")
ReportStat(value: "\(reviewed)", label: "已复习")
}
}
.padding(16)
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
}
}
private struct ReportStat: View {
let value: String; let label: String
var body: some View {
VStack(spacing: 2) {
Text(value).font(.system(size: 20, weight: .heavy)).foregroundColor(Color.zxF0)
Text(label).font(.system(size: 10)).foregroundColor(Color.zxF04)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Learning Method Preferences
struct LearningMethodPreferencesView: View {
@State private var selectedMethods: Set<String> = ["间隔回忆", "费曼技巧"]
var body: some View {
ZStack {
ZXGradient.page.ignoresSafeArea()
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ZXBackHeader(title: "学习方法偏好")
VStack(spacing: 0) {
ForEach([
("间隔回忆", "基于遗忘曲线,在最佳时机提醒你复习"),
("费曼技巧", "用自己的语言重新解释知识,发现理解盲区"),
("AI 分析", "AI 自动定位薄弱知识点,给出针对性建议")
], id: \.0) { method, desc in
let sel = selectedMethods.contains(method)
Button {
if sel { selectedMethods.remove(method) }
else { selectedMethods.insert(method) }
} label: {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(method).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04)
}
Spacer()
Image(systemName: sel ? "checkmark.circle.fill" : "circle")
.font(.system(size: 22))
.foregroundColor(sel ? Color.zxPurple : Color.zxF02)
}
.padding(.horizontal, 16).padding(.vertical, 14)
}
.foregroundColor(.primary)
if method != "AI 分析" {
Divider().background(Color.zxBorder006)
}
}
}
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
}
.padding(.horizontal, 20)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
.navigationBarHidden(true)
}
}
// MARK: - Data Sync Settings
struct DataSyncSettingsView: View {
@State private var iCloudEnabled = false
@State private var autoBackupEnabled = true
var body: some View {
ZStack {
ZXGradient.page.ignoresSafeArea()
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ZXBackHeader(title: "数据同步与备份")
VStack(spacing: 0) {
Toggle(isOn: $iCloudEnabled) {
VStack(alignment: .leading, spacing: 2) {
Text("iCloud 同步").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
Text("在 Apple ID 关联的设备间同步学习数据").font(.system(size: 12)).foregroundColor(Color.zxF04)
}
}
.tint(Color.zxPurple)
.padding(.horizontal, 16).padding(.vertical, 14)
Divider().background(Color.zxBorder006)
Toggle(isOn: $autoBackupEnabled) {
VStack(alignment: .leading, spacing: 2) {
Text("自动备份").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
Text("每日自动备份学习记录和知识库").font(.system(size: 12)).foregroundColor(Color.zxF04)
}
}
.tint(Color.zxPurple)
.padding(.horizontal, 16).padding(.vertical, 14)
}
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
}
.padding(.horizontal, 20)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
.navigationBarHidden(true)
}
}