学习记录新增日历视图,支持按日期切换查看学习记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:35:45 +08:00
parent 673e35aec2
commit ad58d9ea5a
@@ -1,12 +1,22 @@
import UIKit import UIKit
final class HistoryViewController: UITableViewController { final class HistoryViewController: UIViewController {
private let sections: [(title: String, items: [LearnedRecord])] private var selectedDate = Date()
private var selectedRecords: [LearnedRecord] = []
private var speechObserver: NSObjectProtocol? 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() { init() {
sections = LearnedStore.shared.sectioned super.init(nibName: nil, bundle: nil)
super.init(style: .plain)
} }
@available(*, unavailable) @available(*, unavailable)
@@ -22,11 +32,11 @@ final class HistoryViewController: UITableViewController {
super.viewDidLoad() super.viewDidLoad()
title = "学习记录" title = "学习记录"
navigationItem.largeTitleDisplayMode = .never navigationItem.largeTitleDisplayMode = .never
tableView.register(WordCardCell.self, forCellReuseIdentifier: "cell") view.backgroundColor = .systemBackground
tableView.separatorStyle = .none
tableView.backgroundColor = .systemBackground setupCalendar()
tableView.contentInset.top = 8 setupTable()
updateEmptyState() loadRecords(for: selectedDate)
speechObserver = NotificationCenter.default.addObserver( speechObserver = NotificationCenter.default.addObserver(
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
@@ -35,45 +45,107 @@ final class HistoryViewController: UITableViewController {
} }
} }
private func updateEmptyState() { override func viewWillAppear(_ animated: Bool) {
if sections.isEmpty { super.viewWillAppear(animated)
let label = UILabel() calendarView.reloadDecorations(forDateComponents: recordDates(), animated: false)
label.text = "暂无学习记录" loadRecords(for: selectedDate)
label.textAlignment = .center }
label.textColor = .secondaryLabel
label.font = .preferredFont(forTextStyle: .body) private func setupCalendar() {
let bg = UIView() calendarView.delegate = self
bg.addSubview(label) calendarView.calendar = .current
label.translatesAutoresizingMaskIntoConstraints = false calendarView.locale = Locale(identifier: "zh_CN")
NSLayoutConstraint.activate([ let selection = UICalendarSelectionSingleDate(delegate: self)
label.centerXAnchor.constraint(equalTo: bg.centerXAnchor), calendarView.selectionBehavior = selection
label.centerYAnchor.constraint(equalTo: bg.centerYAnchor) selection.selectedDate = dateComponents(from: selectedDate)
])
tableView.backgroundView = bg 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)
])
}
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)
])
}
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() { private func updateVisibleSpeakers() {
for cell in tableView.visibleCells.compactMap({ $0 as? WordCardCell }) { for cell in tableView.visibleCells.compactMap({ $0 as? WordCardCell }) {
cell.updateSpeakerState() cell.updateSpeakerState()
} }
} }
}
override func numberOfSections(in tableView: UITableView) -> Int { extension HistoryViewController: UICalendarViewDelegate {
sections.count 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
} }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
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) as! WordCardCell let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! WordCardCell
let record = sections[indexPath.section].items[indexPath.row] let record = selectedRecords[indexPath.row]
cell.configure(with: record.word) cell.configure(with: record.word)
cell.onSpeak = { word in cell.onSpeak = { word in
SpeechService.shared.speak(word.word) SpeechService.shared.speak(word.word)
@@ -86,9 +158,9 @@ final class HistoryViewController: UITableViewController {
return cell return cell
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
Haptics.selection() Haptics.selection()
let record = sections[indexPath.section].items[indexPath.row] let record = selectedRecords[indexPath.row]
let detail = WordDetailViewController(word: record.word, categoryName: "") let detail = WordDetailViewController(word: record.word, categoryName: "")
navigationController?.pushViewController(detail, animated: true) navigationController?.pushViewController(detail, animated: true)
} }