学习记录新增日历视图,支持按日期切换查看学习记录
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user