饮食分类支持子分类分组,WordCategory 新增 subcategories 字段实现单词分层展示

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 22:49:50 +08:00
parent 0ed88622af
commit 3d2466edba
4 changed files with 130 additions and 50 deletions
@@ -9,6 +9,12 @@ struct WordCategory: Codable, Identifiable, Hashable {
let id: String
let name: String
let icon: String
var words: [Word]
var subcategories: [Subcategory]?
}
struct Subcategory: Codable, Hashable {
let name: String
let words: [Word]
}
+48 -7
View File
@@ -290,43 +290,82 @@
"id": "food_drink",
"name": "饮食",
"icon": "fork.knife",
"words": [],
"subcategories": [
{
"name": "主食",
"words": [
{ "word": "rice", "phonetic": "/raɪs/", "meaning": "米饭", "example": "I eat rice every day.", "exampleMeaning": "我每天吃米饭。" },
{ "word": "bread", "phonetic": "/bred/", "meaning": "面包", "example": "I have bread for breakfast.", "exampleMeaning": "我早餐吃面包。" },
{ "word": "noodle", "phonetic": "/ˈnuːdl/", "meaning": "面条", "example": "I like beef noodles.", "exampleMeaning": "我喜欢牛肉面。" },
{ "word": "dumpling", "phonetic": "/ˈdʌmplɪŋ/", "meaning": "饺子", "example": "We make dumplings together.", "exampleMeaning": "我们一起包饺子。" },
{ "word": "egg", "phonetic": "/eɡ/", "meaning": "鸡蛋", "example": "I have an egg for breakfast.", "exampleMeaning": "我早餐吃一个鸡蛋。" },
{ "word": "dumpling", "phonetic": "/ˈdʌmplɪŋ/", "meaning": "饺子", "example": "We make dumplings together.", "exampleMeaning": "我们一起包饺子。" }
]
},
{
"name": "肉类",
"words": [
{ "word": "meat", "phonetic": "/miːt/", "meaning": "肉", "example": "I like to eat meat.", "exampleMeaning": "我喜欢吃肉。" },
{ "word": "beef", "phonetic": "/biːf/", "meaning": "牛肉", "example": "The beef is tender.", "exampleMeaning": "牛肉很嫩。" },
{ "word": "pork", "phonetic": "/pɔːrk/", "meaning": "猪肉", "example": "I don't eat pork.", "exampleMeaning": "我不吃猪肉。" },
{ "word": "chicken", "phonetic": "/ˈtʃɪkɪn/", "meaning": "鸡肉", "example": "Fried chicken is popular.", "exampleMeaning": "炸鸡很受欢迎。" },
{ "word": "egg", "phonetic": "/eɡ/", "meaning": "鸡蛋", "example": "I have an egg for breakfast.", "exampleMeaning": "我早餐吃一个鸡蛋。" }
]
},
{
"name": "蔬菜",
"words": [
{ "word": "vegetable", "phonetic": "/ˈvedʒtəbl/", "meaning": "蔬菜", "example": "Eat more vegetables.", "exampleMeaning": "多吃蔬菜。" },
{ "word": "carrot", "phonetic": "/ˈkærət/", "meaning": "胡萝卜", "example": "Rabbits like carrots.", "exampleMeaning": "兔子喜欢胡萝卜。" },
{ "word": "tomato", "phonetic": "/təˈmeɪtoʊ/", "meaning": "番茄", "example": "The tomato is red.", "exampleMeaning": "番茄是红色的。" },
{ "word": "potato", "phonetic": "/pəˈteɪtoʊ/", "meaning": "土豆", "example": "Potatoes grow underground.", "exampleMeaning": "土豆长在地下。" },
{ "word": "onion", "phonetic": "/ˈʌnjən/", "meaning": "洋葱", "example": "The onion makes me cry.", "exampleMeaning": "洋葱让我流泪。" },
{ "word": "cucumber", "phonetic": "/ˈkjuːkʌmbər/", "meaning": "黄瓜", "example": "The cucumber is fresh.", "exampleMeaning": "黄瓜很新鲜。" },
{ "word": "cabbage", "phonetic": "/ˈkæbɪdʒ/", "meaning": "卷心菜", "example": "Cabbage is good for health.", "exampleMeaning": "卷心菜对健康有好处。" },
{ "word": "cabbage", "phonetic": "/ˈkæbɪdʒ/", "meaning": "卷心菜", "example": "Cabbage is good for health.", "exampleMeaning": "卷心菜对健康有好处。" }
]
},
{
"name": "饮品",
"words": [
{ "word": "water", "phonetic": "/ˈwɔːtər/", "meaning": "水", "example": "I drink water every day.", "exampleMeaning": "我每天喝水。" },
{ "word": "milk", "phonetic": "/mɪlk/", "meaning": "牛奶", "example": "I drink a glass of milk.", "exampleMeaning": "我喝一杯牛奶。" },
{ "word": "juice", "phonetic": "/dʒuːs/", "meaning": "果汁", "example": "Orange juice is sweet.", "exampleMeaning": "橙汁很甜。" },
{ "word": "tea", "phonetic": "/tiː/", "meaning": "茶", "example": "I drink tea in the afternoon.", "exampleMeaning": "我下午喝茶。" },
{ "word": "coffee", "phonetic": "/ˈkɔːfi/", "meaning": "咖啡", "example": "I need a cup of coffee.", "exampleMeaning": "我需要一杯咖啡。" },
{ "word": "beer", "phonetic": "/bɪr/", "meaning": "啤酒", "example": "He drinks beer with friends.", "exampleMeaning": "他和朋友喝啤酒。" },
{ "word": "wine", "phonetic": "/waɪn/", "meaning": "葡萄酒", "example": "Red wine is good for health.", "exampleMeaning": "红酒对健康有好处。" },
{ "word": "wine", "phonetic": "/waɪn/", "meaning": "葡萄酒", "example": "Red wine is good for health.", "exampleMeaning": "红酒对健康有好处。" }
]
},
{
"name": "调料",
"words": [
{ "word": "salt", "phonetic": "/sɔːlt/", "meaning": "盐", "example": "Don't add too much salt.", "exampleMeaning": "不要加太多盐。" },
{ "word": "sugar", "phonetic": "/ˈʃʊɡər/", "meaning": "糖", "example": "I put sugar in my coffee.", "exampleMeaning": "我在咖啡里加糖。" },
{ "word": "oil", "phonetic": "/ɔɪl/", "meaning": "油", "example": "Add some oil to the pan.", "exampleMeaning": "往锅里加点油。" },
{ "word": "sauce", "phonetic": "/sɔːs/", "meaning": "酱", "example": "The sauce is delicious.", "exampleMeaning": "这个酱很好吃。" },
{ "word": "sauce", "phonetic": "/sɔːs/", "meaning": "酱", "example": "The sauce is delicious.", "exampleMeaning": "这个酱很好吃。" }
]
},
{
"name": "餐食",
"words": [
{ "word": "breakfast", "phonetic": "/ˈbrekfəst/", "meaning": "早餐", "example": "Breakfast is the most important meal.", "exampleMeaning": "早餐是最重要的一餐。" },
{ "word": "lunch", "phonetic": "/lʌntʃ/", "meaning": "午餐", "example": "I have lunch at twelve.", "exampleMeaning": "我十二点吃午餐。" },
{ "word": "dinner", "phonetic": "/ˈdɪnər/", "meaning": "晚餐", "example": "We have dinner at six.", "exampleMeaning": "我们六点吃晚餐。" },
{ "word": "soup", "phonetic": "/suːp/", "meaning": "汤", "example": "The soup is hot.", "exampleMeaning": "汤是热的。" },
{ "word": "soup", "phonetic": "/suːp/", "meaning": "汤", "example": "The soup is hot.", "exampleMeaning": "汤是热的。" }
]
},
{
"name": "零食甜品",
"words": [
{ "word": "cake", "phonetic": "/keɪk/", "meaning": "蛋糕", "example": "I like chocolate cake.", "exampleMeaning": "我喜欢巧克力蛋糕。" },
{ "word": "cookie", "phonetic": "/ˈkʊki/", "meaning": "饼干", "example": "The cookie is crunchy.", "exampleMeaning": "饼干很脆。" },
{ "word": "ice cream", "phonetic": "/ˌaɪs ˈkriːm/", "meaning": "冰淇淋", "example": "Ice cream is cold and sweet.", "exampleMeaning": "冰淇淋又冰又甜。" },
{ "word": "chocolate", "phonetic": "/ˈtʃɔːklət/", "meaning": "巧克力", "example": "I love dark chocolate.", "exampleMeaning": "我爱黑巧克力。" },
{ "word": "candy", "phonetic": "/ˈkændi/", "meaning": "糖果", "example": "The child likes candy.", "exampleMeaning": "小孩喜欢糖果。" },
{ "word": "candy", "phonetic": "/ˈkændi/", "meaning": "糖果", "example": "The child likes candy.", "exampleMeaning": "小孩喜欢糖果。" }
]
},
{
"name": "其他",
"words": [
{ "word": "sandwich", "phonetic": "/ˈsænwɪtʃ/", "meaning": "三明治", "example": "I made a sandwich for lunch.", "exampleMeaning": "我做了三明治当午餐。" },
{ "word": "pizza", "phonetic": "/ˈpiːtsə/", "meaning": "披萨", "example": "Pizza is from Italy.", "exampleMeaning": "披萨来自意大利。" },
{ "word": "hamburger", "phonetic": "/ˈhæmbɜːrɡər/", "meaning": "汉堡", "example": "The hamburger is big.", "exampleMeaning": "这个汉堡很大。" },
@@ -336,3 +375,5 @@
}
]
}
]
}
@@ -28,22 +28,29 @@ final class WordStore {
let data = try Data(contentsOf: url)
let decoded = try JSONDecoder().decode(WordBank.self, from: data)
let categories = decoded.categories.map { category -> WordCategory in
var cat = category
let source: [Word]
if let subs = cat.subcategories, !subs.isEmpty {
source = subs.flatMap { $0.words }
} else {
source = cat.words
}
let tagged: [Word]
if category.id == "numbers" {
var words = NumberWords.makeWords(atoms: category.words)
let extras = category.words.filter { NumberWords.atomNumbers[$0.word] == nil }
var words = NumberWords.makeWords(atoms: source)
let extras = source.filter { NumberWords.atomNumbers[$0.word] == nil }
words.append(contentsOf: extras)
tagged = words
} else {
tagged = category.words
tagged = source
}
let words = tagged.map { word -> Word in
var w = word
w.categoryId = category.id
return w
}
return WordCategory(id: category.id, name: category.name, icon: category.icon,
words: words)
cat.words = words
return cat
}
bank = WordBank(version: decoded.version, categories: categories)
} catch {
@@ -137,13 +137,33 @@ final class WordListViewController: UIViewController {
}
extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
private var sections: [Subcategory]? {
category.subcategories.flatMap { $0.isEmpty ? nil : $0 }
}
func numberOfSections(in tableView: UITableView) -> Int {
sections?.count ?? 1
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
sections?[section].name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
category.words.count
if let sections {
return sections[section].words.count
}
return category.words.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! WordCardCell
let word = category.words[indexPath.row]
let word: Word
if let sections {
word = sections[indexPath.section].words[indexPath.row]
} else {
word = category.words[indexPath.row]
}
cell.configure(with: word)
cell.onSpeak = { word in
SpeechService.shared.speak(word.word)
@@ -158,7 +178,13 @@ extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
Haptics.selection()
let detail = WordDetailViewController(word: category.words[indexPath.row], categoryName: category.name)
let word: Word
if let sections {
word = sections[indexPath.section].words[indexPath.row]
} else {
word = category.words[indexPath.row]
}
let detail = WordDetailViewController(word: word, categoryName: category.name)
navigationController?.pushViewController(detail, animated: true)
}
}