单词详情页新增历史记录入口,可查看单个单词的学习统计
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,10 @@ final class LearnedStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func record(for word: Word) -> LearnedRecord? {
|
||||||
|
records.first { $0.word.id == word.id }
|
||||||
|
}
|
||||||
|
|
||||||
func isRecorded(_ word: Word) -> Bool {
|
func isRecorded(_ word: Word) -> Bool {
|
||||||
records.contains { $0.word.id == word.id }
|
records.contains { $0.word.id == word.id }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ final class WordDetailViewController: UIViewController {
|
|||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
title = categoryName
|
title = categoryName
|
||||||
navigationItem.largeTitleDisplayMode = .never
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "学习", style: .plain, target: self, action: #selector(startSpelling))
|
navigationItem.rightBarButtonItems = [
|
||||||
|
UIBarButtonItem(image: UIImage(systemName: "clock.arrow.circlepath"), style: .plain, target: self, action: #selector(showHistory)),
|
||||||
|
UIBarButtonItem(title: "学习", style: .plain, target: self, action: #selector(startSpelling))
|
||||||
|
]
|
||||||
buildLayout()
|
buildLayout()
|
||||||
updateSpeakState()
|
updateSpeakState()
|
||||||
speechObserver = NotificationCenter.default.addObserver(
|
speechObserver = NotificationCenter.default.addObserver(
|
||||||
@@ -187,6 +190,10 @@ final class WordDetailViewController: UIViewController {
|
|||||||
return card
|
return card
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func showHistory() {
|
||||||
|
navigationController?.pushViewController(WordHistoryViewController(word: word), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func startSpelling() {
|
@objc private func startSpelling() {
|
||||||
Haptics.selection()
|
Haptics.selection()
|
||||||
let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true)
|
let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true)
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class WordHistoryViewController: UIViewController {
|
||||||
|
private let word: Word
|
||||||
|
private let record: LearnedRecord?
|
||||||
|
|
||||||
|
init(word: Word) {
|
||||||
|
self.word = word
|
||||||
|
record = LearnedStore.shared.record(for: word)
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
title = "学习记录"
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
buildLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildLayout() {
|
||||||
|
guard let record else {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "暂无学习记录"
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
|
view.addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let wordCard = makeWordCard(record)
|
||||||
|
let statsCard = makeStatsCard(record)
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [wordCard, statsCard])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 20
|
||||||
|
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
scrollView.alwaysBounceVertical = true
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
scrollView.addSubview(stack)
|
||||||
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 20),
|
||||||
|
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
|
||||||
|
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeWordCard(_ record: LearnedRecord) -> UIView {
|
||||||
|
let card = UIView()
|
||||||
|
card.backgroundColor = Theme.Color.cardBackground
|
||||||
|
card.layer.cornerRadius = Theme.Metrics.cardRadius
|
||||||
|
|
||||||
|
let wordLabel = UILabel()
|
||||||
|
wordLabel.text = record.word.word
|
||||||
|
wordLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
|
||||||
|
let phoneticLabel = UILabel()
|
||||||
|
phoneticLabel.text = record.word.phonetic
|
||||||
|
phoneticLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
phoneticLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let meaningLabel = UILabel()
|
||||||
|
meaningLabel.text = record.word.meaning
|
||||||
|
meaningLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
meaningLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let textStack = UIStackView(arrangedSubviews: [wordLabel, phoneticLabel, meaningLabel])
|
||||||
|
textStack.axis = .vertical
|
||||||
|
textStack.spacing = 8
|
||||||
|
|
||||||
|
card.addSubview(textStack)
|
||||||
|
textStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
textStack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
||||||
|
textStack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
|
||||||
|
textStack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
|
||||||
|
textStack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14)
|
||||||
|
])
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeStatsCard(_ record: LearnedRecord) -> UIView {
|
||||||
|
let card = UIView()
|
||||||
|
card.backgroundColor = Theme.Color.cardBackground
|
||||||
|
card.layer.cornerRadius = Theme.Metrics.cardRadius
|
||||||
|
|
||||||
|
let title = UILabel()
|
||||||
|
title.text = "学习统计"
|
||||||
|
title.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
title.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let countLabel = UILabel()
|
||||||
|
countLabel.text = "\(record.count) 次"
|
||||||
|
countLabel.font = .systemFont(ofSize: 36, weight: .bold)
|
||||||
|
|
||||||
|
let accuracyLabel = UILabel()
|
||||||
|
accuracyLabel.text = "正确率 \(record.accuracy)%"
|
||||||
|
accuracyLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
accuracyLabel.textColor = Theme.Color.correct
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
||||||
|
let dateLabel = UILabel()
|
||||||
|
dateLabel.text = "最近学习:\(formatter.string(from: record.learnedAt))"
|
||||||
|
dateLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
dateLabel.textColor = .tertiaryLabel
|
||||||
|
|
||||||
|
let statsStack = UIStackView(arrangedSubviews: [countLabel, accuracyLabel])
|
||||||
|
statsStack.axis = .horizontal
|
||||||
|
statsStack.spacing = 16
|
||||||
|
statsStack.alignment = .lastBaseline
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [title, statsStack, dateLabel])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 12
|
||||||
|
|
||||||
|
card.addSubview(stack)
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
|
||||||
|
stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16)
|
||||||
|
])
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user