实现背单词首页功能:内置分类词库,支持分类浏览、单词详情和离线发音

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 23:20:30 +08:00
parent 1a1563cd11
commit f5dd7fa724
8 changed files with 349 additions and 13 deletions
+15 -13
View File
@@ -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() }
}
}
@@ -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
}
@@ -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": "宝宝在睡觉。" }
]
}
]
}
@@ -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
}
}
@@ -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
}
}
}
@@ -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: "苹果是红色的。")
])
])
}
@@ -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())
}
}
@@ -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: "苹果是红色的。")
]))
}
}