学习记录新增日历视图,支持按日期切换查看学习记录
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
import UIKit
|
||||
|
||||
final class HistoryViewController: UITableViewController {
|
||||
private let sections: [(title: String, items: [LearnedRecord])]
|
||||
final class HistoryViewController: UIViewController {
|
||||
private var selectedDate = Date()
|
||||
private var selectedRecords: [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() {
|
||||
sections = LearnedStore.shared.sectioned
|
||||
super.init(style: .plain)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@@ -22,11 +32,11 @@ final class HistoryViewController: UITableViewController {
|
||||
super.viewDidLoad()
|
||||
title = "学习记录"
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
tableView.register(WordCardCell.self, forCellReuseIdentifier: "cell")
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .systemBackground
|
||||
tableView.contentInset.top = 8
|
||||
updateEmptyState()
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
setupCalendar()
|
||||
setupTable()
|
||||
loadRecords(for: selectedDate)
|
||||
|
||||
speechObserver = NotificationCenter.default.addObserver(
|
||||
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
|
||||
@@ -35,22 +45,71 @@ final class HistoryViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
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 viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
calendarView.reloadDecorations(forDateComponents: recordDates(), animated: false)
|
||||
loadRecords(for: selectedDate)
|
||||
}
|
||||
|
||||
private func setupCalendar() {
|
||||
calendarView.delegate = self
|
||||
calendarView.calendar = .current
|
||||
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)
|
||||
])
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -58,22 +117,35 @@ final class HistoryViewController: UITableViewController {
|
||||
cell.updateSpeakerState()
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
sections[section].title
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
sections[section].items.count
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
extension HistoryViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
selectedRecords.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
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.onSpeak = { word in
|
||||
SpeechService.shared.speak(word.word)
|
||||
@@ -86,9 +158,9 @@ final class HistoryViewController: UITableViewController {
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
Haptics.selection()
|
||||
let record = sections[indexPath.section].items[indexPath.row]
|
||||
let record = selectedRecords[indexPath.row]
|
||||
let detail = WordDetailViewController(word: record.word, categoryName: "")
|
||||
navigationController?.pushViewController(detail, animated: true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user