ios-projects/AIStudyApp/AIStudyApp/Features/AI/ActiveRecallView.swift
wangdl 4ebb70c036 feat: 图标线型化 + 首页重设计 + 知识库卡片优化 + 知识点列表重构
- 所有 SF Symbol .fill 图标替换为线性版本
- 自定义加载动画全部替换为原生 ProgressView/refreshable
- StudyHomeView 重设计:优先级驱动主行动卡片
- ZLibraryCard 重新设计:封面图自适应、信息布局优化
- LibraryDetailPage:顶部KB信息区、···菜单、排序、长按操作
- 知识点列表:文件类型图标、学习时长、分割线样式
- 弥散渐变顶部背景
- 新增 icon-folder、icon-xmark SVG

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:07:15 +08:00

222 lines
9.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
struct ActiveRecallView: View {
@StateObject private var viewModel = ActiveRecallViewModel()
let questions: [RecallQuestion] = [
.init(id: "1", question: "请解释贝叶斯定理的核心思想,并写出公式", source: "机器学习 · 概率论", isVoice: false),
.init(id: "2", question: "请用自己的话解释梯度下降算法的工作原理", source: "机器学习 · 优化算法", isVoice: false),
.init(id: "3", question: "用费曼学习法解释「过拟合与欠拟合」的区别", source: "机器学习 · 模型选择", isVoice: true),
]
@State private var idx = 0
@State private var answers: [String: String] = [:]
@State private var currentAnswer = ""
@State private var submitted: Set<String> = []
@State private var showFinish = false
@State private var showThinking = false
@State private var showCelebration = false
var current: RecallQuestion { questions[idx] }
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
VStack(spacing: 0) {
progressHeader
ScrollView {
VStack(spacing: 16) {
questionCard
if !isSubmitted {
answerInput
} else {
submittedView
}
}
.padding(.horizontal, 20)
.padding(.top, 12)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
}
}
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar)
.toolbarBackground(.hidden, for: .navigationBar)
.task { await viewModel.loadQuestions() }
.overlay {
if showThinking {
ZXThinkingOverlay("AI 正在分析你的回答…")
}
}
.overlay {
if showCelebration {
ZXCelebrationView(title: "回忆完成", subtitle: "你已完成所有主动回忆题目AI 分析结果已生成") {
withAnimation(.easeOut(duration: 0.3)) { showCelebration = false }
}
}
}
}
private var isSubmitted: Bool { submitted.contains(current.id) }
private var progressHeader: some View {
VStack(spacing: 8) {
HStack {
Text("主动回忆 \(idx + 1)/\(questions.count)")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.zxF04)
Spacer()
Text("已答 \(submitted.count)")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.zxPurple)
}
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2).fill(Color.zxFill008).frame(height: 3)
RoundedRectangle(cornerRadius: 2)
.fill(ZXGradient.progressBar)
.frame(width: max(3, CGFloat(idx + 1) / CGFloat(questions.count) * (UIScreen.main.bounds.width - 40)), height: 3)
}
}
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 4)
}
private var questionCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
if current.isVoice {
Image("icon-mic").font(.system(size: 12)).foregroundColor(Color.zxOrange)
Text("语音题").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxOrange)
.padding(.horizontal, 6).padding(.vertical, 2).background(Color.zxOrangeBG(0.1)).clipShape(Capsule())
} else {
Image("icon-pencil").font(.system(size: 12)).foregroundColor(Color.zxPurple)
Text("文字题").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple)
.padding(.horizontal, 6).padding(.vertical, 2).background(Color.zxPurpleBG(0.1)).clipShape(Capsule())
}
Spacer()
Text(current.source).font(.system(size: 10)).foregroundColor(Color.zxF03)
}
Text(current.question)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(Color.zxF0)
.lineSpacing(5)
}
.padding(16)
.background(ZXGradient.thinkingCard)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 16))
.accessibilityElement(children: .combine)
.accessibilityLabel("问题 \(idx + 1)\(current.question)")
.accessibilityHint(current.isVoice ? "语音题,双击录音回答" : "文字题,在下方输入回答")
}
private var answerInput: some View {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("你的回答").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
if current.isVoice {
voiceInputArea
} else {
TextEditor(text: $currentAnswer)
.font(.system(size: 14))
.foregroundColor(Color.zxF0)
.tint(Color.zxPurple)
.frame(minHeight: 150)
.scrollContentBackground(.hidden)
.padding(12)
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
}
Button {
answers[current.id] = current.isVoice ? "语音答案已录制" : currentAnswer
submitted.insert(current.id)
currentAnswer = ""
if submitted.count == questions.count {
showThinking = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
showThinking = false
showCelebration = true
}
}
} label: {
Text("提交回答")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity).frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.zxPressable()
.disabled(currentAnswer.isEmpty && !current.isVoice)
.opacity(currentAnswer.isEmpty && !current.isVoice ? 0.5 : 1)
.accessibilityLabel("提交回答")
.accessibilityHint("提交后可由 AI 分析你的回答质量")
}
}
private var voiceInputArea: some View {
VStack(spacing: 12) {
ZStack {
Circle().fill(Color.zxOrangeBG(0.1)).frame(width: 80, height: 80)
Image("icon-mic").font(.system(size: 32)).foregroundColor(Color.zxOrange)
}
Text("点击按钮开始录音,用费曼方法口头解释").font(.system(size: 12)).foregroundColor(Color.zxF04)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
private var submittedView: some View {
VStack(spacing: 16) {
HStack(spacing: 10) {
Image(systemName: "checkmark.circle").font(.system(size: 22)).foregroundColor(Color.zxGreen)
VStack(alignment: .leading, spacing: 3) {
Text("回答已提交").font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxGreen)
Text("AI 分析中,稍后可查看反馈").font(.system(size: 12)).foregroundColor(Color.zxF04)
}
Spacer()
}
.padding(16)
.background(Color.zxGreenBG(0.06))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "#34D399", opacity: 0.15), lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
if idx < questions.count - 1 {
Button { idx += 1 } label: {
Label("下一题", systemImage: "arrow.right")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity).frame(height: 52)
.background(ZXGradient.brand)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.zxPressable()
} else {
Button {
showCelebration = true
} label: {
Label("查看 AI 分析结果", systemImage: "sparkles")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity).frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.zxPressable()
}
}
}
}
struct RecallQuestion: Identifiable {
let id: String
let question: String
let source: String
let isVoice: Bool
}