Compare commits
11 Commits
eb984848cd
...
b4b71a5075
| Author | SHA1 | Date | |
|---|---|---|---|
| b4b71a5075 | |||
| f5324bd57b | |||
| 86a4a3b168 | |||
| 7d123afe79 | |||
| 120dac0ec9 | |||
| 94ef6ad84a | |||
| 1d7b48837c | |||
| 71ab42250e | |||
| d09edd395c | |||
| dc2e6a142e | |||
| 72b38ad416 |
@@ -3,6 +3,7 @@ import Foundation
|
||||
enum QuizMode: String, Hashable {
|
||||
case enToZh = "英选汉"
|
||||
case zhToEn = "汉选英"
|
||||
case spelling = "拼写"
|
||||
}
|
||||
|
||||
struct QuizSetupRequest: Hashable {
|
||||
@@ -21,6 +22,8 @@ struct QuizQuestion: Identifiable {
|
||||
let prompt: String
|
||||
let options: [String]
|
||||
let answer: String
|
||||
let blankedWord: String
|
||||
let blankPositions: [Int]
|
||||
}
|
||||
|
||||
enum QuizGenerator {
|
||||
@@ -28,20 +31,41 @@ enum QuizGenerator {
|
||||
words.shuffled().map { word in
|
||||
let distractors = words.filter { $0 != word }.shuffled().prefix(3)
|
||||
let options = (distractors.map { optionText($0, mode) } + [optionText(word, mode)]).shuffled()
|
||||
let blankedWord = mode == .spelling ? blankWord(word.word) : ""
|
||||
let blankPositions = blankedWord.enumerated().filter { $0.element == "_" }.map { $0.offset }
|
||||
return QuizQuestion(
|
||||
word: word,
|
||||
prompt: promptText(word, mode),
|
||||
options: options,
|
||||
answer: optionText(word, mode)
|
||||
answer: optionText(word, mode),
|
||||
blankedWord: blankedWord,
|
||||
blankPositions: blankPositions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func promptText(_ word: Word, _ mode: QuizMode) -> String {
|
||||
mode == .enToZh ? word.word : word.meaning
|
||||
switch mode {
|
||||
case .enToZh: return word.word
|
||||
case .zhToEn: return word.meaning
|
||||
case .spelling: return word.meaning
|
||||
}
|
||||
}
|
||||
|
||||
private static func optionText(_ word: Word, _ mode: QuizMode) -> String {
|
||||
mode == .enToZh ? word.meaning : word.word
|
||||
switch mode {
|
||||
case .enToZh: return word.meaning
|
||||
case .zhToEn: return word.word
|
||||
case .spelling: return word.word
|
||||
}
|
||||
}
|
||||
|
||||
private static func blankWord(_ word: String) -> String {
|
||||
let chars = Array(word)
|
||||
let count = chars.count
|
||||
guard count > 2 else { return String(repeating: "_", count: count) }
|
||||
let blanks = max(1, Int(Double(count) * 0.4))
|
||||
let indices = Set(Array(0..<count).shuffled().prefix(blanks))
|
||||
return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ struct QuizSetupView: View {
|
||||
List {
|
||||
NavigationLink(value: AppRoute.quiz(QuizConfig(mode: request.mode, words: request.words))) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("全部测试")
|
||||
Text("全部随机测试")
|
||||
.font(.headline)
|
||||
Text("\(request.words.count) 题")
|
||||
.font(.subheadline)
|
||||
|
||||
@@ -9,6 +9,7 @@ struct QuizView: View {
|
||||
@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
|
||||
@@ -53,6 +54,9 @@ struct QuizView: View {
|
||||
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: {
|
||||
@@ -64,31 +68,35 @@ struct QuizView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ForEach(question.options, id: \.self) { option in
|
||||
Button {
|
||||
select(option, for: question)
|
||||
} label: {
|
||||
Text(option)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(optionColor(option, for: question))
|
||||
.disabled(selectedOption != nil)
|
||||
}
|
||||
if config.mode == .spelling {
|
||||
spellingView(question)
|
||||
} else {
|
||||
optionsView(question)
|
||||
}
|
||||
|
||||
if let selectedOption {
|
||||
VStack(spacing: 12) {
|
||||
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
|
||||
.font(.headline)
|
||||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||||
Button(currentIndex + 1 < questions.count ? "下一题" : "查看成绩") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +105,94 @@ struct QuizView: View {
|
||||
.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: 24) {
|
||||
Text(question.prompt)
|
||||
.font(.title.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(question.blankedWord.enumerated()), id: \.offset) { i, ch in
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.quaternary.opacity(0.15))
|
||||
.frame(width: 36, height: 44)
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(.quaternary, lineWidth: 1)
|
||||
.frame(width: 36, height: 44)
|
||||
if ch == "_" {
|
||||
Rectangle()
|
||||
.fill(.secondary.opacity(0.5))
|
||||
.frame(width: 16, height: 2)
|
||||
.offset(y: 12)
|
||||
} else {
|
||||
Text(ch.uppercased())
|
||||
.font(.title2.weight(.bold))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("\(question.word.word.count) 个字母,填写完整单词")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
|
||||
TextField("在此输入完整单词", text: $userInput)
|
||||
.font(.body)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.disabled(selectedOption != nil)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
.background(.quaternary.opacity(0.2), in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.quaternary, lineWidth: 1)
|
||||
)
|
||||
|
||||
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)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.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 }
|
||||
@@ -116,6 +212,7 @@ struct QuizView: View {
|
||||
if currentIndex + 1 < questions.count {
|
||||
currentIndex += 1
|
||||
selectedOption = nil
|
||||
userInput = ""
|
||||
} else {
|
||||
finished = true
|
||||
}
|
||||
@@ -125,6 +222,7 @@ struct QuizView: View {
|
||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||
currentIndex = 0
|
||||
selectedOption = nil
|
||||
userInput = ""
|
||||
wrongWords = []
|
||||
finished = false
|
||||
startDate = .now
|
||||
|
||||
@@ -25,6 +25,7 @@ struct WordListView: View {
|
||||
Menu {
|
||||
Button("英选汉") { startQuiz(.enToZh) }
|
||||
Button("汉选英") { startQuiz(.zhToEn) }
|
||||
Button("拼写") { startQuiz(.spelling) }
|
||||
} label: {
|
||||
Text("测验")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user