新增学习记录功能,拼写验证后自动记录,首页可查看时间线
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> = []
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user