From 75b967893bb3ea122e29dd88682729ebf961c506 Mon Sep 17 00:00:00 2001 From: fish Date: Sun, 19 Jul 2026 23:10:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=88=86=E7=B1=BB=E6=94=AF?= =?UTF-8?q?=E6=8C=81=201-999=20=E6=95=B0=E5=AD=97=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E8=A7=84=E5=88=99=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E7=94=9F=E6=88=90=E8=AF=8D=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../LearnEnglish/Models/NumberWords.swift | 115 ++++++++++++++++++ LearnEnglish/LearnEnglish/Models/Quiz.swift | 8 +- .../LearnEnglish/Resources/words.json | 10 +- .../LearnEnglish/Services/WordStore.swift | 8 +- .../Views/QuizSetupViewController.swift | 23 +++- .../Views/SpellingInputView.swift | 20 ++- 6 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 LearnEnglish/LearnEnglish/Models/NumberWords.swift diff --git a/LearnEnglish/LearnEnglish/Models/NumberWords.swift b/LearnEnglish/LearnEnglish/Models/NumberWords.swift new file mode 100644 index 0000000..7e34855 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Models/NumberWords.swift @@ -0,0 +1,115 @@ +import Foundation + +enum NumberWords { + static let range = 1...999 + + private static let atomNumbers: [String: Int] = [ + "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, + "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, + "eleven": 11, "twelve": 12, "thirteen": 13, "fourteen": 14, "fifteen": 15, + "sixteen": 16, "seventeen": 17, "eighteen": 18, "nineteen": 19, "twenty": 20, + "thirty": 30, "forty": 40, "fifty": 50, "sixty": 60, + "seventy": 70, "eighty": 80, "ninety": 90, "hundred": 100 + ] + + private static let ones = [ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", "thirteen", "fourteen", "fifteen", + "sixteen", "seventeen", "eighteen", "nineteen" + ] + + private static let onesPhonetics = [ + "wʌn", "tuː", "θriː", "fɔːr", "faɪv", "sɪks", "ˈsevn", "eɪt", "naɪn", "ten", + "ɪˈlevn", "twelv", "ˌθɜːrˈtiːn", "ˌfɔːrˈtiːn", "ˌfɪfˈtiːn", + "ˌsɪksˈtiːn", "ˌsevnˈtiːn", "ˌeɪˈtiːn", "ˌnaɪnˈtiːn" + ] + + private static let tens: [Int: String] = [ + 20: "twenty", 30: "thirty", 40: "forty", 50: "fifty", + 60: "sixty", 70: "seventy", 80: "eighty", 90: "ninety" + ] + + private static let tensPhonetics: [Int: String] = [ + 20: "ˈtwenti", 30: "ˈθɜːrti", 40: "ˈfɔːrti", 50: "ˈfɪfti", + 60: "ˈsɪksti", 70: "ˈsevnti", 80: "ˈeɪti", 90: "ˈnaɪnti" + ] + + static func makeWords(atoms: [Word]) -> [Word] { + var byNumber: [Int: Word] = [:] + for atom in atoms { + if let number = atomNumbers[atom.word] { + byNumber[number] = atom + } + } + return range.map { byNumber[$0] ?? makeWord($0) } + } + + static func english(_ n: Int) -> String { + switch n { + case 1...19: + return ones[n - 1] + case 20...99: + let ten = n / 10 * 10 + let one = n % 10 + return one == 0 ? tens[ten]! : "\(tens[ten]!)-\(ones[one - 1])" + case 100...999: + let head = "\(ones[n / 100 - 1]) hundred" + let rest = n % 100 + return rest == 0 ? head : "\(head) \(english(rest))" + default: + return "\(n)" + } + } + + static func phonetic(_ n: Int) -> String { + switch n { + case 1...19: + return onesPhonetics[n - 1] + case 20...99: + let ten = n / 10 * 10 + let one = n % 10 + return one == 0 ? tensPhonetics[ten]! : "\(tensPhonetics[ten]!) \(onesPhonetics[one - 1])" + case 100...999: + let head = "\(onesPhonetics[n / 100 - 1]) ˈhʌndrəd" + let rest = n % 100 + return rest == 0 ? head : "\(head) \(phonetic(rest))" + default: + return "" + } + } + + static func chinese(_ n: Int) -> String { + let digits = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"] + switch n { + case 0...9: + return digits[n] + case 10...19: + return "十" + (n % 10 == 0 ? "" : digits[n % 10]) + case 20...99: + return digits[n / 10] + "十" + (n % 10 == 0 ? "" : digits[n % 10]) + case 100...999: + let head = digits[n / 100] + "百" + let rest = n % 100 + switch rest { + case 0: + return head + case 1...9: + return head + "零" + digits[rest] + case 10...19: + return head + "一十" + (rest % 10 == 0 ? "" : digits[rest % 10]) + default: + return head + digits[rest / 10] + "十" + (rest % 10 == 0 ? "" : digits[rest % 10]) + } + default: + return "\(n)" + } + } + + private static func makeWord(_ n: Int) -> Word { + Word(word: english(n), + phonetic: "/\(phonetic(n))/", + meaning: chinese(n), + example: "I can count to \(english(n)).", + exampleMeaning: "我能数到\(chinese(n))。") + } +} diff --git a/LearnEnglish/LearnEnglish/Models/Quiz.swift b/LearnEnglish/LearnEnglish/Models/Quiz.swift index b8ed3a7..b265ac8 100644 --- a/LearnEnglish/LearnEnglish/Models/Quiz.swift +++ b/LearnEnglish/LearnEnglish/Models/Quiz.swift @@ -62,10 +62,10 @@ enum QuizGenerator { 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.. 2 else { return String(repeating: "_", count: chars.count) } + let blanks = max(1, Int(Double(letterPositions.count) * 0.4)) + let indices = Set(letterPositions.shuffled().prefix(blanks)) return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined() } } diff --git a/LearnEnglish/LearnEnglish/Resources/words.json b/LearnEnglish/LearnEnglish/Resources/words.json index a6e76d6..a3fff4b 100644 --- a/LearnEnglish/LearnEnglish/Resources/words.json +++ b/LearnEnglish/LearnEnglish/Resources/words.json @@ -166,7 +166,15 @@ { "word": "seventeen", "phonetic": "/ˌsevnˈtiːn/", "meaning": "十七", "example": "He got seventeen points.", "exampleMeaning": "他得了十七分。" }, { "word": "eighteen", "phonetic": "/ˌeɪˈtiːn/", "meaning": "十八", "example": "She is eighteen years old.", "exampleMeaning": "她十八岁。" }, { "word": "nineteen", "phonetic": "/ˌnaɪnˈtiːn/", "meaning": "十九", "example": "Nineteen people came to the party.", "exampleMeaning": "十九个人来参加了聚会。" }, - { "word": "twenty", "phonetic": "/ˈtwenti/", "meaning": "二十", "example": "I have twenty yuan.", "exampleMeaning": "我有二十元。" } + { "word": "twenty", "phonetic": "/ˈtwenti/", "meaning": "二十", "example": "I have twenty yuan.", "exampleMeaning": "我有二十元。" }, + { "word": "thirty", "phonetic": "/ˈθɜːrti/", "meaning": "三十", "example": "She has thirty stamps.", "exampleMeaning": "她有三十张邮票。" }, + { "word": "forty", "phonetic": "/ˈfɔːrti/", "meaning": "四十", "example": "There are forty students.", "exampleMeaning": "有四十个学生。" }, + { "word": "fifty", "phonetic": "/ˈfɪfti/", "meaning": "五十", "example": "My grandfather is fifty.", "exampleMeaning": "我爷爷五十岁。" }, + { "word": "sixty", "phonetic": "/ˈsɪksti/", "meaning": "六十", "example": "An hour has sixty minutes.", "exampleMeaning": "一小时有六十分钟。" }, + { "word": "seventy", "phonetic": "/ˈsevnti/", "meaning": "七十", "example": "My grandmother is seventy.", "exampleMeaning": "我奶奶七十岁。" }, + { "word": "eighty", "phonetic": "/ˈeɪti/", "meaning": "八十", "example": "He can count to eighty.", "exampleMeaning": "他能数到八十。" }, + { "word": "ninety", "phonetic": "/ˈnaɪnti/", "meaning": "九十", "example": "The book has ninety pages.", "exampleMeaning": "这本书有九十页。" }, + { "word": "hundred", "phonetic": "/ˈhʌndrəd/", "meaning": "百", "example": "One hundred people came.", "exampleMeaning": "来了一百个人。" } ] }, { diff --git a/LearnEnglish/LearnEnglish/Services/WordStore.swift b/LearnEnglish/LearnEnglish/Services/WordStore.swift index d2119e4..78fb553 100644 --- a/LearnEnglish/LearnEnglish/Services/WordStore.swift +++ b/LearnEnglish/LearnEnglish/Services/WordStore.swift @@ -26,7 +26,13 @@ final class WordStore { } do { let data = try Data(contentsOf: url) - bank = try JSONDecoder().decode(WordBank.self, from: data) + let decoded = try JSONDecoder().decode(WordBank.self, from: data) + let categories = decoded.categories.map { category -> WordCategory in + guard category.id == "numbers" else { return category } + return WordCategory(id: category.id, name: category.name, icon: category.icon, + words: NumberWords.makeWords(atoms: category.words)) + } + bank = WordBank(version: decoded.version, categories: categories) } catch { loadError = WordStoreError.decodingFailed(error).localizedDescription } diff --git a/LearnEnglish/LearnEnglish/Views/QuizSetupViewController.swift b/LearnEnglish/LearnEnglish/Views/QuizSetupViewController.swift index 3700d5f..65b06c8 100644 --- a/LearnEnglish/LearnEnglish/Views/QuizSetupViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/QuizSetupViewController.swift @@ -162,9 +162,14 @@ final class QuizSetupViewController: UIViewController { } private func setupCountControl() { - countControl.insertSegment(withTitle: "全部 \(request.words.count) 题", at: 0, animated: false) - if request.words.count > 10 { - countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false) + if request.words.count > 50 { + countControl.insertSegment(withTitle: "随机 10 题", at: 0, animated: false) + countControl.insertSegment(withTitle: "随机 20 题", at: 1, animated: false) + } else { + countControl.insertSegment(withTitle: "全部 \(request.words.count) 题", at: 0, animated: false) + if request.words.count > 10 { + countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false) + } } countControl.selectedSegmentIndex = 0 } @@ -198,9 +203,15 @@ final class QuizSetupViewController: UIViewController { private func startQuiz() { guard let selectedIndex else { return } Haptics.selection() - let words = countControl.selectedSegmentIndex == 0 - ? request.words - : Array(request.words.shuffled().prefix(10)) + let words: [Word] + if request.words.count > 50 { + let count = countControl.selectedSegmentIndex == 0 ? 10 : 20 + words = Array(request.words.shuffled().prefix(count)) + } else if countControl.selectedSegmentIndex == 0 { + words = request.words + } else { + words = Array(request.words.shuffled().prefix(10)) + } let config = QuizConfig(mode: items[selectedIndex].mode, words: words) navigationController?.pushViewController(QuizViewController(config: config), animated: true) } diff --git a/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift b/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift index 9d5f706..10d76e0 100644 --- a/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift +++ b/LearnEnglish/LearnEnglish/Views/SpellingInputView.swift @@ -126,8 +126,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate { private func rebuildBoxes() { stack.arrangedSubviews.forEach { $0.removeFromSuperview() } + let count = CGFloat(max(chars.count, 1)) + let available = (window?.screen.bounds.width ?? 390) - 32 + var spacing: CGFloat = 6 + var boxWidth = (available - spacing * (count - 1)) / count + if boxWidth < 16 { + spacing = 3 + boxWidth = (available - spacing * (count - 1)) / count + } + boxWidth = min(36, boxWidth) + stack.spacing = spacing + let fontSize = min(22, max(12, boxWidth * 0.62)) boxViews = chars.enumerated().map { index, ch in - let box = makeBox() + let box = makeBox(fontSize: fontSize) box.tag = index if ch != "_", let label = box.viewWithTag(100) as? UILabel { label.text = ch.uppercased() @@ -136,7 +147,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate { box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:)))) stack.addArrangedSubview(box) NSLayoutConstraint.activate([ - box.widthAnchor.constraint(equalToConstant: 36), + box.widthAnchor.constraint(equalToConstant: boxWidth), box.heightAnchor.constraint(equalToConstant: 44) ]) return box @@ -158,7 +169,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate { activate() } - private func makeBox() -> UIView { + private func makeBox(fontSize: CGFloat) -> UIView { let box = UIView() box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15) box.layer.cornerRadius = 8 @@ -166,8 +177,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate { box.layer.borderColor = UIColor.separator.cgColor let label = UILabel() - label.font = .preferredFont(forTextStyle: .title2) - label.font = .systemFont(ofSize: label.font.pointSize, weight: .bold) + label.font = .systemFont(ofSize: fontSize, weight: .bold) label.isHidden = true label.tag = 100 box.addSubview(label)