From f48fd5c3d87e471c854e8e63f354ed3834f10cb5 Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 21 Jul 2026 21:48:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E8=AE=B0=E5=BD=95=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AD=A6=E4=B9=A0=E6=AC=A1=E6=95=B0=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=EF=BC=8C=E6=AF=8F=E6=AC=A1=E6=8B=BC=E5=86=99=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=B4=AF=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../LearnEnglish/Models/LearnedStore.swift | 11 +- .../Views/HistoryViewController.swift | 154 +++++------------- .../LearnEnglish/Views/WordCardCell.swift | 14 +- 3 files changed, 58 insertions(+), 121 deletions(-) diff --git a/LearnEnglish/LearnEnglish/Models/LearnedStore.swift b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift index d057d1a..7841f50 100644 --- a/LearnEnglish/LearnEnglish/Models/LearnedStore.swift +++ b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift @@ -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 { diff --git a/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift b/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift index f6447ee..48d81e6 100644 --- a/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/HistoryViewController.swift @@ -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) } diff --git a/LearnEnglish/LearnEnglish/Views/WordCardCell.swift b/LearnEnglish/LearnEnglish/Views/WordCardCell.swift index fd0fb92..6a8fe10 100644 --- a/LearnEnglish/LearnEnglish/Views/WordCardCell.swift +++ b/LearnEnglish/LearnEnglish/Views/WordCardCell.swift @@ -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)