Compare commits

..

3 Commits

Author SHA1 Message Date
fish 09800c5b17 单词列表卡片增加学习按钮,点击跳转拼写练习
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-20 08:31:53 +08:00
fish a3c7d197b1 学习模式拼写页面加入刷新倒计时提示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-20 08:27:39 +08:00
fish 01b943185f 修复拼写算法,按比例空缺并保证至少展示一个字母
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-20 08:25:48 +08:00
4 changed files with 47 additions and 5 deletions
+3 -3
View File
@@ -70,9 +70,9 @@ enum QuizGenerator {
private static func blankWord(_ word: String) -> String { private static func blankWord(_ word: String) -> String {
let chars = Array(word) let chars = Array(word)
let letterPositions = chars.indices.filter { chars[$0].isLetter } let letterPositions = chars.indices.filter { chars[$0].isLetter }
guard letterPositions.count > 2 else { return String(repeating: "_", count: chars.count) } guard !letterPositions.isEmpty else { return word }
let blanks = max(1, Int(Double(letterPositions.count) * 0.4)) let blankCount = min(letterPositions.count - 1, Int(ceil(Double(letterPositions.count) * 0.4)))
let indices = Set(letterPositions.shuffled().prefix(blanks)) let indices = Set(letterPositions.shuffled().prefix(blankCount))
return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined() return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined()
} }
} }
@@ -21,9 +21,11 @@ final class QuizViewController: UIViewController {
private let spellingHintLabel = UILabel() private let spellingHintLabel = UILabel()
private let resultLabel = UILabel() private let resultLabel = UILabel()
private let resultPhoneticLabel = UILabel() private let resultPhoneticLabel = UILabel()
private let countdownLabel = UILabel()
private let nextButton = UIButton(type: .system) private let nextButton = UIButton(type: .system)
private var optionButtons: [UIButton] = [] private var optionButtons: [UIButton] = []
private var optionIcons: [UIImageView] = [] private var optionIcons: [UIImageView] = []
private var countdownTimer: Timer?
private let promptStack = UIStackView() private let promptStack = UIStackView()
private let inputStack = UIStackView() private let inputStack = UIStackView()
@@ -48,6 +50,10 @@ final class QuizViewController: UIViewController {
@available(*, unavailable) @available(*, unavailable)
required init?(coder: NSCoder) { fatalError() } required init?(coder: NSCoder) { fatalError() }
deinit {
countdownTimer?.invalidate()
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground view.backgroundColor = .systemBackground
@@ -135,7 +141,11 @@ final class QuizViewController: UIViewController {
resultStack.axis = .vertical resultStack.axis = .vertical
resultStack.spacing = 12 resultStack.spacing = 12
[resultLabel, resultPhoneticLabel, nextButton].forEach { resultStack.addArrangedSubview($0) } countdownLabel.font = .preferredFont(forTextStyle: .caption1)
countdownLabel.textColor = .tertiaryLabel
countdownLabel.textAlignment = .center
countdownLabel.isHidden = true
[resultLabel, resultPhoneticLabel, countdownLabel, nextButton].forEach { resultStack.addArrangedSubview($0) }
let mainStack = UIStackView(arrangedSubviews: [headerStack, promptStack, inputStack, resultStack, UIView()]) let mainStack = UIStackView(arrangedSubviews: [headerStack, promptStack, inputStack, resultStack, UIView()])
mainStack.axis = .vertical mainStack.axis = .vertical
@@ -358,6 +368,18 @@ final class QuizViewController: UIViewController {
if config.isStudyMode { if config.isStudyMode {
nextButton.isHidden = true nextButton.isHidden = true
let delay: TimeInterval = correct ? 1.0 : 3.0 let delay: TimeInterval = correct ? 1.0 : 3.0
countdownLabel.text = "\(Int(delay)) 秒后继续"
countdownLabel.isHidden = false
countdownTimer?.invalidate()
var remaining = Int(delay)
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
remaining -= 1
if remaining > 0 {
self?.countdownLabel.text = "\(remaining) 秒后继续"
} else {
timer.invalidate()
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.restart() self?.restart()
} }
@@ -440,6 +462,8 @@ final class QuizViewController: UIViewController {
} }
private func restart() { private func restart() {
countdownTimer?.invalidate()
countdownLabel.isHidden = true
view.subviews.compactMap { $0 as? QuizResultView }.forEach { $0.removeFromSuperview() } view.subviews.compactMap { $0 as? QuizResultView }.forEach { $0.removeFromSuperview() }
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words) questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
currentIndex = 0 currentIndex = 0
@@ -5,10 +5,12 @@ final class WordCardCell: UITableViewCell {
private let wordLabel = UILabel() private let wordLabel = UILabel()
private let phoneticLabel = UILabel() private let phoneticLabel = UILabel()
private let meaningLabel = UILabel() private let meaningLabel = UILabel()
private let learnButton = UIButton(type: .system)
private let speakerButton = UIButton(type: .system) private let speakerButton = UIButton(type: .system)
private var word: Word? private var word: Word?
var onSpeak: ((Word) -> Void)? var onSpeak: ((Word) -> Void)?
var onLearn: ((Word) -> Void)?
var zoomSourceView: UIView { cardView } var zoomSourceView: UIView { cardView }
@@ -39,6 +41,15 @@ final class WordCardCell: UITableViewCell {
textStack.axis = .vertical textStack.axis = .vertical
textStack.spacing = 8 textStack.spacing = 8
learnButton.setImage(UIImage(systemName: "book",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)),
for: .normal)
learnButton.tintColor = Theme.Color.accent
learnButton.addAction(UIAction { [weak self] _ in
guard let self, let word else { return }
onLearn?(word)
}, for: .touchUpInside)
speakerButton.tintColor = Theme.Color.accent speakerButton.tintColor = Theme.Color.accent
speakerButton.addAction(UIAction { [weak self] _ in speakerButton.addAction(UIAction { [weak self] _ in
guard let self, let word else { return } guard let self, let word else { return }
@@ -46,7 +57,7 @@ final class WordCardCell: UITableViewCell {
}, for: .touchUpInside) }, for: .touchUpInside)
speakerButton.setContentHuggingPriority(.required, for: .horizontal) speakerButton.setContentHuggingPriority(.required, for: .horizontal)
let row = UIStackView(arrangedSubviews: [textStack, speakerButton]) let row = UIStackView(arrangedSubviews: [textStack, learnButton, speakerButton])
row.axis = .horizontal row.axis = .horizontal
row.alignment = .center row.alignment = .center
row.spacing = 8 row.spacing = 8
@@ -57,6 +68,8 @@ final class WordCardCell: UITableViewCell {
row.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14), row.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14),
row.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8), row.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
row.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -10), row.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -10),
learnButton.widthAnchor.constraint(equalToConstant: 44),
learnButton.heightAnchor.constraint(equalToConstant: 44),
speakerButton.widthAnchor.constraint(equalToConstant: 44), speakerButton.widthAnchor.constraint(equalToConstant: 44),
speakerButton.heightAnchor.constraint(equalToConstant: 44) speakerButton.heightAnchor.constraint(equalToConstant: 44)
]) ])
@@ -148,6 +148,11 @@ extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
cell.onSpeak = { word in cell.onSpeak = { word in
SpeechService.shared.speak(word.word) SpeechService.shared.speak(word.word)
} }
cell.onLearn = { [weak self] word in
guard let self else { return }
let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true)
navigationController?.pushViewController(QuizViewController(config: config), animated: true)
}
return cell return cell
} }