f5324bd57b
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
208 lines
7.8 KiB
Swift
208 lines
7.8 KiB
Swift
import SwiftUI
|
||
|
||
struct QuizView: View {
|
||
let config: QuizConfig
|
||
|
||
@Environment(SpeechService.self) private var speech
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@State private var questions: [QuizQuestion]
|
||
@State private var currentIndex = 0
|
||
@State private var selectedOption: String?
|
||
@State private var userInput = ""
|
||
@State private var wrongWords: [Word] = []
|
||
@State private var finished = false
|
||
@State private var startDate = Date.now
|
||
|
||
init(config: QuizConfig) {
|
||
self.config = config
|
||
_questions = State(initialValue: QuizGenerator.makeQuestions(mode: config.mode, words: config.words))
|
||
}
|
||
|
||
private var current: QuizQuestion? {
|
||
questions.indices.contains(currentIndex) ? questions[currentIndex] : nil
|
||
}
|
||
|
||
var body: some View {
|
||
Group {
|
||
if finished {
|
||
QuizResultView(
|
||
total: questions.count,
|
||
wrongWords: wrongWords,
|
||
duration: Date.now.timeIntervalSince(startDate),
|
||
onRestart: restart,
|
||
onExit: { dismiss() }
|
||
)
|
||
} else if let question = current {
|
||
questionView(question)
|
||
}
|
||
}
|
||
.navigationTitle(config.mode.rawValue)
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
}
|
||
|
||
private func questionView(_ question: QuizQuestion) -> some View {
|
||
VStack(spacing: 24) {
|
||
VStack(spacing: 8) {
|
||
ProgressView(value: Double(currentIndex), total: Double(questions.count))
|
||
Text("第 \(currentIndex + 1) / \(questions.count) 题")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
VStack(spacing: 12) {
|
||
Text(question.prompt)
|
||
.font(.system(size: config.mode == .enToZh ? 44 : 34, weight: .bold))
|
||
if config.mode == .enToZh {
|
||
Text(question.word.phonetic)
|
||
.font(.title3)
|
||
.foregroundStyle(.secondary)
|
||
Button {
|
||
speech.speak(question.prompt)
|
||
} label: {
|
||
Image(systemName: "speaker.wave.2.fill")
|
||
.font(.title2)
|
||
}
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 24)
|
||
|
||
if config.mode == .spelling {
|
||
spellingView(question)
|
||
} else {
|
||
optionsView(question)
|
||
}
|
||
|
||
if let selectedOption {
|
||
VStack(spacing: 12) {
|
||
if config.mode == .spelling {
|
||
Text(selectedOption == question.answer ? "正确" : "正确答案:\(question.answer)")
|
||
.font(.title2.weight(.bold))
|
||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||
Text(question.word.phonetic)
|
||
.font(.subheadline)
|
||
.foregroundStyle(.secondary)
|
||
} else {
|
||
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
|
||
.font(.headline)
|
||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||
}
|
||
Button {
|
||
next()
|
||
} label: {
|
||
Text(currentIndex + 1 < questions.count ? "下一题" : "查看成绩")
|
||
.frame(maxWidth: .infinity)
|
||
.frame(height: 44)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.font(.headline)
|
||
}
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
.padding()
|
||
}
|
||
|
||
private func optionsView(_ question: QuizQuestion) -> some View {
|
||
VStack(spacing: 12) {
|
||
ForEach(question.options, id: \.self) { option in
|
||
Button {
|
||
select(option, for: question)
|
||
} label: {
|
||
VStack(spacing: 4) {
|
||
Text(option)
|
||
.font(.headline)
|
||
if config.mode == .zhToEn, let w = config.words.first(where: { $0.word == option }) {
|
||
Text(w.phonetic)
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 10)
|
||
}
|
||
.buttonStyle(.bordered)
|
||
.tint(optionColor(option, for: question))
|
||
.disabled(selectedOption != nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func spellingView(_ question: QuizQuestion) -> some View {
|
||
VStack(spacing: 20) {
|
||
Text(question.blankedWord)
|
||
.font(.system(size: 44, weight: .bold, design: .monospaced))
|
||
.tracking(6)
|
||
Text("\(question.word.word.count) 个字母")
|
||
.font(.subheadline)
|
||
.foregroundStyle(.secondary)
|
||
HStack(spacing: 12) {
|
||
TextField("填写完整单词", text: $userInput)
|
||
.textFieldStyle(.roundedBorder)
|
||
.autocapitalization(.none)
|
||
.disableAutocorrection(true)
|
||
.disabled(selectedOption != nil)
|
||
Button("确认") {
|
||
let answer = userInput.trimmingCharacters(in: .whitespaces).lowercased()
|
||
guard !answer.isEmpty else { return }
|
||
selectedOption = answer
|
||
if answer != question.answer {
|
||
wrongWords.append(question.word)
|
||
}
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.disabled(userInput.trimmingCharacters(in: .whitespaces).isEmpty || selectedOption != nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func optionColor(_ option: String, for question: QuizQuestion) -> Color {
|
||
guard let selectedOption else { return .accentColor }
|
||
if option == question.answer { return .green }
|
||
if option == selectedOption { return .red }
|
||
return .gray
|
||
}
|
||
|
||
private func select(_ option: String, for question: QuizQuestion) {
|
||
guard selectedOption == nil else { return }
|
||
selectedOption = option
|
||
if option != question.answer {
|
||
wrongWords.append(question.word)
|
||
}
|
||
}
|
||
|
||
private func next() {
|
||
if currentIndex + 1 < questions.count {
|
||
currentIndex += 1
|
||
selectedOption = nil
|
||
userInput = ""
|
||
} else {
|
||
finished = true
|
||
}
|
||
}
|
||
|
||
private func restart() {
|
||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||
currentIndex = 0
|
||
selectedOption = nil
|
||
userInput = ""
|
||
wrongWords = []
|
||
finished = false
|
||
startDate = .now
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
NavigationStack {
|
||
QuizView(config: QuizConfig(mode: .enToZh, words: [
|
||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。"),
|
||
Word(word: "blue", phonetic: "/bluː/", meaning: "蓝色", example: "The sky is blue.", exampleMeaning: "天空是蓝色的。"),
|
||
Word(word: "green", phonetic: "/ɡriːn/", meaning: "绿色", example: "The grass is green.", exampleMeaning: "草地是绿色的。"),
|
||
Word(word: "yellow", phonetic: "/ˈjeloʊ/", meaning: "黄色", example: "The banana is yellow.", exampleMeaning: "香蕉是黄色的。")
|
||
]))
|
||
.environment(SpeechService())
|
||
}
|
||
}
|