From f5dd7fa72436f27808fc9bff6a99b1f03d0195eb Mon Sep 17 00:00:00 2001 From: fish Date: Fri, 17 Jul 2026 23:20:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=83=8C=E5=8D=95=E8=AF=8D?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E8=AF=8D=E5=BA=93=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E6=B5=8F=E8=A7=88=E3=80=81=E5=8D=95=E8=AF=8D?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=92=8C=E7=A6=BB=E7=BA=BF=E5=8F=91=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- LearnEnglish/LearnEnglish/ContentView.swift | 28 +++--- LearnEnglish/LearnEnglish/Models/Word.swift | 22 +++++ .../LearnEnglish/Resources/words.json | 90 +++++++++++++++++++ .../LearnEnglish/Services/SpeechService.swift | 33 +++++++ .../LearnEnglish/Services/WordStore.swift | 35 ++++++++ .../LearnEnglish/Views/CategoryListView.swift | 41 +++++++++ .../LearnEnglish/Views/WordDetailView.swift | 81 +++++++++++++++++ .../LearnEnglish/Views/WordListView.swift | 32 +++++++ 8 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 LearnEnglish/LearnEnglish/Models/Word.swift create mode 100644 LearnEnglish/LearnEnglish/Resources/words.json create mode 100644 LearnEnglish/LearnEnglish/Services/SpeechService.swift create mode 100644 LearnEnglish/LearnEnglish/Services/WordStore.swift create mode 100644 LearnEnglish/LearnEnglish/Views/CategoryListView.swift create mode 100644 LearnEnglish/LearnEnglish/Views/WordDetailView.swift create mode 100644 LearnEnglish/LearnEnglish/Views/WordListView.swift diff --git a/LearnEnglish/LearnEnglish/ContentView.swift b/LearnEnglish/LearnEnglish/ContentView.swift index d915cb2..363dd2b 100644 --- a/LearnEnglish/LearnEnglish/ContentView.swift +++ b/LearnEnglish/LearnEnglish/ContentView.swift @@ -1,21 +1,23 @@ -// -// ContentView.swift -// LearnEnglish -// -// Created by fish on 2026/7/17. -// - import SwiftUI struct ContentView: View { + @State private var store = WordStore() + @State private var speech = SpeechService() + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + Group { + if let error = store.loadError { + ContentUnavailableView( + "词库加载失败", + systemImage: "exclamationmark.triangle", + description: Text(error) + ) + } else { + CategoryListView(categories: store.categories) + } } - .padding() + .environment(speech) + .onAppear { store.load() } } } diff --git a/LearnEnglish/LearnEnglish/Models/Word.swift b/LearnEnglish/LearnEnglish/Models/Word.swift new file mode 100644 index 0000000..b52e4d8 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Models/Word.swift @@ -0,0 +1,22 @@ +import Foundation + +struct WordBank: Codable { + let version: Int + let categories: [WordCategory] +} + +struct WordCategory: Codable, Identifiable, Hashable { + let id: String + let name: String + let icon: String + let words: [Word] +} + +struct Word: Codable, Identifiable, Hashable { + var id: String { word } + let word: String + let phonetic: String + let meaning: String + let example: String + let exampleMeaning: String +} diff --git a/LearnEnglish/LearnEnglish/Resources/words.json b/LearnEnglish/LearnEnglish/Resources/words.json new file mode 100644 index 0000000..2a358d5 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Resources/words.json @@ -0,0 +1,90 @@ +{ + "version": 1, + "categories": [ + { + "id": "colors", + "name": "颜色", + "icon": "paintpalette.fill", + "words": [ + { "word": "red", "phonetic": "/red/", "meaning": "红色", "example": "The apple is red.", "exampleMeaning": "这个苹果是红色的。" }, + { "word": "blue", "phonetic": "/bluː/", "meaning": "蓝色", "example": "The sky is blue.", "exampleMeaning": "天空是蓝色的。" }, + { "word": "green", "phonetic": "/ɡriːn/", "meaning": "绿色", "example": "The grass is green.", "exampleMeaning": "草地是绿色的。" }, + { "word": "yellow", "phonetic": "/ˈjeloʊ/", "meaning": "黄色", "example": "The banana is yellow.", "exampleMeaning": "香蕉是黄色的。" }, + { "word": "black", "phonetic": "/blæk/", "meaning": "黑色", "example": "The cat is black.", "exampleMeaning": "这只猫是黑色的。" }, + { "word": "white", "phonetic": "/waɪt/", "meaning": "白色", "example": "The snow is white.", "exampleMeaning": "雪是白色的。" }, + { "word": "orange", "phonetic": "/ˈɔːrɪndʒ/", "meaning": "橙色", "example": "The orange is orange.", "exampleMeaning": "橙子是橙色的。" }, + { "word": "purple", "phonetic": "/ˈpɜːrpl/", "meaning": "紫色", "example": "The grapes are purple.", "exampleMeaning": "葡萄是紫色的。" }, + { "word": "pink", "phonetic": "/pɪŋk/", "meaning": "粉色", "example": "The flower is pink.", "exampleMeaning": "这朵花是粉色的。" }, + { "word": "brown", "phonetic": "/braʊn/", "meaning": "棕色", "example": "The bear is brown.", "exampleMeaning": "这只熊是棕色的。" } + ] + }, + { + "id": "animals", + "name": "动物", + "icon": "pawprint.fill", + "words": [ + { "word": "cat", "phonetic": "/kæt/", "meaning": "猫", "example": "The cat is cute.", "exampleMeaning": "这只猫很可爱。" }, + { "word": "dog", "phonetic": "/dɔːɡ/", "meaning": "狗", "example": "The dog is running.", "exampleMeaning": "狗在跑。" }, + { "word": "bird", "phonetic": "/bɜːrd/", "meaning": "鸟", "example": "The bird can fly.", "exampleMeaning": "鸟会飞。" }, + { "word": "fish", "phonetic": "/fɪʃ/", "meaning": "鱼", "example": "The fish swims fast.", "exampleMeaning": "鱼游得很快。" }, + { "word": "rabbit", "phonetic": "/ˈræbɪt/", "meaning": "兔子", "example": "The rabbit likes carrots.", "exampleMeaning": "兔子喜欢胡萝卜。" }, + { "word": "tiger", "phonetic": "/ˈtaɪɡər/", "meaning": "老虎", "example": "The tiger is strong.", "exampleMeaning": "老虎很强壮。" }, + { "word": "elephant", "phonetic": "/ˈelɪfənt/", "meaning": "大象", "example": "The elephant has a long nose.", "exampleMeaning": "大象有长长的鼻子。" }, + { "word": "monkey", "phonetic": "/ˈmʌŋki/", "meaning": "猴子", "example": "The monkey eats bananas.", "exampleMeaning": "猴子吃香蕉。" }, + { "word": "panda", "phonetic": "/ˈpændə/", "meaning": "熊猫", "example": "The panda is black and white.", "exampleMeaning": "熊猫是黑白相间的。" }, + { "word": "lion", "phonetic": "/ˈlaɪən/", "meaning": "狮子", "example": "The lion is the king of animals.", "exampleMeaning": "狮子是动物之王。" } + ] + }, + { + "id": "fruits", + "name": "水果", + "icon": "leaf.fill", + "words": [ + { "word": "apple", "phonetic": "/ˈæpl/", "meaning": "苹果", "example": "I eat an apple every day.", "exampleMeaning": "我每天吃一个苹果。" }, + { "word": "banana", "phonetic": "/bəˈnænə/", "meaning": "香蕉", "example": "Monkeys like bananas.", "exampleMeaning": "猴子喜欢香蕉。" }, + { "word": "orange", "phonetic": "/ˈɔːrɪndʒ/", "meaning": "橙子", "example": "The orange is sweet.", "exampleMeaning": "这个橙子很甜。" }, + { "word": "grape", "phonetic": "/ɡreɪp/", "meaning": "葡萄", "example": "Grapes can make wine.", "exampleMeaning": "葡萄可以酿酒。" }, + { "word": "watermelon", "phonetic": "/ˈwɔːtərmelən/", "meaning": "西瓜", "example": "Watermelon is a summer fruit.", "exampleMeaning": "西瓜是夏天的水果。" }, + { "word": "strawberry", "phonetic": "/ˈstrɔːberi/", "meaning": "草莓", "example": "The strawberry is red and sweet.", "exampleMeaning": "草莓又红又甜。" }, + { "word": "peach", "phonetic": "/piːtʃ/", "meaning": "桃子", "example": "The peach is juicy.", "exampleMeaning": "桃子多汁。" }, + { "word": "pear", "phonetic": "/per/", "meaning": "梨", "example": "The pear is crisp.", "exampleMeaning": "梨很脆。" }, + { "word": "mango", "phonetic": "/ˈmæŋɡoʊ/", "meaning": "芒果", "example": "Mangoes grow in hot places.", "exampleMeaning": "芒果生长在炎热的地方。" }, + { "word": "cherry", "phonetic": "/ˈtʃeri/", "meaning": "樱桃", "example": "Cherries are small and red.", "exampleMeaning": "樱桃又小又红。" } + ] + }, + { + "id": "numbers", + "name": "数字", + "icon": "number", + "words": [ + { "word": "one", "phonetic": "/wʌn/", "meaning": "一", "example": "I have one book.", "exampleMeaning": "我有一本书。" }, + { "word": "two", "phonetic": "/tuː/", "meaning": "二", "example": "Two birds are singing.", "exampleMeaning": "两只鸟在唱歌。" }, + { "word": "three", "phonetic": "/θriː/", "meaning": "三", "example": "Three cats are sleeping.", "exampleMeaning": "三只猫在睡觉。" }, + { "word": "four", "phonetic": "/fɔːr/", "meaning": "四", "example": "The table has four legs.", "exampleMeaning": "桌子有四条腿。" }, + { "word": "five", "phonetic": "/faɪv/", "meaning": "五", "example": "I have five fingers.", "exampleMeaning": "我有五根手指。" }, + { "word": "six", "phonetic": "/sɪks/", "meaning": "六", "example": "Six ducks are swimming.", "exampleMeaning": "六只鸭子在游泳。" }, + { "word": "seven", "phonetic": "/ˈsevn/", "meaning": "七", "example": "A week has seven days.", "exampleMeaning": "一周有七天。" }, + { "word": "eight", "phonetic": "/eɪt/", "meaning": "八", "example": "The spider has eight legs.", "exampleMeaning": "蜘蛛有八条腿。" }, + { "word": "nine", "phonetic": "/naɪn/", "meaning": "九", "example": "Nine students are in the room.", "exampleMeaning": "房间里有九个学生。" }, + { "word": "ten", "phonetic": "/ten/", "meaning": "十", "example": "I can count to ten.", "exampleMeaning": "我能数到十。" } + ] + }, + { + "id": "family", + "name": "家庭", + "icon": "house.fill", + "words": [ + { "word": "father", "phonetic": "/ˈfɑːðər/", "meaning": "爸爸", "example": "My father is tall.", "exampleMeaning": "我爸爸很高。" }, + { "word": "mother", "phonetic": "/ˈmʌðər/", "meaning": "妈妈", "example": "My mother cooks well.", "exampleMeaning": "我妈妈做饭很好吃。" }, + { "word": "brother", "phonetic": "/ˈbrʌðər/", "meaning": "兄弟", "example": "My brother likes soccer.", "exampleMeaning": "我哥哥喜欢足球。" }, + { "word": "sister", "phonetic": "/ˈsɪstər/", "meaning": "姐妹", "example": "My sister can draw.", "exampleMeaning": "我妹妹会画画。" }, + { "word": "grandfather", "phonetic": "/ˈɡrænfɑːðər/", "meaning": "爷爷/外公", "example": "My grandfather tells stories.", "exampleMeaning": "我爷爷会讲故事。" }, + { "word": "grandmother", "phonetic": "/ˈɡrænmʌðər/", "meaning": "奶奶/外婆", "example": "My grandmother makes dumplings.", "exampleMeaning": "我奶奶会包饺子。" }, + { "word": "uncle", "phonetic": "/ˈʌŋkl/", "meaning": "叔叔/舅舅", "example": "My uncle lives in Beijing.", "exampleMeaning": "我叔叔住在北京。" }, + { "word": "aunt", "phonetic": "/ænt/", "meaning": "阿姨/姑姑", "example": "My aunt gives me gifts.", "exampleMeaning": "我阿姨送我礼物。" }, + { "word": "cousin", "phonetic": "/ˈkʌzn/", "meaning": "堂/表兄弟姐妹", "example": "My cousin and I play together.", "exampleMeaning": "我和我表哥一起玩。" }, + { "word": "baby", "phonetic": "/ˈbeɪbi/", "meaning": "婴儿", "example": "The baby is sleeping.", "exampleMeaning": "宝宝在睡觉。" } + ] + } + ] +} diff --git a/LearnEnglish/LearnEnglish/Services/SpeechService.swift b/LearnEnglish/LearnEnglish/Services/SpeechService.swift new file mode 100644 index 0000000..4392c81 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Services/SpeechService.swift @@ -0,0 +1,33 @@ +import AVFoundation + +@Observable +final class SpeechService: NSObject { + private let synthesizer = AVSpeechSynthesizer() + private(set) var speakingText: String? + + override init() { + super.init() + synthesizer.delegate = self + } + + func speak(_ text: String) { + if synthesizer.isSpeaking { + synthesizer.stopSpeaking(at: .immediate) + } + let utterance = AVSpeechUtterance(string: text) + utterance.voice = AVSpeechSynthesisVoice(language: "en-US") + utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9 + speakingText = text + synthesizer.speak(utterance) + } +} + +extension SpeechService: AVSpeechSynthesizerDelegate { + func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + speakingText = nil + } + + func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { + speakingText = nil + } +} diff --git a/LearnEnglish/LearnEnglish/Services/WordStore.swift b/LearnEnglish/LearnEnglish/Services/WordStore.swift new file mode 100644 index 0000000..49df848 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Services/WordStore.swift @@ -0,0 +1,35 @@ +import Foundation + +enum WordStoreError: Error, LocalizedError { + case fileNotFound + case decodingFailed(Error) + + var errorDescription: String? { + switch self { + case .fileNotFound: return "词库文件缺失" + case .decodingFailed(let error): return "词库解析失败:\(error.localizedDescription)" + } + } +} + +@Observable +final class WordStore { + private(set) var bank: WordBank? + private(set) var loadError: String? + + var categories: [WordCategory] { bank?.categories ?? [] } + + func load() { + guard bank == nil, loadError == nil else { return } + guard let url = Bundle.main.url(forResource: "words", withExtension: "json") else { + loadError = WordStoreError.fileNotFound.localizedDescription + return + } + do { + let data = try Data(contentsOf: url) + bank = try JSONDecoder().decode(WordBank.self, from: data) + } catch { + loadError = WordStoreError.decodingFailed(error).localizedDescription + } + } +} diff --git a/LearnEnglish/LearnEnglish/Views/CategoryListView.swift b/LearnEnglish/LearnEnglish/Views/CategoryListView.swift new file mode 100644 index 0000000..9825b0f --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/CategoryListView.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct CategoryListView: View { + let categories: [WordCategory] + + var body: some View { + NavigationStack { + List(categories) { category in + NavigationLink(value: category) { + HStack(spacing: 16) { + Image(systemName: category.icon) + .font(.title2) + .foregroundStyle(.white) + .frame(width: 44, height: 44) + .background(.tint, in: RoundedRectangle(cornerRadius: 10)) + VStack(alignment: .leading, spacing: 4) { + Text(category.name) + .font(.headline) + Text("\(category.words.count) 个单词") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 4) + } + } + .navigationTitle("单词分类") + .navigationDestination(for: WordCategory.self) { category in + WordListView(category: category) + } + } + } +} + +#Preview { + CategoryListView(categories: [ + WordCategory(id: "colors", name: "颜色", icon: "paintpalette.fill", words: [ + Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。") + ]) + ]) +} diff --git a/LearnEnglish/LearnEnglish/Views/WordDetailView.swift b/LearnEnglish/LearnEnglish/Views/WordDetailView.swift new file mode 100644 index 0000000..a18cd90 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/WordDetailView.swift @@ -0,0 +1,81 @@ +import SwiftUI + +struct WordDetailView: View { + let word: Word + @Environment(SpeechService.self) private var speech + + private var isSpeaking: Bool { + speech.speakingText == word.word + } + + var body: some View { + ScrollView { + VStack(spacing: 32) { + VStack(spacing: 12) { + Text(word.word) + .font(.system(size: 48, weight: .bold)) + Text(word.phonetic) + .font(.title3) + .foregroundStyle(.secondary) + Button { + speech.speak(word.word) + } label: { + Label(isSpeaking ? "朗读中" : "发音", systemImage: isSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2.fill") + .font(.headline) + .padding(.horizontal, 24) + .padding(.vertical, 10) + } + .buttonStyle(.borderedProminent) + .disabled(isSpeaking) + } + + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 6) { + Text("释义") + .font(.caption) + .foregroundStyle(.secondary) + Text(word.meaning) + .font(.title2) + } + + Divider() + + VStack(alignment: .leading, spacing: 6) { + Text("例句") + .font(.caption) + .foregroundStyle(.secondary) + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + Text(word.example) + .font(.body) + Text(word.exampleMeaning) + .font(.subheadline) + .foregroundStyle(.secondary) + } + Spacer() + Button { + speech.speak(word.example) + } label: { + Image(systemName: "speaker.wave.2") + } + .buttonStyle(.borderless) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 16)) + } + .padding() + } + .navigationTitle(word.word) + .navigationBarTitleDisplayMode(.inline) + } +} + +#Preview { + NavigationStack { + WordDetailView(word: Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")) + .environment(SpeechService()) + } +} diff --git a/LearnEnglish/LearnEnglish/Views/WordListView.swift b/LearnEnglish/LearnEnglish/Views/WordListView.swift new file mode 100644 index 0000000..eea458f --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/WordListView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct WordListView: View { + let category: WordCategory + + var body: some View { + List(category.words) { word in + NavigationLink(value: word) { + VStack(alignment: .leading, spacing: 4) { + Text(word.word) + .font(.headline) + Text(word.meaning) + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding(.vertical, 2) + } + } + .navigationTitle(category.name) + .navigationDestination(for: Word.self) { word in + WordDetailView(word: word) + } + } +} + +#Preview { + NavigationStack { + WordListView(category: WordCategory(id: "colors", name: "颜色", icon: "paintpalette.fill", words: [ + Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。") + ])) + } +}