学习记录新增学习次数统计,每次拼写验证自动累加

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:48:29 +08:00
parent 35300e9aa1
commit f48fd5c3d8
3 changed files with 58 additions and 121 deletions
@@ -2,7 +2,8 @@ import Foundation
struct LearnedRecord: Codable {
let word: Word
let learnedAt: Date
var learnedAt: Date
var count: Int = 1
}
final class LearnedStore {
@@ -26,8 +27,12 @@ final class LearnedStore {
}
func record(_ word: Word) {
guard !records.contains(where: { $0.word.id == word.id }) else { return }
records.append(LearnedRecord(word: word, learnedAt: Date()))
if let index = records.firstIndex(where: { $0.word.id == word.id }) {
records[index].learnedAt = Date()
records[index].count += 1
} else {
records.append(LearnedRecord(word: word, learnedAt: Date()))
}
}
func isRecorded(_ word: Word) -> Bool {
@@ -1,22 +1,12 @@
import UIKit
final class HistoryViewController: UIViewController {
private var selectedDate = Date()
private var selectedRecords: [LearnedRecord] = []
final class HistoryViewController: UITableViewController {
private let sections: [(title: String, items: [LearnedRecord])]
private var speechObserver: NSObjectProtocol?
private let calendarView = UICalendarView()
private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyLabel = UILabel()
private let formatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "zh_CN")
f.dateFormat = "yyyy-MM-dd"
return f
}()
init() {
super.init(nibName: nil, bundle: nil)
sections = LearnedStore.shared.sectioned
super.init(style: .plain)
}
@available(*, unavailable)
@@ -32,122 +22,52 @@ final class HistoryViewController: UIViewController {
super.viewDidLoad()
title = "学习记录"
navigationItem.largeTitleDisplayMode = .never
view.backgroundColor = .systemBackground
tableView.register(WordCardCell.self, forCellReuseIdentifier: "cell")
tableView.separatorStyle = .none
tableView.backgroundColor = .systemBackground
tableView.contentInset.top = 8
setupCalendar()
setupTable()
loadRecords(for: selectedDate)
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
}
speechObserver = NotificationCenter.default.addObserver(
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
) { [weak self] _ in
self?.updateVisibleSpeakers()
for cell in self?.tableView.visibleCells ?? [] {
(cell as? WordCardCell)?.updateSpeakerState()
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
calendarView.reloadDecorations(forDateComponents: recordDates(), animated: false)
loadRecords(for: selectedDate)
override func numberOfSections(in tableView: UITableView) -> Int {
sections.count
}
private func setupCalendar() {
calendarView.delegate = self
calendarView.calendar = .current
calendarView.calendar.firstWeekday = 2
calendarView.locale = Locale(identifier: "zh_CN")
let selection = UICalendarSelectionSingleDate(delegate: self)
calendarView.selectionBehavior = selection
selection.selectedDate = dateComponents(from: selectedDate)
view.addSubview(calendarView)
calendarView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
calendarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
calendarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
calendarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
calendarView.heightAnchor.constraint(equalToConstant: 360)
])
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
sections[section].title
}
private func setupTable() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(WordCardCell.self, forCellReuseIdentifier: "cell")
tableView.separatorStyle = .none
tableView.backgroundColor = .systemBackground
emptyLabel.text = "当天暂无学习记录"
emptyLabel.textAlignment = .center
emptyLabel.textColor = .secondaryLabel
emptyLabel.font = .preferredFont(forTextStyle: .body)
emptyLabel.isHidden = true
tableView.backgroundView = emptyLabel
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: calendarView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
sections[section].items.count
}
private func loadRecords(for date: Date) {
let dateStr = formatter.string(from: date)
let all = LearnedStore.shared.records
selectedRecords = all.filter { formatter.string(from: $0.learnedAt) == dateStr }
.sorted { $0.learnedAt > $1.learnedAt }
emptyLabel.isHidden = !selectedRecords.isEmpty
tableView.reloadData()
}
private func recordDates() -> [DateComponents] {
LearnedStore.shared.records.map {
dateComponents(from: $0.learnedAt)
}
}
private func dateComponents(from date: Date) -> DateComponents {
Calendar.current.dateComponents([.year, .month, .day], from: date)
}
private func updateVisibleSpeakers() {
for cell in tableView.visibleCells.compactMap({ $0 as? WordCardCell }) {
cell.updateSpeakerState()
}
}
}
extension HistoryViewController: UICalendarViewDelegate {
func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
let dates = recordDates()
if dates.contains(where: { $0.year == dateComponents.year && $0.month == dateComponents.month && $0.day == dateComponents.day }) {
return .default(color: Theme.Color.accent, size: .small)
}
return nil
}
}
extension HistoryViewController: UICalendarSelectionSingleDateDelegate {
func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
guard let dateComponents,
let date = Calendar.current.date(from: dateComponents) else { return }
selectedDate = date
loadRecords(for: date)
}
}
extension HistoryViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
selectedRecords.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! WordCardCell
let record = selectedRecords[indexPath.row]
cell.configure(with: record.word)
let record = sections[indexPath.section].items[indexPath.row]
cell.configure(with: record.word, count: record.count)
cell.onSpeak = { word in
SpeechService.shared.speak(word.word)
}
@@ -159,9 +79,9 @@ extension HistoryViewController: UITableViewDataSource, UITableViewDelegate {
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
Haptics.selection()
let record = selectedRecords[indexPath.row]
let record = sections[indexPath.section].items[indexPath.row]
let detail = WordDetailViewController(word: record.word, categoryName: "")
navigationController?.pushViewController(detail, animated: true)
}
@@ -5,6 +5,7 @@ final class WordCardCell: UITableViewCell {
private let wordLabel = UILabel()
private let phoneticLabel = UILabel()
private let meaningLabel = UILabel()
private let countLabel = UILabel()
private let learnButton = UIButton(type: .system)
private let speakerButton = UIButton(type: .system)
private var word: Word?
@@ -37,6 +38,10 @@ final class WordCardCell: UITableViewCell {
meaningLabel.font = .preferredFont(forTextStyle: .subheadline)
meaningLabel.textColor = .secondaryLabel
countLabel.font = .preferredFont(forTextStyle: .caption2)
countLabel.textColor = Theme.Color.accent
countLabel.setContentHuggingPriority(.required, for: .horizontal)
let textStack = UIStackView(arrangedSubviews: [wordLabel, phoneticLabel, meaningLabel])
textStack.axis = .vertical
textStack.spacing = 8
@@ -57,7 +62,7 @@ final class WordCardCell: UITableViewCell {
}, for: .touchUpInside)
speakerButton.setContentHuggingPriority(.required, for: .horizontal)
let row = UIStackView(arrangedSubviews: [textStack, learnButton, speakerButton])
let row = UIStackView(arrangedSubviews: [textStack, countLabel, learnButton, speakerButton])
row.axis = .horizontal
row.alignment = .center
row.spacing = 8
@@ -83,9 +88,16 @@ final class WordCardCell: UITableViewCell {
wordLabel.text = word.word
phoneticLabel.text = word.phonetic
meaningLabel.text = word.meaning
countLabel.isHidden = true
updateSpeakerState()
}
func configure(with word: Word, count: Int) {
configure(with: word)
countLabel.text = "\(count)"
countLabel.isHidden = false
}
func updateSpeakerState() {
guard let word else { return }
let speaking = SpeechService.shared.isSpeaking(word.word)