From 106b60b58f826a3b1562ba97d7134e35c5572dad Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 21 Jul 2026 21:57:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=95=E8=AF=8D=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E5=8F=AF=E6=9F=A5=E7=9C=8B=E5=8D=95=E4=B8=AA?= =?UTF-8?q?=E5=8D=95=E8=AF=8D=E7=9A=84=E5=AD=A6=E4=B9=A0=E7=BB=9F=E8=AE=A1?= 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 | 4 + .../Views/WordDetailViewController.swift | 9 +- .../Views/WordHistoryViewController.swift | 146 ++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 LearnEnglish/LearnEnglish/Views/WordHistoryViewController.swift diff --git a/LearnEnglish/LearnEnglish/Models/LearnedStore.swift b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift index 303d8da..bccdcf9 100644 --- a/LearnEnglish/LearnEnglish/Models/LearnedStore.swift +++ b/LearnEnglish/LearnEnglish/Models/LearnedStore.swift @@ -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 { records.contains { $0.word.id == word.id } } diff --git a/LearnEnglish/LearnEnglish/Views/WordDetailViewController.swift b/LearnEnglish/LearnEnglish/Views/WordDetailViewController.swift index ce74fcc..9c6928f 100644 --- a/LearnEnglish/LearnEnglish/Views/WordDetailViewController.swift +++ b/LearnEnglish/LearnEnglish/Views/WordDetailViewController.swift @@ -31,7 +31,10 @@ final class WordDetailViewController: UIViewController { view.backgroundColor = .systemBackground title = categoryName 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() updateSpeakState() speechObserver = NotificationCenter.default.addObserver( @@ -187,6 +190,10 @@ final class WordDetailViewController: UIViewController { return card } + @objc private func showHistory() { + navigationController?.pushViewController(WordHistoryViewController(word: word), animated: true) + } + @objc private func startSpelling() { Haptics.selection() let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true) diff --git a/LearnEnglish/LearnEnglish/Views/WordHistoryViewController.swift b/LearnEnglish/LearnEnglish/Views/WordHistoryViewController.swift new file mode 100644 index 0000000..adaed13 --- /dev/null +++ b/LearnEnglish/LearnEnglish/Views/WordHistoryViewController.swift @@ -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 + } +}