From 41841896dd4905b8a03d45767540d9b7cb88df65 Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 21 Jul 2026 21:29:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AD=A6=E4=B9=A0=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=8B=BC=E5=86=99=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=90=8E=E8=87=AA=E5=8A=A8=E8=AE=B0=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=8F=AF=E6=9F=A5=E7=9C=8B=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../LearnEnglish/Models/LearnedStore.swift | 73 +++++++++++++++++++ .../Views/CategoryListViewController.swift | 10 +++ .../Views/HistoryViewController.swift | 71 ++++++++++++++++++ .../Views/QuizViewController.swift | 1 + 4 files changed, 155 insertions(+) create mode 100644 LearnEnglish/LearnEnglish/Models/LearnedStore.swift create mode 100644 LearnEnglish/LearnEnglish/Views/HistoryViewController.swift diff --git a/LearnEnglish/LearnEnglish/Models/LearnedStore.swift b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift new file mode 100644 index 0000000..d057d1a --- /dev/null +++ b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift @@ -0,0 +1,73 @@ +import Foundation + +struct LearnedRecord: Codable { + let word: Word + let learnedAt: Date +} + +final class LearnedStore { + static let shared = LearnedStore() + private let key = "learned_records" + + private(set) var records: [LearnedRecord] { + didSet { + guard let data = try? JSONEncoder().encode(records) else { return } + UserDefaults.standard.set(data, forKey: key) + } + } + + private init() { + if let data = UserDefaults.standard.data(forKey: key), + let decoded = try? JSONDecoder().decode([LearnedRecord].self, from: data) { + records = decoded + } else { + records = [] + } + } + + func record(_ word: Word) { + guard !records.contains(where: { $0.word.id == word.id }) else { return } + records.append(LearnedRecord(word: word, learnedAt: Date())) + } + + func isRecorded(_ word: Word) -> Bool { + records.contains { $0.word.id == word.id } + } + + var sectioned: [(title: String, items: [LearnedRecord])] { + let sorted = records.sorted { $0.learnedAt > $1.learnedAt } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "zh_CN") + formatter.dateFormat = "yyyy-MM-dd" + + var groups: [(String, [LearnedRecord])] = [] + var seen: Set = [] + for record in sorted { + let dateStr = formatter.string(from: record.learnedAt) + let title = sectionTitle(dateStr) + if !seen.contains(title) { + groups.append((title, [])) + seen.insert(title) + } + if let index = groups.firstIndex(where: { $0.0 == title }) { + groups[index].1.append(record) + } + } + return groups + } + + private func sectionTitle(_ dateStr: String) -> String { + let today = DateFormatter() + today.locale = Locale(identifier: "zh_CN") + today.dateFormat = "yyyy-MM-dd" + let todayStr = today.string(from: Date()) + if dateStr == todayStr { return "今天" } + + let calendar = Calendar.current + if let yesterday = calendar.date(byAdding: .day, value: -1, to: Date()) { + let yesterdayStr = today.string(from: yesterday) + if dateStr == yesterdayStr { return "昨天" } + } + return dateStr + } +} diff --git a/LearnEnglish/LearnEnglish/Views/CategoryListViewController.swift b/LearnEnglish/LearnEnglish/Views/CategoryListViewController.swift index 446564b..b681cfb 100644 --- a/LearnEnglish/LearnEnglish/Views/CategoryListViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/CategoryListViewController.swift @@ -14,6 +14,12 @@ final class CategoryListViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() title = "背单词" + navigationItem.rightBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "list.bullet.clipboard"), + style: .plain, + target: self, + action: #selector(showHistory) + ) tableView.register(CategoryCardCell.self, forCellReuseIdentifier: "cell") tableView.separatorStyle = .none tableView.backgroundColor = .systemBackground @@ -23,6 +29,10 @@ final class CategoryListViewController: UITableViewController { } } + @objc private func showHistory() { + navigationController?.pushViewController(HistoryViewController(), animated: true) + } + private func showError(_ message: String) { let label = UILabel() label.text = "词库加载失败\n\(message)" diff --git a/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift b/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift new file mode 100644 index 0000000..e154618 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift @@ -0,0 +1,71 @@ +import UIKit + +final class HistoryViewController: UITableViewController { + private let sections: [(title: String, items: [LearnedRecord])] + + init() { + sections = LearnedStore.shared.sectioned + super.init(style: .insetGrouped) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + title = "学习记录" + navigationItem.largeTitleDisplayMode = .never + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + tableView.backgroundColor = .systemBackground + updateEmptyState() + } + + private func updateEmptyState() { + if sections.isEmpty { + let label = UILabel() + label.text = "暂无学习记录" + label.textAlignment = .center + label.textColor = .secondaryLabel + label.font = .preferredFont(forTextStyle: .body) + let bg = UIView() + bg.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: bg.centerXAnchor), + label.centerYAnchor.constraint(equalTo: bg.centerYAnchor) + ]) + tableView.backgroundView = bg + } + } + + override func numberOfSections(in tableView: UITableView) -> Int { + sections.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + sections[section].title + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + sections[section].items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + let record = sections[indexPath.section].items[indexPath.row] + var config = cell.defaultContentConfiguration() + config.text = record.word.word + config.secondaryText = "\(record.word.phonetic) \(record.word.meaning)" + config.image = UIImage(systemName: "text.book.closed") + config.imageProperties.tintColor = Theme.Color.accent + cell.contentConfiguration = config + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + Haptics.selection() + let record = sections[indexPath.section].items[indexPath.row] + let detail = WordDetailViewController(word: record.word, categoryName: "") + navigationController?.pushViewController(detail, animated: true) + } +} diff --git a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift index 1d96773..55ff548 100644 --- a/LearnEnglish/LearnEnglish/Views/QuizViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/QuizViewController.swift @@ -357,6 +357,7 @@ final class QuizViewController: UIViewController { private func submitSpelling(_ typed: String, for question: QuizQuestion) { guard selectedOption == nil else { return } selectedOption = typed + LearnedStore.shared.record(question.word) let correct = typed.lowercased() == question.answer.lowercased() if !correct { wrongWords.append(question.word)