新增拼写功能:出题随机空缺字母,用户输入完整单词
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import Foundation
|
|||||||
enum QuizMode: String, Hashable {
|
enum QuizMode: String, Hashable {
|
||||||
case enToZh = "英选汉"
|
case enToZh = "英选汉"
|
||||||
case zhToEn = "汉选英"
|
case zhToEn = "汉选英"
|
||||||
|
case spelling = "拼写"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QuizSetupRequest: Hashable {
|
struct QuizSetupRequest: Hashable {
|
||||||
@@ -21,6 +22,7 @@ struct QuizQuestion: Identifiable {
|
|||||||
let prompt: String
|
let prompt: String
|
||||||
let options: [String]
|
let options: [String]
|
||||||
let answer: String
|
let answer: String
|
||||||
|
let blankedWord: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum QuizGenerator {
|
enum QuizGenerator {
|
||||||
@@ -32,16 +34,34 @@ enum QuizGenerator {
|
|||||||
word: word,
|
word: word,
|
||||||
prompt: promptText(word, mode),
|
prompt: promptText(word, mode),
|
||||||
options: options,
|
options: options,
|
||||||
answer: optionText(word, mode)
|
answer: optionText(word, mode),
|
||||||
|
blankedWord: mode == .spelling ? blankWord(word.word) : ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func promptText(_ word: Word, _ mode: QuizMode) -> String {
|
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 {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct QuizView: View {
|
|||||||
@State private var questions: [QuizQuestion]
|
@State private var questions: [QuizQuestion]
|
||||||
@State private var currentIndex = 0
|
@State private var currentIndex = 0
|
||||||
@State private var selectedOption: String?
|
@State private var selectedOption: String?
|
||||||
|
@State private var userInput = ""
|
||||||
@State private var wrongWords: [Word] = []
|
@State private var wrongWords: [Word] = []
|
||||||
@State private var finished = false
|
@State private var finished = false
|
||||||
@State private var startDate = Date.now
|
@State private var startDate = Date.now
|
||||||
@@ -67,34 +68,26 @@ struct QuizView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 24)
|
.padding(.vertical, 24)
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
if config.mode == .spelling {
|
||||||
ForEach(question.options, id: \.self) { option in
|
spellingView(question)
|
||||||
Button {
|
} else {
|
||||||
select(option, for: question)
|
optionsView(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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let selectedOption {
|
if let selectedOption {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
|
if config.mode == .spelling {
|
||||||
.font(.headline)
|
Text(selectedOption == question.answer ? "正确" : "正确答案:\(question.answer)")
|
||||||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
.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 {
|
Button {
|
||||||
next()
|
next()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -112,6 +105,58 @@ struct QuizView: View {
|
|||||||
.padding()
|
.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: 16) {
|
||||||
|
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("确认") {
|
||||||
|
guard !userInput.trimmingCharacters(in: .whitespaces).isEmpty else { return }
|
||||||
|
selectedOption = userInput.trimmingCharacters(in: .whitespaces).lowercased()
|
||||||
|
if selectedOption != 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 {
|
private func optionColor(_ option: String, for question: QuizQuestion) -> Color {
|
||||||
guard let selectedOption else { return .accentColor }
|
guard let selectedOption else { return .accentColor }
|
||||||
if option == question.answer { return .green }
|
if option == question.answer { return .green }
|
||||||
@@ -131,6 +176,7 @@ struct QuizView: View {
|
|||||||
if currentIndex + 1 < questions.count {
|
if currentIndex + 1 < questions.count {
|
||||||
currentIndex += 1
|
currentIndex += 1
|
||||||
selectedOption = nil
|
selectedOption = nil
|
||||||
|
userInput = ""
|
||||||
} else {
|
} else {
|
||||||
finished = true
|
finished = true
|
||||||
}
|
}
|
||||||
@@ -140,6 +186,7 @@ struct QuizView: View {
|
|||||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
selectedOption = nil
|
selectedOption = nil
|
||||||
|
userInput = ""
|
||||||
wrongWords = []
|
wrongWords = []
|
||||||
finished = false
|
finished = false
|
||||||
startDate = .now
|
startDate = .now
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct WordListView: View {
|
|||||||
Menu {
|
Menu {
|
||||||
Button("英选汉") { startQuiz(.enToZh) }
|
Button("英选汉") { startQuiz(.enToZh) }
|
||||||
Button("汉选英") { startQuiz(.zhToEn) }
|
Button("汉选英") { startQuiz(.zhToEn) }
|
||||||
|
Button("拼写") { startQuiz(.spelling) }
|
||||||
} label: {
|
} label: {
|
||||||
Text("测验")
|
Text("测验")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user