学习记录新增学习次数统计,每次拼写验证自动累加
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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,9 +27,13 @@ final class LearnedStore {
|
||||
}
|
||||
|
||||
func record(_ word: Word) {
|
||||
guard !records.contains(where: { $0.word.id == word.id }) else { return }
|
||||
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 {
|
||||
records.contains { $0.word.id == word.id }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user